airwallex 0.7.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Represents an offboarding request for a Connected Account. Always
5
+ # accessed through its parent ConnectedAccount, since its API path is
6
+ # scoped by account_id.
7
+ #
8
+ # This gem currently only implements the sandbox Simulation endpoints for
9
+ # completing/cancelling an offboarding; it does not yet implement the live
10
+ # create/retrieve/list endpoints (`/api/v1/accounts/{account_id}/offboardings`).
11
+ # See https://www.airwallex.com/docs/api/simulation/connected-account-offboarding
12
+ #
13
+ # @example Simulate an offboarding finishing
14
+ # Airwallex::AccountOffboarding.simulate_complete("acct_123", "obd_456")
15
+ class AccountOffboarding < APIResource
16
+ # @param account_id [String] the parent ConnectedAccount's id
17
+ # @return [String] API resource path for this account's simulated
18
+ # offboarding actions
19
+ def self.resource_path(account_id)
20
+ "/api/v1/simulation/accounts/#{account_id}/offboardings"
21
+ end
22
+
23
+ # Simulate an offboarding completing
24
+ #
25
+ # @param account_id [String]
26
+ # @param offboarding_id [String]
27
+ # @return [AccountOffboarding]
28
+ def self.simulate_complete(account_id, offboarding_id)
29
+ response = Airwallex.client.post("#{resource_path(account_id)}/#{offboarding_id}/complete", {})
30
+ new(response)
31
+ end
32
+
33
+ # Simulate an offboarding being cancelled
34
+ #
35
+ # @param account_id [String]
36
+ # @param offboarding_id [String]
37
+ # @return [AccountOffboarding]
38
+ def self.simulate_cancel(account_id, offboarding_id)
39
+ response = Airwallex.client.post("#{resource_path(account_id)}/#{offboarding_id}/cancel", {})
40
+ new(response)
41
+ end
42
+ end
43
+ end
@@ -64,36 +64,36 @@ module Airwallex
64
64
  # method) before attempting to create the beneficiary.
65
65
  #
66
66
  # @param params [Hash] the same shape of params you'd pass to .create
67
- # @return [Hash] raw validation result
67
+ # @return [Hash] symbolized validation result
68
68
  def self.validate(params = {})
69
- Airwallex.client.post("#{resource_path}/validate", params)
69
+ symbolized_post("#{resource_path}/validate", params)
70
70
  end
71
71
 
72
72
  # Verify ownership of a beneficiary's bank account via Confirmation of
73
73
  # Payee (CoP) before creation.
74
74
  #
75
75
  # @param params [Hash] beneficiary bank account details to verify
76
- # @return [Hash] raw verification result (status, account_name_match_result)
76
+ # @return [Hash] symbolized verification result (status, account_name_match_result)
77
77
  def self.verify_account(params = {})
78
- Airwallex.client.post("#{resource_path}/verify_account", params)
78
+ symbolized_post("#{resource_path}/verify_account", params)
79
79
  end
80
80
 
81
81
  # Retrieve the API schema (field validation rules) for beneficiary bank
82
82
  # details, keyed by beneficiary type / entity type / bank country.
83
83
  #
84
84
  # @param params [Hash] e.g. beneficiary_type:, bank_country_code:
85
- # @return [Hash] raw schema response
85
+ # @return [Hash] symbolized schema response
86
86
  def self.api_schema(params = {})
87
- Airwallex.client.post(API_SCHEMA_PATH, params)
87
+ symbolized_post(API_SCHEMA_PATH, params)
88
88
  end
89
89
 
90
90
  # Retrieve the dynamic form schema used to render beneficiary bank-detail
91
91
  # forms in the UI, keyed by beneficiary type / entity type / bank country.
92
92
  #
93
93
  # @param params [Hash] e.g. beneficiary_type:, bank_country_code:
94
- # @return [Hash] raw schema response
94
+ # @return [Hash] symbolized schema response
95
95
  def self.form_schema(params = {})
96
- Airwallex.client.post(FORM_SCHEMA_PATH, params)
96
+ symbolized_post(FORM_SCHEMA_PATH, params)
97
97
  end
98
98
 
99
99
  # Search financial institutions supported for a given country, currency,
@@ -102,9 +102,9 @@ module Airwallex
102
102
  # @param params [Hash] required: bank_country_code:, account_currency:,
103
103
  # entity_type:, transfer_method:, keyword: (a bank-name search term,
104
104
  # min 3 chars)
105
- # @return [Hash] raw response listing supported institutions
105
+ # @return [Hash] symbolized response listing supported institutions
106
106
  def self.supported_financial_institutions(params = {})
107
- Airwallex.client.get(SUPPORTED_FINANCIAL_INSTITUTIONS_PATH, params)
107
+ symbolized_get(SUPPORTED_FINANCIAL_INSTITUTIONS_PATH, params)
108
108
  end
109
109
  end
110
110
  end
@@ -40,9 +40,9 @@ module Airwallex
40
40
  # Retrieve the bank transfer instructions for funding this customer's
41
41
  # subscriptions (e.g. for a BT-funded instalment plan).
42
42
  #
43
- # @return [Hash] raw bank transfer instructions
43
+ # @return [Hash] symbolized bank transfer instructions
44
44
  def bank_transfer_instructions
45
- Airwallex.client.get("#{self.class.resource_path}/#{id}/bank_transfer_instructions")
45
+ self.class.symbolized_get("#{self.class.resource_path}/#{id}/bank_transfer_instructions")
46
46
  end
47
47
  end
48
48
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Represents an Issuing Cardholder.
5
+ #
6
+ # This gem currently only implements the sandbox Simulation endpoint for
7
+ # bypassing a cardholder's RFI review stage; it does not yet implement the
8
+ # live create/retrieve/list endpoints.
9
+ # See https://www.airwallex.com/docs/api/simulation/issuing-cardholders
10
+ #
11
+ # @example Bypass a cardholder's pending review
12
+ # Airwallex::Cardholder.simulate_pass_review("chd_123")
13
+ class Cardholder < APIResource
14
+ # @return [String] API resource path for simulated cardholder actions
15
+ def self.resource_path
16
+ "/api/v1/simulation/issuing/cardholders"
17
+ end
18
+
19
+ # Simulate bypassing this cardholder's RFI review stage
20
+ #
21
+ # @param cardholder_id [String]
22
+ # @return [Cardholder]
23
+ def self.simulate_pass_review(cardholder_id)
24
+ response = Airwallex.client.post("#{resource_path}/#{cardholder_id}/pass_review", {})
25
+ new(response)
26
+ end
27
+ end
28
+ end
@@ -57,11 +57,29 @@ module Airwallex
57
57
  CURRENT_ACCOUNT_PATH = "/api/v1/account"
58
58
  WALLET_INFO_PATH = "/api/v1/account/wallet_info"
59
59
 
60
+ # Sandbox-only — see https://www.airwallex.com/docs/api/simulation/connected-accounts
61
+ SIMULATION_PATH = "/api/v1/simulation/accounts"
62
+
60
63
  # @return [String] API resource path for connected accounts
61
64
  def self.resource_path
62
65
  "/api/v1/accounts"
63
66
  end
64
67
 
68
+ # Simulate this account's status transitioning. The account must
69
+ # currently be SUBMITTED.
70
+ #
71
+ # @param account_id [String]
72
+ # @param next_status [String] one of "ACTIVE", "SUSPENDED",
73
+ # "ACTION_REQUIRED"
74
+ # @param force [Boolean] defaults to true
75
+ # @return [ConnectedAccount]
76
+ def self.simulate_update_status(account_id, next_status:, force: true)
77
+ response = Airwallex.client.post(
78
+ "#{SIMULATION_PATH}/#{account_id}/update_status", next_status: next_status, force: force
79
+ )
80
+ new(response)
81
+ end
82
+
65
83
  # Retrieve whichever account the current request is authenticated as
66
84
  #
67
85
  # @param params [Hash] additional params
@@ -75,9 +93,9 @@ module Airwallex
75
93
  # authenticated as
76
94
  #
77
95
  # @param params [Hash] additional params
78
- # @return [Hash] raw wallet info response
96
+ # @return [Hash] symbolized wallet info response
79
97
  def self.wallet_info(params = {})
80
- Airwallex.client.get(WALLET_INFO_PATH, params)
98
+ symbolized_get(WALLET_INFO_PATH, params)
81
99
  end
82
100
 
83
101
  # The legal_entity_id needed by BillingCustomer.create's
@@ -132,5 +150,36 @@ module Airwallex
132
150
  refresh_from(response)
133
151
  self
134
152
  end
153
+
154
+ # Simulate this account's status transitioning. The account must
155
+ # currently be SUBMITTED.
156
+ #
157
+ # @param next_status [String] one of "ACTIVE", "SUSPENDED",
158
+ # "ACTION_REQUIRED"
159
+ # @param force [Boolean] defaults to true
160
+ # @return [ConnectedAccount] self
161
+ def simulate_update_status(next_status:, force: true)
162
+ response = Airwallex.client.post(
163
+ "#{self.class::SIMULATION_PATH}/#{id}/update_status", next_status: next_status, force: force
164
+ )
165
+ refresh_from(response)
166
+ self
167
+ end
168
+
169
+ # Simulate a pending offboarding on this account completing
170
+ #
171
+ # @param offboarding_id [String]
172
+ # @return [AccountOffboarding]
173
+ def simulate_complete_offboarding(offboarding_id)
174
+ AccountOffboarding.simulate_complete(id, offboarding_id)
175
+ end
176
+
177
+ # Simulate a pending offboarding on this account being cancelled
178
+ #
179
+ # @param offboarding_id [String]
180
+ # @return [AccountOffboarding]
181
+ def simulate_cancel_offboarding(offboarding_id)
182
+ AccountOffboarding.simulate_cancel(id, offboarding_id)
183
+ end
135
184
  end
136
185
  end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Represents a Deposit — funds arriving in your Airwallex account, either
5
+ # as an inbound bank transfer into a Global Account (type: BANK_TRANSFER)
6
+ # or a Direct Debit pull from a verified LinkedAccount (type:
7
+ # DIRECT_DEBIT, made via .create).
8
+ #
9
+ # The sandbox Simulation actions split cleanly along that same line:
10
+ # .simulate_create simulates a BANK_TRANSFER deposit landing, and it
11
+ # auto-settles on its own within a few seconds — same as a real bank
12
+ # transfer, there's no PENDING state to force through. .simulate_settle/
13
+ # .simulate_reject/.simulate_reverse only operate on DIRECT_DEBIT
14
+ # deposits made via .create, since a real direct debit pull takes days
15
+ # to clear and the sandbox has no other way to resolve it. Calling
16
+ # settle/reject/reverse on a BANK_TRANSFER deposit's id returns a 404
17
+ # "Deposit does not exist" — confirmed against the real sandbox.
18
+ # See https://www.airwallex.com/docs/api/simulation/deposits
19
+ #
20
+ # @example Simulate an inbound bank transfer landing (settles on its own)
21
+ # Airwallex::Deposit.simulate_create(amount: 100.00, global_account_id: "gacc_123")
22
+ #
23
+ # @example Pull via Direct Debit, then force it through the sandbox lifecycle
24
+ # deposit = Airwallex::Deposit.create(
25
+ # funding_source_id: "la_123", # a verified LinkedAccount id
26
+ # amount: 50.00,
27
+ # currency: "AUD"
28
+ # )
29
+ # deposit.simulate_settle
30
+ class Deposit < APIResource
31
+ extend APIOperations::Create
32
+ extend APIOperations::Retrieve
33
+ extend APIOperations::List
34
+
35
+ # Airwallex's own API is inconsistent here: simulating a bank-transfer
36
+ # deposit landing uses the singular "deposit", while every other
37
+ # simulation action on an existing deposit uses the plural "deposits".
38
+ SIMULATE_CREATE_PATH = "/api/v1/simulation/deposit/create"
39
+ SIMULATION_PATH = "/api/v1/simulation/deposits"
40
+
41
+ # @return [String] API resource path for deposits
42
+ def self.resource_path
43
+ "/api/v1/deposits"
44
+ end
45
+
46
+ # Simulate an inbound bank-transfer deposit landing in a Global
47
+ # Account
48
+ #
49
+ # @param params [Hash] amount:, global_account_id: (required);
50
+ # payer_bankname:, payer_country:, payer_name:, reference:,
51
+ # statement_ref:, status: ("PENDING", "REJECTED", or "SETTLED",
52
+ # defaults to "SETTLED") (optional)
53
+ # @return [Deposit]
54
+ def self.simulate_create(params = {})
55
+ response = Airwallex.client.post(SIMULATE_CREATE_PATH, params)
56
+ new(response)
57
+ end
58
+
59
+ # Simulate a PENDING Direct Debit deposit (made via .create) settling
60
+ #
61
+ # @param deposit_id [String]
62
+ # @return [Deposit]
63
+ def self.simulate_settle(deposit_id)
64
+ response = Airwallex.client.post("#{SIMULATION_PATH}/#{deposit_id}/settle", {})
65
+ new(response)
66
+ end
67
+
68
+ # Simulate a PENDING Direct Debit deposit (made via .create) being
69
+ # rejected
70
+ #
71
+ # @param deposit_id [String]
72
+ # @return [Deposit]
73
+ def self.simulate_reject(deposit_id)
74
+ response = Airwallex.client.post("#{SIMULATION_PATH}/#{deposit_id}/reject", {})
75
+ new(response)
76
+ end
77
+
78
+ # Simulate reversing a SETTLED Direct Debit deposit (creates an
79
+ # offsetting settled deposit and deactivates the LinkedAccount)
80
+ #
81
+ # @param deposit_id [String]
82
+ # @return [Deposit]
83
+ def self.simulate_reverse(deposit_id)
84
+ response = Airwallex.client.post("#{SIMULATION_PATH}/#{deposit_id}/reverse", {})
85
+ new(response)
86
+ end
87
+
88
+ # Simulate this Direct Debit deposit settling
89
+ #
90
+ # @return [Deposit] self
91
+ def simulate_settle
92
+ response = Airwallex.client.post("#{self.class::SIMULATION_PATH}/#{id}/settle", {})
93
+ refresh_from(response)
94
+ self
95
+ end
96
+
97
+ # Simulate this Direct Debit deposit being rejected
98
+ #
99
+ # @return [Deposit] self
100
+ def simulate_reject
101
+ response = Airwallex.client.post("#{self.class::SIMULATION_PATH}/#{id}/reject", {})
102
+ refresh_from(response)
103
+ self
104
+ end
105
+
106
+ # Simulate reversing this Direct Debit deposit
107
+ #
108
+ # @return [Deposit] self
109
+ def simulate_reverse
110
+ response = Airwallex.client.post("#{self.class::SIMULATION_PATH}/#{id}/reverse", {})
111
+ refresh_from(response)
112
+ self
113
+ end
114
+ end
115
+ end
@@ -5,7 +5,11 @@ module Airwallex
5
5
  #
6
6
  # Disputes represent chargebacks or payment disputes initiated by cardholders.
7
7
  # Merchants can view disputes, challenge them with evidence, or accept them.
8
- # There is no create — disputes originate from card networks/issuing banks.
8
+ # There is no live create — real disputes originate from card
9
+ # networks/issuing banks. In the sandbox, `.simulate_create` (and
10
+ # `#simulate_escalate`/`#simulate_resolve`) stand in for the card
11
+ # network/issuing bank to drive the full dispute lifecycle for testing.
12
+ # See https://www.airwallex.com/docs/api/simulation/payment-disputes
9
13
  #
10
14
  # @example List open disputes
11
15
  # disputes = Airwallex::Dispute.list(status: 'OPEN')
@@ -18,15 +22,70 @@ module Airwallex
18
22
  #
19
23
  # @example Challenge a dispute
20
24
  # dispute.challenge(...)
25
+ #
26
+ # @example Simulate the full sandbox lifecycle
27
+ # dispute = Airwallex::Dispute.simulate_create(
28
+ # payment_intent_id: "int_123",
29
+ # reason_code: "4853",
30
+ # stage: "CHARGEBACK",
31
+ # due_at: "2026-12-01T23:59:59Z"
32
+ # )
33
+ # dispute.challenge(customer_communication: "Email thread")
34
+ # dispute.simulate_resolve(in_favor_of: "MERCHANT")
21
35
  class Dispute < APIResource
22
36
  extend APIOperations::Retrieve
23
37
  extend APIOperations::List
24
38
  include APIOperations::Update
25
39
 
40
+ # Sandbox-only — see https://www.airwallex.com/docs/api/simulation/payment-disputes
41
+ SIMULATION_PATH = "/api/v1/simulation/pa/payment_disputes"
42
+
26
43
  def self.resource_path
27
44
  "/api/v1/pa/payment_disputes"
28
45
  end
29
46
 
47
+ # Simulate a card network/issuing bank raising a dispute against a
48
+ # PaymentIntent
49
+ #
50
+ # @param params [Hash] payment_intent_id:, reason_code: (card-brand
51
+ # specific, e.g. Mastercard "4853", Visa "10.4"), stage: (one of
52
+ # "RFI", "PRE_CHARGEBACK", "CHARGEBACK", "PRE_ARBITRATION",
53
+ # "ARBITRATION"), due_at: (required); amount:, comment:, documents:
54
+ # (optional)
55
+ # @return [Dispute]
56
+ def self.simulate_create(params = {})
57
+ response = Airwallex.client.post("#{SIMULATION_PATH}/create", params)
58
+ new(response)
59
+ end
60
+
61
+ # Simulate the issuing bank rejecting the merchant's challenge evidence
62
+ # and advancing the dispute to the next stage (e.g. Chargeback ->
63
+ # Pre-arbitration). Not valid while status is REQUIRES_RESPONSE — the
64
+ # merchant must #accept or #challenge first.
65
+ #
66
+ # @param dispute_id [String]
67
+ # @param params [Hash] due_at: (required); amount:, comment:,
68
+ # documents: (optional)
69
+ # @return [Dispute]
70
+ def self.simulate_escalate(dispute_id, params = {})
71
+ response = Airwallex.client.post("#{SIMULATION_PATH}/#{dispute_id}/escalate", params)
72
+ new(response)
73
+ end
74
+
75
+ # Simulate the issuing bank's final decision on a dispute.
76
+ # in_favor_of: "MERCHANT" resolves it WON/REVERSED; "CUSTOMER" resolves
77
+ # it LOST. Not valid while status is REQUIRES_RESPONSE — the merchant
78
+ # must #accept or #challenge first.
79
+ #
80
+ # @param dispute_id [String]
81
+ # @param params [Hash] in_favor_of: ("MERCHANT" or "CUSTOMER",
82
+ # required); amount: (optional, defaults to the full disputed amount)
83
+ # @return [Dispute]
84
+ def self.simulate_resolve(dispute_id, params = {})
85
+ response = Airwallex.client.post("#{SIMULATION_PATH}/#{dispute_id}/resolve", params)
86
+ new(response)
87
+ end
88
+
30
89
  # Accept a dispute without challenging it
31
90
  #
32
91
  # @return [Airwallex::Dispute] self
@@ -47,6 +106,29 @@ module Airwallex
47
106
  self
48
107
  end
49
108
 
109
+ # Simulate the issuing bank rejecting this dispute's challenge evidence
110
+ # and advancing it to the next stage
111
+ #
112
+ # @param params [Hash] due_at: (required); amount:, comment:,
113
+ # documents: (optional)
114
+ # @return [Airwallex::Dispute] self
115
+ def simulate_escalate(params = {})
116
+ response = Airwallex.client.post("#{self.class::SIMULATION_PATH}/#{id}/escalate", params)
117
+ refresh_from(response)
118
+ self
119
+ end
120
+
121
+ # Simulate the issuing bank's final decision on this dispute
122
+ #
123
+ # @param params [Hash] in_favor_of: ("MERCHANT" or "CUSTOMER",
124
+ # required); amount: (optional)
125
+ # @return [Airwallex::Dispute] self
126
+ def simulate_resolve(params = {})
127
+ response = Airwallex.client.post("#{self.class::SIMULATION_PATH}/#{id}/resolve", params)
128
+ refresh_from(response)
129
+ self
130
+ end
131
+
50
132
  # List payment intents related to this dispute
51
133
  #
52
134
  # @param params [Hash] additional query params (e.g. pagination)
@@ -45,9 +45,9 @@ module Airwallex
45
45
  # @param params [Hash] account_statement_type: (e.g. "AMAZON"),
46
46
  # registration_info: { agreement:, registered_name:, registered_email:,
47
47
  # registered_address: { address:, city:, state:, postcode:, country: } }
48
- # @return [Hash] raw response (the generated letter/document reference)
48
+ # @return [Hash] symbolized response (the generated letter/document reference)
49
49
  def generate_statement_letter(params = {})
50
- Airwallex.client.post("#{self.class.resource_path}/#{id}/generate_statement_letter", params)
50
+ self.class.symbolized_post("#{self.class.resource_path}/#{id}/generate_statement_letter", params)
51
51
  end
52
52
 
53
53
  # List inbound transactions (deposits) received into this account
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Represents a card (Issuing) transaction event.
5
+ #
6
+ # This gem currently only implements the sandbox Simulation endpoints for
7
+ # issuing transactions — creating an authorization, capturing/reversing a
8
+ # PENDING one, refunding a CAPTURED one, and delivering a 3DS delegation
9
+ # notification. It does not yet implement the live Issuing resources
10
+ # (Card, Cardholder) or the live transaction retrieve/list endpoints.
11
+ # See https://www.airwallex.com/docs/api/simulation/issuing-transactions
12
+ #
13
+ # @example Authorize, then capture in full
14
+ # txn = Airwallex::IssuingTransaction.simulate_create(
15
+ # card_id: "card_123",
16
+ # transaction_amount: 25.00,
17
+ # transaction_currency: "USD"
18
+ # )
19
+ # Airwallex::IssuingTransaction.simulate_capture(txn.transaction_id)
20
+ #
21
+ # @example Authorize and clear in a single step
22
+ # Airwallex::IssuingTransaction.simulate_create(
23
+ # card_id: "card_123",
24
+ # transaction_amount: 25.00,
25
+ # transaction_currency: "USD",
26
+ # single_phase: true
27
+ # )
28
+ class IssuingTransaction < APIResource
29
+ # @return [String] API resource path for simulated issuing transactions
30
+ def self.resource_path
31
+ "/api/v1/simulation/issuing"
32
+ end
33
+
34
+ # Simulate a card authorization (or, with single_phase: true, an
35
+ # authorization cleared in one step)
36
+ #
37
+ # @param params [Hash] card_id: (or card_number:), transaction_amount:,
38
+ # transaction_currency: (required); single_phase:, auth_code:,
39
+ # merchant_category_code:, merchant_info:, transaction_failure_reason:
40
+ # (optional)
41
+ # @return [IssuingTransaction] the resulting transaction — read its
42
+ # `transaction_id` to pass to .simulate_capture/.simulate_reverse
43
+ def self.simulate_create(params = {})
44
+ response = Airwallex.client.post("#{resource_path}/create", params)
45
+ new(response)
46
+ end
47
+
48
+ # Simulate capturing a PENDING transaction
49
+ #
50
+ # @param transaction_id [String] the `transaction_id` from
51
+ # .simulate_create's response
52
+ # @param params [Hash] merchant_info:, transaction_amount: (optional —
53
+ # a partial capture if less than the authorized amount, full amount
54
+ # if omitted)
55
+ # @return [IssuingTransaction]
56
+ def self.simulate_capture(transaction_id, params = {})
57
+ response = Airwallex.client.post(
58
+ "#{resource_path}/card_transaction_lifecycles/#{transaction_id}/capture", params
59
+ )
60
+ new(response)
61
+ end
62
+
63
+ # Simulate reversing a PENDING transaction
64
+ #
65
+ # @param transaction_id [String] the `transaction_id` from
66
+ # .simulate_create's response
67
+ # @param params [Hash] transaction_amount: (optional — a partial
68
+ # reversal if less than the authorized amount, full reversal if
69
+ # omitted)
70
+ # @return [IssuingTransaction]
71
+ def self.simulate_reverse(transaction_id, params = {})
72
+ response = Airwallex.client.post(
73
+ "#{resource_path}/card_transaction_lifecycles/#{transaction_id}/reverse", params
74
+ )
75
+ new(response)
76
+ end
77
+
78
+ # Simulate refunding a CAPTURED, not-fully-refunded transaction back to
79
+ # the card
80
+ #
81
+ # @param params [Hash] card_id: (or card_number:), transaction_amount:,
82
+ # transaction_currency: (required); merchant_category_code:,
83
+ # merchant_info: (optional)
84
+ # @return [IssuingTransaction]
85
+ def self.simulate_refund(params = {})
86
+ response = Airwallex.client.post("#{resource_path}/refund", params)
87
+ new(response)
88
+ end
89
+
90
+ # Simulate a 3DS delegation-mode notification for a card
91
+ #
92
+ # @param params [Hash] card_number: (required); merchant_info: (object,
93
+ # optional: acquirer_id:, merchant_category_code:,
94
+ # merchant_country_code:, merchant_id:, merchant_name:, merchant_url:)
95
+ # @return [Hash] raw response
96
+ def self.simulate_notify_three_ds(params = {})
97
+ Airwallex.client.post("#{resource_path}/threeds/notify", params)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Airwallex
4
+ # Represents a Linked Account — a customer's external bank account
5
+ # authorized for Direct Debit pulls.
6
+ #
7
+ # This gem currently only implements the sandbox Simulation endpoints for
8
+ # the mandate and micro-deposit lifecycle; it does not yet implement the
9
+ # live create/retrieve/list endpoints. All four actions below return
10
+ # HTTP 200 with an empty body on success, so they return `true` rather
11
+ # than a resource instance.
12
+ # See https://www.airwallex.com/docs/api/simulation/linked-accounts
13
+ #
14
+ # MANDATE STATUS LIFECYCLE:
15
+ # PROCESSING -> ACTIVE (simulate_accept_mandate)
16
+ # PROCESSING -> INACTIVE (simulate_reject_mandate)
17
+ # PROCESSING or ACTIVE -> INACTIVE (simulate_cancel_mandate)
18
+ #
19
+ # @example Approve a mandate that's awaiting the customer's bank
20
+ # Airwallex::LinkedAccount.simulate_accept_mandate("la_123")
21
+ class LinkedAccount < APIResource
22
+ # @return [String] API resource path for simulated linked account actions
23
+ def self.resource_path
24
+ "/api/v1/simulation/linked_accounts"
25
+ end
26
+
27
+ # Simulate the mandate transitioning PROCESSING -> ACTIVE
28
+ #
29
+ # @param linked_account_id [String]
30
+ # @return [true]
31
+ def self.simulate_accept_mandate(linked_account_id)
32
+ Airwallex.client.post("#{resource_path}/#{linked_account_id}/mandate/accept", {})
33
+ true
34
+ end
35
+
36
+ # Simulate the mandate transitioning PROCESSING -> INACTIVE
37
+ #
38
+ # @param linked_account_id [String]
39
+ # @return [true]
40
+ def self.simulate_reject_mandate(linked_account_id)
41
+ Airwallex.client.post("#{resource_path}/#{linked_account_id}/mandate/reject", {})
42
+ true
43
+ end
44
+
45
+ # Simulate the mandate transitioning PROCESSING or ACTIVE -> INACTIVE
46
+ #
47
+ # @param linked_account_id [String]
48
+ # @return [true]
49
+ def self.simulate_cancel_mandate(linked_account_id)
50
+ Airwallex.client.post("#{resource_path}/#{linked_account_id}/mandate/cancel", {})
51
+ true
52
+ end
53
+
54
+ # Simulate a failed micro-deposit verification, transitioning the
55
+ # linked account itself from REQUIRES_ACTION to FAILED
56
+ #
57
+ # @param linked_account_id [String]
58
+ # @return [true]
59
+ def self.simulate_fail_microdeposits(linked_account_id)
60
+ Airwallex.client.post("#{resource_path}/#{linked_account_id}/fail_microdeposits", {})
61
+ true
62
+ end
63
+ end
64
+ end
@@ -33,6 +33,9 @@ module Airwallex
33
33
  "/api/v1/pa/payment_consents"
34
34
  end
35
35
 
36
+ # Sandbox-only — see https://www.airwallex.com/docs/api/simulation/shopper-actions
37
+ SIMULATION_SHOPPER_ACTION_PATH = "/api/v1/simulation/pa/shopper_actions"
38
+
36
39
  # Verify this consent (e.g. via a zero/low-value authorization) before
37
40
  # it can be used for off-session charges
38
41
  #
@@ -64,5 +67,17 @@ module Airwallex
64
67
  refresh_from(response)
65
68
  self
66
69
  end
70
+
71
+ # Simulate the shopper completing a redirect/3DS challenge raised
72
+ # during #verify, using the `url` from the verify response's
73
+ # next_action
74
+ #
75
+ # @param url [String] the redirect URL from #verify's next_action
76
+ # @return [PaymentConsent] self
77
+ def simulate_shopper_verify(url:)
78
+ response = Airwallex.client.post("#{SIMULATION_SHOPPER_ACTION_PATH}/verify", url: url)
79
+ refresh_from(response)
80
+ self
81
+ end
67
82
  end
68
83
  end