forest_admin_datasource_mambu_payments 1.33.1
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.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/Rakefile +6 -0
- data/forest_admin_datasource_mambu_payments.gemspec +37 -0
- data/lib/forest_admin_datasource_mambu_payments/client/reads.rb +66 -0
- data/lib/forest_admin_datasource_mambu_payments/client/writes.rb +42 -0
- data/lib/forest_admin_datasource_mambu_payments/client.rb +166 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/account_holder.rb +64 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/balance.rb +75 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/base_collection.rb +254 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/claim.rb +98 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/connected_account.rb +103 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/direct_debit_mandate.rb +125 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/event.rb +133 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/expected_payment.rb +132 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/external_account.rb +121 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/file.rb +89 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/incoming_payment.rb +120 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/internal_account.rb +136 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/payee_verification_request.rb +88 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/payment_capture.rb +136 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/payment_order.rb +132 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/reconciliation.rb +93 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/return.rb +132 -0
- data/lib/forest_admin_datasource_mambu_payments/collections/transaction.rb +113 -0
- data/lib/forest_admin_datasource_mambu_payments/configuration.rb +36 -0
- data/lib/forest_admin_datasource_mambu_payments/datasource.rb +35 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/disable_search.rb +31 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/helpers.rb +94 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/messages.rb +30 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/holder_link_plugin.rb +56 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_account_holder_to_direct_debit_mandates.rb +14 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_account_holder_to_incoming_payments.rb +14 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_external_account_to_direct_debit_mandates.rb +13 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_external_account_to_incoming_payments.rb +13 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_external_account_to_payment_orders.rb +13 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_incoming_payment_to_events.rb +13 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_incoming_payment_to_expected_payments.rb +21 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_incoming_payment_to_returns.rb +12 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_incoming_payment_to_transactions.rb +20 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_internal_account_to_balances.rb +17 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_internal_account_to_incoming_payments.rb +13 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_internal_account_to_payment_orders.rb +17 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_payment_order_to_events.rb +13 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_payment_order_to_receiving_account_holder.rb +15 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_payment_order_to_returns.rb +12 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_payment_order_to_transactions.rb +51 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/one_to_many_link_plugin.rb +35 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/pivot_resolution.rb +73 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/two_step_connected_account_filter.rb +38 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/two_step_cross_reconciliation_filter.rb +55 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/two_step_holder_filter.rb +32 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/two_step_link_plugin.rb +64 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/relations/two_step_reconciliation_filter.rb +39 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/approve_payment_order.rb +56 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/cancel_payment_order.rb +66 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/create_account_holder.rb +44 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/create_external_account.rb +54 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/create_internal_account.rb +58 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/create_payment_order.rb +66 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/trigger_payee_verification.rb +58 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/update_account_holder.rb +67 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/update_external_account.rb +75 -0
- data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/update_internal_account.rb +75 -0
- data/lib/forest_admin_datasource_mambu_payments/query/condition_tree_translator.rb +115 -0
- data/lib/forest_admin_datasource_mambu_payments/version.rb +3 -0
- data/lib/forest_admin_datasource_mambu_payments.rb +44 -0
- metadata +170 -0
data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/create_account_holder.rb
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module SmartActions
|
|
4
|
+
class CreateAccountHolder < ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
5
|
+
BaseAction = ForestAdminDatasourceCustomizer::Decorators::Action::BaseAction
|
|
6
|
+
ActionScope = ForestAdminDatasourceCustomizer::Decorators::Action::Types::ActionScope
|
|
7
|
+
FieldType = ForestAdminDatasourceCustomizer::Decorators::Action::Types::FieldType
|
|
8
|
+
|
|
9
|
+
NAME = 'Create Mambu account holder'.freeze
|
|
10
|
+
|
|
11
|
+
def run(_datasource_customizer, collection_customizer = nil, options = {})
|
|
12
|
+
datasource = options[:datasource]
|
|
13
|
+
raise ArgumentError, 'CreateAccountHolder plugin requires :datasource' unless datasource
|
|
14
|
+
raise ArgumentError, 'CreateAccountHolder plugin requires a collection' unless collection_customizer
|
|
15
|
+
|
|
16
|
+
collection_customizer.add_action(options[:action_name] || NAME, build_action(datasource, options))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def build_action(datasource, opts)
|
|
22
|
+
BaseAction.new(scope: ActionScope::SINGLE, form: form, &executor(datasource, opts))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def form
|
|
26
|
+
[{ type: FieldType::STRING, label: 'Name', is_required: true,
|
|
27
|
+
description: 'Display name of the account holder.' }]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def executor(datasource, opts)
|
|
31
|
+
lambda do |context, result_builder|
|
|
32
|
+
values = context.form_values
|
|
33
|
+
payload = { 'name' => values['Name'] }
|
|
34
|
+
holder = datasource.client.create_account_holder(payload)
|
|
35
|
+
id = holder.is_a?(Hash) ? holder['id'] : nil
|
|
36
|
+
writeback = Helpers.write_back(context, opts[:result_field], id)
|
|
37
|
+
message = id ? "Account holder ##{id} created." : 'Account holder created.'
|
|
38
|
+
result_builder.success(message: "#{message}#{Helpers.write_back_warning(writeback)}")
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/create_external_account.rb
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module SmartActions
|
|
4
|
+
class CreateExternalAccount < ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
5
|
+
BaseAction = ForestAdminDatasourceCustomizer::Decorators::Action::BaseAction
|
|
6
|
+
ActionScope = ForestAdminDatasourceCustomizer::Decorators::Action::Types::ActionScope
|
|
7
|
+
FieldType = ForestAdminDatasourceCustomizer::Decorators::Action::Types::FieldType
|
|
8
|
+
|
|
9
|
+
NAME = 'Create Mambu external account'.freeze
|
|
10
|
+
|
|
11
|
+
def run(_datasource_customizer, collection_customizer = nil, options = {})
|
|
12
|
+
datasource = options[:datasource]
|
|
13
|
+
raise ArgumentError, 'CreateExternalAccount plugin requires :datasource' unless datasource
|
|
14
|
+
raise ArgumentError, 'CreateExternalAccount plugin requires a collection' unless collection_customizer
|
|
15
|
+
|
|
16
|
+
collection_customizer.add_action(options[:action_name] || NAME, build_action(datasource, options))
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
def build_action(datasource, opts)
|
|
22
|
+
BaseAction.new(scope: ActionScope::SINGLE, form: form, &executor(datasource, opts))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def form
|
|
26
|
+
[
|
|
27
|
+
{ type: FieldType::STRING, label: 'Holder name', is_required: true,
|
|
28
|
+
description: 'Name of the legal entity or individual holding the account.' },
|
|
29
|
+
{ type: FieldType::STRING, label: 'Account number', is_required: true,
|
|
30
|
+
description: 'IBAN, UK account number, or local format.' },
|
|
31
|
+
{ type: FieldType::STRING, label: 'Bank code', is_required: true,
|
|
32
|
+
description: 'BIC, UK sort code, US routing number, or local equivalent.' }
|
|
33
|
+
]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def executor(datasource, opts)
|
|
37
|
+
lambda do |context, result_builder|
|
|
38
|
+
values = context.form_values
|
|
39
|
+
payload = {
|
|
40
|
+
'holder_name' => values['Holder name'],
|
|
41
|
+
'account_number' => values['Account number'],
|
|
42
|
+
'bank_code' => values['Bank code']
|
|
43
|
+
}
|
|
44
|
+
account = datasource.client.create_external_account(payload)
|
|
45
|
+
id = account.is_a?(Hash) ? account['id'] : nil
|
|
46
|
+
writeback = Helpers.write_back(context, opts[:result_field], id)
|
|
47
|
+
message = id ? "External account ##{id} created." : 'External account created.'
|
|
48
|
+
result_builder.success(message: "#{message}#{Helpers.write_back_warning(writeback)}")
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/create_internal_account.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module SmartActions
|
|
4
|
+
class CreateInternalAccount < ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
5
|
+
BaseAction = ForestAdminDatasourceCustomizer::Decorators::Action::BaseAction
|
|
6
|
+
ActionScope = ForestAdminDatasourceCustomizer::Decorators::Action::Types::ActionScope
|
|
7
|
+
FieldType = ForestAdminDatasourceCustomizer::Decorators::Action::Types::FieldType
|
|
8
|
+
|
|
9
|
+
NAME = 'Create Mambu internal account'.freeze
|
|
10
|
+
TYPES = %w[own virtual].freeze
|
|
11
|
+
|
|
12
|
+
def run(_datasource_customizer, collection_customizer = nil, options = {})
|
|
13
|
+
datasource = options[:datasource]
|
|
14
|
+
raise ArgumentError, 'CreateInternalAccount plugin requires :datasource' unless datasource
|
|
15
|
+
raise ArgumentError, 'CreateInternalAccount plugin requires a collection' unless collection_customizer
|
|
16
|
+
|
|
17
|
+
collection_customizer.add_action(options[:action_name] || NAME, build_action(datasource, options))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def build_action(datasource, opts)
|
|
23
|
+
BaseAction.new(scope: ActionScope::SINGLE, form: form, &executor(datasource, opts))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def form
|
|
27
|
+
[
|
|
28
|
+
{ type: FieldType::ENUM, label: 'Type', is_required: true, enum_values: TYPES,
|
|
29
|
+
description: 'own (real bank account) or virtual (sub-account).' },
|
|
30
|
+
{ type: FieldType::STRING, label: 'Name', is_required: true,
|
|
31
|
+
description: 'Display name (max 100 characters).' },
|
|
32
|
+
{ type: FieldType::STRING, label: 'Holder name', is_required: true,
|
|
33
|
+
description: 'Account holder name (max 100 characters).' },
|
|
34
|
+
{ type: FieldType::STRING, label: 'Account number', is_required: true,
|
|
35
|
+
description: 'IBAN or local account number (own); up to 35 alnum chars (virtual).' }
|
|
36
|
+
]
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def executor(datasource, opts)
|
|
40
|
+
lambda do |context, result_builder|
|
|
41
|
+
values = context.form_values
|
|
42
|
+
payload = {
|
|
43
|
+
'type' => values['Type'],
|
|
44
|
+
'name' => values['Name'],
|
|
45
|
+
'holder_name' => values['Holder name'],
|
|
46
|
+
'account_number' => values['Account number']
|
|
47
|
+
}
|
|
48
|
+
account = datasource.client.create_internal_account(payload)
|
|
49
|
+
id = account.is_a?(Hash) ? account['id'] : nil
|
|
50
|
+
writeback = Helpers.write_back(context, opts[:result_field], id)
|
|
51
|
+
message = id ? "Internal account ##{id} created." : 'Internal account created.'
|
|
52
|
+
result_builder.success(message: "#{message}#{Helpers.write_back_warning(writeback)}")
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module SmartActions
|
|
4
|
+
class CreatePaymentOrder < ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
5
|
+
BaseAction = ForestAdminDatasourceCustomizer::Decorators::Action::BaseAction
|
|
6
|
+
ActionScope = ForestAdminDatasourceCustomizer::Decorators::Action::Types::ActionScope
|
|
7
|
+
FieldType = ForestAdminDatasourceCustomizer::Decorators::Action::Types::FieldType
|
|
8
|
+
|
|
9
|
+
NAME = 'Create Mambu payment order'.freeze
|
|
10
|
+
DIRECTIONS = %w[credit debit].freeze
|
|
11
|
+
|
|
12
|
+
def run(_datasource_customizer, collection_customizer = nil, options = {})
|
|
13
|
+
datasource = options[:datasource]
|
|
14
|
+
raise ArgumentError, 'CreatePaymentOrder plugin requires :datasource' unless datasource
|
|
15
|
+
raise ArgumentError, 'CreatePaymentOrder plugin requires a collection' unless collection_customizer
|
|
16
|
+
|
|
17
|
+
collection_customizer.add_action(options[:action_name] || NAME, build_action(datasource, options))
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def build_action(datasource, opts)
|
|
23
|
+
BaseAction.new(scope: ActionScope::SINGLE, form: form, &executor(datasource, opts))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def form
|
|
27
|
+
[
|
|
28
|
+
{ type: FieldType::STRING, label: 'Type', is_required: true,
|
|
29
|
+
description: 'Payment type (e.g. sepa_credit_transfer, swift). See Numeral docs for the full list.' },
|
|
30
|
+
{ type: FieldType::ENUM, label: 'Direction', is_required: true, enum_values: DIRECTIONS },
|
|
31
|
+
{ type: FieldType::NUMBER, label: 'Amount', is_required: true,
|
|
32
|
+
description: "Amount in the currency's smallest unit (e.g. cents for EUR)." },
|
|
33
|
+
{ type: FieldType::STRING, label: 'Currency', is_required: true,
|
|
34
|
+
description: 'ISO 4217 code (e.g. EUR, USD).' },
|
|
35
|
+
{ type: FieldType::STRING, label: 'Reference', is_required: true,
|
|
36
|
+
description: 'Reference shown on the account statements (max 140 characters).' },
|
|
37
|
+
{ type: FieldType::STRING, label: 'Connected account id', is_required: true,
|
|
38
|
+
description: 'UUID of the connected account that triggers the payment.' }
|
|
39
|
+
]
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def executor(datasource, opts)
|
|
43
|
+
lambda do |context, result_builder|
|
|
44
|
+
values = context.form_values
|
|
45
|
+
amount = Helpers.to_int(values['Amount'])
|
|
46
|
+
next result_builder.error(message: 'Amount must be an integer (smallest currency unit).') unless amount
|
|
47
|
+
|
|
48
|
+
payload = {
|
|
49
|
+
'type' => values['Type'],
|
|
50
|
+
'direction' => values['Direction'],
|
|
51
|
+
'amount' => amount,
|
|
52
|
+
'currency' => values['Currency'],
|
|
53
|
+
'reference' => values['Reference'],
|
|
54
|
+
'connected_account_id' => values['Connected account id']
|
|
55
|
+
}
|
|
56
|
+
order = datasource.client.create_payment_order(payload)
|
|
57
|
+
id = order.is_a?(Hash) ? order['id'] : nil
|
|
58
|
+
writeback = Helpers.write_back(context, opts[:result_field], id)
|
|
59
|
+
message = id ? "Payment order ##{id} created." : 'Payment order created.'
|
|
60
|
+
result_builder.success(message: "#{message}#{Helpers.write_back_warning(writeback)}")
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/trigger_payee_verification.rb
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module SmartActions
|
|
4
|
+
# Triggers Numeral's asynchronous external-account verification (a.k.a.
|
|
5
|
+
# Verification of Payee / VOP). The API returns immediately with status
|
|
6
|
+
# `pending_verification`; the actual result lands ~30s later via webhook.
|
|
7
|
+
class TriggerPayeeVerification < ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
8
|
+
BaseAction = ForestAdminDatasourceCustomizer::Decorators::Action::BaseAction
|
|
9
|
+
ActionScope = ForestAdminDatasourceCustomizer::Decorators::Action::Types::ActionScope
|
|
10
|
+
|
|
11
|
+
NAMES = { single: 'Trigger payee verification',
|
|
12
|
+
bulk: 'Trigger payee verification on selected accounts' }.freeze
|
|
13
|
+
|
|
14
|
+
def run(_datasource_customizer, collection_customizer = nil, options = {})
|
|
15
|
+
datasource = options[:datasource]
|
|
16
|
+
record_id_field = options[:record_id_field]
|
|
17
|
+
raise ArgumentError, 'TriggerPayeeVerification plugin requires :datasource' unless datasource
|
|
18
|
+
raise ArgumentError, 'TriggerPayeeVerification plugin requires :record_id_field' unless record_id_field
|
|
19
|
+
raise ArgumentError, 'TriggerPayeeVerification plugin requires a collection' unless collection_customizer
|
|
20
|
+
|
|
21
|
+
Helpers.normalize_scopes(options[:scopes]).each do |scope_key|
|
|
22
|
+
collection_customizer.add_action(NAMES[scope_key], build_action(datasource, scope_key, record_id_field))
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def build_action(datasource, scope_key, record_id_field)
|
|
29
|
+
BaseAction.new(scope: Helpers::SCOPES[scope_key], &executor(datasource, record_id_field))
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def executor(datasource, record_id_field)
|
|
33
|
+
lambda do |context, result_builder|
|
|
34
|
+
ids = Helpers.resolve_ids(context, record_id_field)
|
|
35
|
+
if ids.empty?
|
|
36
|
+
next result_builder.error(message: "No Mambu external account id found in '#{record_id_field}'.")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
succeeded, failed = Helpers.each_with_rescue(ids, 'verify_external_account') do |id|
|
|
40
|
+
datasource.client.verify_external_account(id)
|
|
41
|
+
end
|
|
42
|
+
finalize(result_builder, succeeded, failed)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def finalize(result_builder, succeeded, failed)
|
|
47
|
+
if succeeded.empty?
|
|
48
|
+
return result_builder.error(message: Messages.all_failed(failed, noun: 'external account',
|
|
49
|
+
verb: 'verify'))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
result_builder.success(message: Messages.success(succeeded, failed, noun: 'external account',
|
|
53
|
+
verb_past: 'now pending verification'))
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/update_account_holder.rb
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module SmartActions
|
|
4
|
+
class UpdateAccountHolder < ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
5
|
+
BaseAction = ForestAdminDatasourceCustomizer::Decorators::Action::BaseAction
|
|
6
|
+
ActionScope = ForestAdminDatasourceCustomizer::Decorators::Action::Types::ActionScope
|
|
7
|
+
FieldType = ForestAdminDatasourceCustomizer::Decorators::Action::Types::FieldType
|
|
8
|
+
|
|
9
|
+
NAME = 'Update Mambu account holder'.freeze
|
|
10
|
+
|
|
11
|
+
def run(_datasource_customizer, collection_customizer = nil, options = {})
|
|
12
|
+
datasource = options[:datasource]
|
|
13
|
+
record_id_field = options[:record_id_field]
|
|
14
|
+
raise ArgumentError, 'UpdateAccountHolder plugin requires :datasource' unless datasource
|
|
15
|
+
raise ArgumentError, 'UpdateAccountHolder plugin requires :record_id_field' unless record_id_field
|
|
16
|
+
raise ArgumentError, 'UpdateAccountHolder plugin requires a collection' unless collection_customizer
|
|
17
|
+
|
|
18
|
+
collection_customizer.add_action(options[:action_name] || NAME, build_action(datasource, options))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def build_action(datasource, opts)
|
|
24
|
+
BaseAction.new(scope: ActionScope::SINGLE, form: form, &executor(datasource, opts))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def form
|
|
28
|
+
[{ type: FieldType::STRING, label: 'Name',
|
|
29
|
+
description: 'New display name (leave empty to keep the current value).' }]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def executor(datasource, opts)
|
|
33
|
+
lambda do |context, result_builder|
|
|
34
|
+
ids = Helpers.resolve_ids(context, opts[:record_id_field])
|
|
35
|
+
if ids.empty?
|
|
36
|
+
next result_builder.error(message: "No Mambu account holder id found in '#{opts[:record_id_field]}'.")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
payload = build_payload(context.form_values)
|
|
40
|
+
next result_builder.error(message: 'Nothing to update: fill at least one field.') if payload.empty?
|
|
41
|
+
|
|
42
|
+
succeeded, failed = Helpers.each_with_rescue(ids, 'update_account_holder') do |id|
|
|
43
|
+
datasource.client.update_account_holder(id, payload)
|
|
44
|
+
end
|
|
45
|
+
finalize(result_builder, succeeded, failed)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def build_payload(values)
|
|
50
|
+
payload = {}
|
|
51
|
+
payload['name'] = values['Name'] if Helpers.present?(values['Name'])
|
|
52
|
+
payload
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def finalize(result_builder, succeeded, failed)
|
|
56
|
+
if succeeded.empty?
|
|
57
|
+
return result_builder.error(message: Messages.all_failed(failed, noun: 'account holder',
|
|
58
|
+
verb: 'update'))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
result_builder.success(message: Messages.success(succeeded, failed, noun: 'account holder',
|
|
62
|
+
verb_past: 'updated'))
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/update_external_account.rb
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module SmartActions
|
|
4
|
+
class UpdateExternalAccount < ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
5
|
+
BaseAction = ForestAdminDatasourceCustomizer::Decorators::Action::BaseAction
|
|
6
|
+
ActionScope = ForestAdminDatasourceCustomizer::Decorators::Action::Types::ActionScope
|
|
7
|
+
FieldType = ForestAdminDatasourceCustomizer::Decorators::Action::Types::FieldType
|
|
8
|
+
|
|
9
|
+
NAME = 'Update Mambu external account'.freeze
|
|
10
|
+
|
|
11
|
+
def run(_datasource_customizer, collection_customizer = nil, options = {})
|
|
12
|
+
datasource = options[:datasource]
|
|
13
|
+
record_id_field = options[:record_id_field]
|
|
14
|
+
raise ArgumentError, 'UpdateExternalAccount plugin requires :datasource' unless datasource
|
|
15
|
+
raise ArgumentError, 'UpdateExternalAccount plugin requires :record_id_field' unless record_id_field
|
|
16
|
+
raise ArgumentError, 'UpdateExternalAccount plugin requires a collection' unless collection_customizer
|
|
17
|
+
|
|
18
|
+
collection_customizer.add_action(options[:action_name] || NAME, build_action(datasource, options))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def build_action(datasource, opts)
|
|
24
|
+
BaseAction.new(scope: ActionScope::SINGLE, form: form, &executor(datasource, opts))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def form
|
|
28
|
+
[
|
|
29
|
+
{ type: FieldType::STRING, label: 'Holder name',
|
|
30
|
+
description: 'Leave empty to keep the current value.' },
|
|
31
|
+
{ type: FieldType::STRING, label: 'Account number',
|
|
32
|
+
description: 'Leave empty to keep the current value.' },
|
|
33
|
+
{ type: FieldType::STRING, label: 'Bank code',
|
|
34
|
+
description: 'Leave empty to keep the current value.' }
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def executor(datasource, opts)
|
|
39
|
+
lambda do |context, result_builder|
|
|
40
|
+
ids = Helpers.resolve_ids(context, opts[:record_id_field])
|
|
41
|
+
if ids.empty?
|
|
42
|
+
next result_builder.error(message: "No Mambu external account id found in '#{opts[:record_id_field]}'.")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
payload = build_payload(context.form_values)
|
|
46
|
+
next result_builder.error(message: 'Nothing to update: fill at least one field.') if payload.empty?
|
|
47
|
+
|
|
48
|
+
succeeded, failed = Helpers.each_with_rescue(ids, 'update_external_account') do |id|
|
|
49
|
+
datasource.client.update_external_account(id, payload)
|
|
50
|
+
end
|
|
51
|
+
finalize(result_builder, succeeded, failed)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_payload(values)
|
|
56
|
+
payload = {}
|
|
57
|
+
payload['holder_name'] = values['Holder name'] if Helpers.present?(values['Holder name'])
|
|
58
|
+
payload['account_number'] = values['Account number'] if Helpers.present?(values['Account number'])
|
|
59
|
+
payload['bank_code'] = values['Bank code'] if Helpers.present?(values['Bank code'])
|
|
60
|
+
payload
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def finalize(result_builder, succeeded, failed)
|
|
64
|
+
if succeeded.empty?
|
|
65
|
+
return result_builder.error(message: Messages.all_failed(failed, noun: 'external account',
|
|
66
|
+
verb: 'update'))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
result_builder.success(message: Messages.success(succeeded, failed, noun: 'external account',
|
|
70
|
+
verb_past: 'updated'))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/update_internal_account.rb
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module SmartActions
|
|
4
|
+
class UpdateInternalAccount < ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
5
|
+
BaseAction = ForestAdminDatasourceCustomizer::Decorators::Action::BaseAction
|
|
6
|
+
ActionScope = ForestAdminDatasourceCustomizer::Decorators::Action::Types::ActionScope
|
|
7
|
+
FieldType = ForestAdminDatasourceCustomizer::Decorators::Action::Types::FieldType
|
|
8
|
+
|
|
9
|
+
NAME = 'Update Mambu internal account'.freeze
|
|
10
|
+
|
|
11
|
+
def run(_datasource_customizer, collection_customizer = nil, options = {})
|
|
12
|
+
datasource = options[:datasource]
|
|
13
|
+
record_id_field = options[:record_id_field]
|
|
14
|
+
raise ArgumentError, 'UpdateInternalAccount plugin requires :datasource' unless datasource
|
|
15
|
+
raise ArgumentError, 'UpdateInternalAccount plugin requires :record_id_field' unless record_id_field
|
|
16
|
+
raise ArgumentError, 'UpdateInternalAccount plugin requires a collection' unless collection_customizer
|
|
17
|
+
|
|
18
|
+
collection_customizer.add_action(options[:action_name] || NAME, build_action(datasource, options))
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def build_action(datasource, opts)
|
|
24
|
+
BaseAction.new(scope: ActionScope::SINGLE, form: form, &executor(datasource, opts))
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def form
|
|
28
|
+
[
|
|
29
|
+
{ type: FieldType::STRING, label: 'Name',
|
|
30
|
+
description: 'Leave empty to keep the current value.' },
|
|
31
|
+
{ type: FieldType::STRING, label: 'Holder name',
|
|
32
|
+
description: 'Leave empty to keep the current value.' },
|
|
33
|
+
{ type: FieldType::STRING, label: 'Account number',
|
|
34
|
+
description: 'Leave empty to keep the current value.' }
|
|
35
|
+
]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def executor(datasource, opts)
|
|
39
|
+
lambda do |context, result_builder|
|
|
40
|
+
ids = Helpers.resolve_ids(context, opts[:record_id_field])
|
|
41
|
+
if ids.empty?
|
|
42
|
+
next result_builder.error(message: "No Mambu internal account id found in '#{opts[:record_id_field]}'.")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
payload = build_payload(context.form_values)
|
|
46
|
+
next result_builder.error(message: 'Nothing to update: fill at least one field.') if payload.empty?
|
|
47
|
+
|
|
48
|
+
succeeded, failed = Helpers.each_with_rescue(ids, 'update_internal_account') do |id|
|
|
49
|
+
datasource.client.update_internal_account(id, payload)
|
|
50
|
+
end
|
|
51
|
+
finalize(result_builder, succeeded, failed)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_payload(values)
|
|
56
|
+
payload = {}
|
|
57
|
+
payload['name'] = values['Name'] if Helpers.present?(values['Name'])
|
|
58
|
+
payload['holder_name'] = values['Holder name'] if Helpers.present?(values['Holder name'])
|
|
59
|
+
payload['account_number'] = values['Account number'] if Helpers.present?(values['Account number'])
|
|
60
|
+
payload
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def finalize(result_builder, succeeded, failed)
|
|
64
|
+
if succeeded.empty?
|
|
65
|
+
return result_builder.error(message: Messages.all_failed(failed, noun: 'internal account',
|
|
66
|
+
verb: 'update'))
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
result_builder.success(message: Messages.success(succeeded, failed, noun: 'internal account',
|
|
70
|
+
verb_past: 'updated'))
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Query
|
|
3
|
+
# Translates a Forest condition tree into a hash of Numeral query params.
|
|
4
|
+
#
|
|
5
|
+
# The previous version of `fetch_records` short-circuited on `id` and
|
|
6
|
+
# otherwise sent an unfiltered list — silently producing wrong counts and
|
|
7
|
+
# missing rows whenever the UI applied a non-id predicate. This translator
|
|
8
|
+
# raises `UnsupportedOperatorError` for anything it cannot map, so the
|
|
9
|
+
# failure mode is "loud error" rather than "wrong data".
|
|
10
|
+
#
|
|
11
|
+
# Each collection declares its server-filterable fields via `api_filters`:
|
|
12
|
+
#
|
|
13
|
+
# { 'connected_account_id' => { ops: [EQUAL, IN], param: 'connected_account_id' } }
|
|
14
|
+
#
|
|
15
|
+
# `param` defaults to the field name. EQUAL emits a scalar value, IN emits
|
|
16
|
+
# an Array (the client joins arrays with commas — see `Client#normalize_params`).
|
|
17
|
+
# Top-level OR aggregation is rejected: Numeral list endpoints have no
|
|
18
|
+
# general OR support, so silently translating "A or B" to "A and B" would
|
|
19
|
+
# be wrong in both directions.
|
|
20
|
+
class ConditionTreeTranslator
|
|
21
|
+
Operators = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Operators
|
|
22
|
+
Branch = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes::ConditionTreeBranch
|
|
23
|
+
Leaf = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes::ConditionTreeLeaf
|
|
24
|
+
|
|
25
|
+
def self.call(condition_tree, api_filters: {})
|
|
26
|
+
return {} if condition_tree.nil?
|
|
27
|
+
|
|
28
|
+
new(api_filters).translate(condition_tree)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def initialize(api_filters)
|
|
32
|
+
@api_filters = api_filters || {}
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def translate(node)
|
|
36
|
+
case node
|
|
37
|
+
when Branch then translate_branch(node)
|
|
38
|
+
when Leaf then translate_leaf(node)
|
|
39
|
+
else
|
|
40
|
+
raise UnsupportedOperatorError, "Unknown condition node: #{node.class}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def translate_branch(branch)
|
|
47
|
+
unless branch.aggregator.to_s.casecmp('and').zero?
|
|
48
|
+
raise UnsupportedOperatorError,
|
|
49
|
+
"Mambu Payments list endpoints do not support OR aggregation (got #{branch.aggregator.inspect}). " \
|
|
50
|
+
'Split the request into separate filters.'
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
branch.conditions.each_with_object({}) do |condition, acc|
|
|
54
|
+
translate(condition).each do |key, value|
|
|
55
|
+
if acc.key?(key)
|
|
56
|
+
raise UnsupportedOperatorError,
|
|
57
|
+
"Conflicting predicates on '#{key}': cannot pass the same query param twice."
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
acc[key] = value
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def translate_leaf(leaf)
|
|
66
|
+
spec = @api_filters[leaf.field]
|
|
67
|
+
unless spec
|
|
68
|
+
raise UnsupportedOperatorError,
|
|
69
|
+
"Mambu Payments datasource does not yet translate filters on '#{leaf.field}'. " \
|
|
70
|
+
'Add it to the collection\'s `api_filters` after verifying the Numeral docs.'
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
param = (spec[:param] || leaf.field).to_s
|
|
74
|
+
ops = Array(spec[:ops])
|
|
75
|
+
unless ops.include?(leaf.operator)
|
|
76
|
+
raise UnsupportedOperatorError,
|
|
77
|
+
"Operator '#{leaf.operator}' is not supported on field '#{leaf.field}'. " \
|
|
78
|
+
"Supported: #{ops.join(", ")}."
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
translate_value(param, leaf.operator, leaf.value)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def translate_value(param, operator, value)
|
|
85
|
+
case operator
|
|
86
|
+
when Operators::EQUAL
|
|
87
|
+
raise_nil_value(param) if value.nil?
|
|
88
|
+
{ param => value }
|
|
89
|
+
when Operators::IN
|
|
90
|
+
values = Array(value).reject { |v| v.nil? || v.to_s.empty? }
|
|
91
|
+
raise_empty_in(param) if values.empty?
|
|
92
|
+
{ param => values }
|
|
93
|
+
else
|
|
94
|
+
raise UnsupportedOperatorError,
|
|
95
|
+
"Operator '#{operator}' is declared in api_filters but has no translation rule."
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# `field=` with a nil value would semantically degrade to "filter present" on
|
|
100
|
+
# most REST APIs — silently the wrong query. Use PRESENT / BLANK instead
|
|
101
|
+
# (once those operators are wired up here).
|
|
102
|
+
def raise_nil_value(param)
|
|
103
|
+
raise UnsupportedOperatorError,
|
|
104
|
+
"Filter value on '#{param}' is nil; the PRESENT / BLANK operators are not yet supported."
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# An empty `IN []` would translate to no params, silently turning
|
|
108
|
+
# "match nothing" into "match everything". Raise instead.
|
|
109
|
+
def raise_empty_in(param)
|
|
110
|
+
raise UnsupportedOperatorError,
|
|
111
|
+
"IN on '#{param}' was given an empty array; pass at least one value."
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
require_relative 'forest_admin_datasource_mambu_payments/version'
|
|
2
|
+
require 'logger'
|
|
3
|
+
require 'zeitwerk'
|
|
4
|
+
require 'faraday'
|
|
5
|
+
require 'faraday/retry'
|
|
6
|
+
require 'forest_admin_datasource_toolkit'
|
|
7
|
+
|
|
8
|
+
loader = Zeitwerk::Loader.for_gem
|
|
9
|
+
loader.setup
|
|
10
|
+
|
|
11
|
+
module ForestAdminDatasourceMambuPayments
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
class ConfigurationError < Error; end
|
|
14
|
+
class UnsupportedOperatorError < Error; end
|
|
15
|
+
|
|
16
|
+
# Raised when a Numeral API call fails. Carries the HTTP status and the
|
|
17
|
+
# (parsed) response body so callers — smart actions in particular — can
|
|
18
|
+
# surface the API's own validation message instead of a generic string.
|
|
19
|
+
class APIError < Error
|
|
20
|
+
attr_reader :status, :body
|
|
21
|
+
|
|
22
|
+
def initialize(message, status: nil, body: nil)
|
|
23
|
+
super(message)
|
|
24
|
+
@status = status
|
|
25
|
+
@body = body
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class << self
|
|
30
|
+
attr_writer :logger
|
|
31
|
+
|
|
32
|
+
def logger
|
|
33
|
+
@logger ||= default_logger
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
private
|
|
37
|
+
|
|
38
|
+
def default_logger
|
|
39
|
+
return Rails.logger if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
|
|
40
|
+
|
|
41
|
+
Logger.new($stderr).tap { |l| l.progname = 'forest_admin_datasource_mambu_payments' }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|