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.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/Rakefile +6 -0
  4. data/forest_admin_datasource_mambu_payments.gemspec +37 -0
  5. data/lib/forest_admin_datasource_mambu_payments/client/reads.rb +66 -0
  6. data/lib/forest_admin_datasource_mambu_payments/client/writes.rb +42 -0
  7. data/lib/forest_admin_datasource_mambu_payments/client.rb +166 -0
  8. data/lib/forest_admin_datasource_mambu_payments/collections/account_holder.rb +64 -0
  9. data/lib/forest_admin_datasource_mambu_payments/collections/balance.rb +75 -0
  10. data/lib/forest_admin_datasource_mambu_payments/collections/base_collection.rb +254 -0
  11. data/lib/forest_admin_datasource_mambu_payments/collections/claim.rb +98 -0
  12. data/lib/forest_admin_datasource_mambu_payments/collections/connected_account.rb +103 -0
  13. data/lib/forest_admin_datasource_mambu_payments/collections/direct_debit_mandate.rb +125 -0
  14. data/lib/forest_admin_datasource_mambu_payments/collections/event.rb +133 -0
  15. data/lib/forest_admin_datasource_mambu_payments/collections/expected_payment.rb +132 -0
  16. data/lib/forest_admin_datasource_mambu_payments/collections/external_account.rb +121 -0
  17. data/lib/forest_admin_datasource_mambu_payments/collections/file.rb +89 -0
  18. data/lib/forest_admin_datasource_mambu_payments/collections/incoming_payment.rb +120 -0
  19. data/lib/forest_admin_datasource_mambu_payments/collections/internal_account.rb +136 -0
  20. data/lib/forest_admin_datasource_mambu_payments/collections/payee_verification_request.rb +88 -0
  21. data/lib/forest_admin_datasource_mambu_payments/collections/payment_capture.rb +136 -0
  22. data/lib/forest_admin_datasource_mambu_payments/collections/payment_order.rb +132 -0
  23. data/lib/forest_admin_datasource_mambu_payments/collections/reconciliation.rb +93 -0
  24. data/lib/forest_admin_datasource_mambu_payments/collections/return.rb +132 -0
  25. data/lib/forest_admin_datasource_mambu_payments/collections/transaction.rb +113 -0
  26. data/lib/forest_admin_datasource_mambu_payments/configuration.rb +36 -0
  27. data/lib/forest_admin_datasource_mambu_payments/datasource.rb +35 -0
  28. data/lib/forest_admin_datasource_mambu_payments/plugins/disable_search.rb +31 -0
  29. data/lib/forest_admin_datasource_mambu_payments/plugins/helpers.rb +94 -0
  30. data/lib/forest_admin_datasource_mambu_payments/plugins/messages.rb +30 -0
  31. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/holder_link_plugin.rb +56 -0
  32. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_account_holder_to_direct_debit_mandates.rb +14 -0
  33. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_account_holder_to_incoming_payments.rb +14 -0
  34. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_external_account_to_direct_debit_mandates.rb +13 -0
  35. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_external_account_to_incoming_payments.rb +13 -0
  36. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_external_account_to_payment_orders.rb +13 -0
  37. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_incoming_payment_to_events.rb +13 -0
  38. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_incoming_payment_to_expected_payments.rb +21 -0
  39. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_incoming_payment_to_returns.rb +12 -0
  40. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_incoming_payment_to_transactions.rb +20 -0
  41. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_internal_account_to_balances.rb +17 -0
  42. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_internal_account_to_incoming_payments.rb +13 -0
  43. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_internal_account_to_payment_orders.rb +17 -0
  44. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_payment_order_to_events.rb +13 -0
  45. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_payment_order_to_receiving_account_holder.rb +15 -0
  46. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_payment_order_to_returns.rb +12 -0
  47. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_payment_order_to_transactions.rb +51 -0
  48. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/one_to_many_link_plugin.rb +35 -0
  49. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/pivot_resolution.rb +73 -0
  50. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/two_step_connected_account_filter.rb +38 -0
  51. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/two_step_cross_reconciliation_filter.rb +55 -0
  52. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/two_step_holder_filter.rb +32 -0
  53. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/two_step_link_plugin.rb +64 -0
  54. data/lib/forest_admin_datasource_mambu_payments/plugins/relations/two_step_reconciliation_filter.rb +39 -0
  55. data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/approve_payment_order.rb +56 -0
  56. data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/cancel_payment_order.rb +66 -0
  57. data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/create_account_holder.rb +44 -0
  58. data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/create_external_account.rb +54 -0
  59. data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/create_internal_account.rb +58 -0
  60. data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/create_payment_order.rb +66 -0
  61. data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/trigger_payee_verification.rb +58 -0
  62. data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/update_account_holder.rb +67 -0
  63. data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/update_external_account.rb +75 -0
  64. data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/update_internal_account.rb +75 -0
  65. data/lib/forest_admin_datasource_mambu_payments/query/condition_tree_translator.rb +115 -0
  66. data/lib/forest_admin_datasource_mambu_payments/version.rb +3 -0
  67. data/lib/forest_admin_datasource_mambu_payments.rb +44 -0
  68. metadata +170 -0
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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,3 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ VERSION = "1.33.1"
3
+ 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