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,132 @@
1
+ # rubocop:disable Metrics/ClassLength, Metrics/MethodLength
2
+ module ForestAdminDatasourceMambuPayments
3
+ module Collections
4
+ class Return < BaseCollection
5
+ ManyToOneSchema = ForestAdminDatasourceToolkit::Schema::Relations::ManyToOneSchema
6
+
7
+ ENUM_DIRECTION = %w[credit debit].freeze
8
+ ENUM_TYPE = %w[sepa sepa_instant].freeze
9
+ ENUM_RETURN_TYPE = %w[return refund reversal].freeze
10
+ ENUM_STATUS = %w[pending sent processing executed received rejected].freeze
11
+ ENUM_RELATED_PAYMENT = %w[payment_order incoming_payment].freeze
12
+
13
+ client_resource :return
14
+
15
+ def initialize(datasource)
16
+ super(datasource, 'MambuReturn')
17
+ define_schema
18
+ define_relations
19
+ reconcile_filter_operators!
20
+ end
21
+
22
+ def create(_caller, data)
23
+ serialize(datasource.client.create_return(build_payload(data)))
24
+ end
25
+
26
+ def update(caller, filter, patch)
27
+ payload = build_payload(patch)
28
+ ids_for(caller, filter).each { |id| datasource.client.update_return(id, payload) }
29
+ end
30
+
31
+ def serialize(record)
32
+ a = attrs_of(record)
33
+ {
34
+ 'id' => a['id'],
35
+ 'object' => a['object'],
36
+ 'connected_account_id' => a['connected_account_id'],
37
+ 'related_payment_id' => a['related_payment_id'],
38
+ 'related_payment_type' => a['related_payment_type'],
39
+ 'related_payment_suspended' => a['related_payment_suspended'],
40
+ 'return_type' => a['return_type'],
41
+ 'type' => a['type'],
42
+ 'direction' => a['direction'],
43
+ 'status' => a['status'],
44
+ 'status_details' => a['status_details'],
45
+ 'return_reason' => a['return_reason'],
46
+ 'amount' => a['amount'],
47
+ 'currency' => a['currency'],
48
+ 'reconciliation_status' => a['reconciliation_status'],
49
+ 'reconciled_amount' => a['reconciled_amount'],
50
+ 'value_date' => a['value_date'],
51
+ 'booking_date' => a['booking_date'],
52
+ 'originating_account' => a['originating_account'],
53
+ 'receiving_account' => a['receiving_account'],
54
+ 'aggregation_reference' => a['aggregation_reference'],
55
+ 'file_id' => a['file_id'],
56
+ 'metadata' => a['metadata'],
57
+ 'created_at' => a['created_at']
58
+ }
59
+ end
60
+
61
+ protected
62
+
63
+ def collection_filters
64
+ {
65
+ 'connected_account_id' => { ops: [Operators::EQUAL, Operators::IN] },
66
+ 'related_payment_id' => { ops: [Operators::EQUAL, Operators::IN] },
67
+ 'status' => { ops: [Operators::EQUAL, Operators::IN] },
68
+ 'type' => { ops: [Operators::EQUAL, Operators::IN] }
69
+ }
70
+ end
71
+
72
+ def many_to_one_embeds
73
+ [
74
+ { foreign_key: 'connected_account_id', relation_name: 'connected_account',
75
+ collection: 'MambuConnectedAccount' }
76
+ ]
77
+ end
78
+
79
+ private
80
+
81
+ def define_schema
82
+ add_field('id', ColumnSchema.new(column_type: 'String', is_primary_key: true,
83
+ is_read_only: true, is_sortable: true))
84
+ add_field('object', ColumnSchema.new(column_type: 'String', is_read_only: true, is_sortable: false))
85
+ add_field('connected_account_id', ColumnSchema.new(column_type: 'String',
86
+ is_read_only: true, is_sortable: true))
87
+ # related_payment_id can target a payment_order OR an incoming_payment
88
+ # depending on related_payment_type — we expose it as a plain string
89
+ # rather than a typed relation (Forest can't model the polymorphism).
90
+ add_field('related_payment_id', ColumnSchema.new(column_type: 'String',
91
+ is_read_only: false, is_sortable: false))
92
+ add_field('related_payment_type', ColumnSchema.new(column_type: 'Enum', enum_values: ENUM_RELATED_PAYMENT,
93
+ is_read_only: true, is_sortable: false))
94
+ add_field('related_payment_suspended', ColumnSchema.new(column_type: 'Boolean',
95
+ is_read_only: false, is_sortable: false))
96
+ add_field('return_type', ColumnSchema.new(column_type: 'Enum', enum_values: ENUM_RETURN_TYPE,
97
+ is_read_only: true, is_sortable: true))
98
+ add_field('type', ColumnSchema.new(column_type: 'Enum', enum_values: ENUM_TYPE,
99
+ is_read_only: true, is_sortable: true))
100
+ add_field('direction', ColumnSchema.new(column_type: 'Enum', enum_values: ENUM_DIRECTION,
101
+ is_read_only: true, is_sortable: false))
102
+ add_field('status', ColumnSchema.new(column_type: 'Enum', enum_values: ENUM_STATUS,
103
+ is_read_only: false, is_sortable: true))
104
+ add_field('status_details', ColumnSchema.new(column_type: 'String', is_read_only: false, is_sortable: false))
105
+ add_field('return_reason', ColumnSchema.new(column_type: 'String', is_read_only: false, is_sortable: false))
106
+ add_field('amount', ColumnSchema.new(column_type: 'Number', is_read_only: true, is_sortable: false))
107
+ add_field('currency', ColumnSchema.new(column_type: 'String', is_read_only: true, is_sortable: false))
108
+ add_field('reconciliation_status', ColumnSchema.new(column_type: 'String', is_read_only: true,
109
+ is_sortable: false))
110
+ add_field('reconciled_amount', ColumnSchema.new(column_type: 'Number', is_read_only: true, is_sortable: false))
111
+ add_field('value_date', ColumnSchema.new(column_type: 'Date', is_read_only: true, is_sortable: true))
112
+ add_field('booking_date', ColumnSchema.new(column_type: 'Date', is_read_only: true, is_sortable: true))
113
+ add_field('originating_account', ColumnSchema.new(column_type: 'Json', is_read_only: true, is_sortable: false))
114
+ add_field('receiving_account', ColumnSchema.new(column_type: 'Json', is_read_only: true, is_sortable: false))
115
+ add_field('aggregation_reference', ColumnSchema.new(column_type: 'String', is_read_only: true,
116
+ is_sortable: false))
117
+ add_field('file_id', ColumnSchema.new(column_type: 'String', is_read_only: true, is_sortable: false))
118
+ add_field('metadata', ColumnSchema.new(column_type: 'Json', is_read_only: false, is_sortable: false))
119
+ add_field('created_at', ColumnSchema.new(column_type: 'Date', is_read_only: true, is_sortable: true))
120
+ end
121
+
122
+ def define_relations
123
+ add_field('connected_account', ManyToOneSchema.new(
124
+ foreign_collection: 'MambuConnectedAccount',
125
+ foreign_key: 'connected_account_id',
126
+ foreign_key_target: 'id'
127
+ ))
128
+ end
129
+ end
130
+ end
131
+ end
132
+ # rubocop:enable Metrics/ClassLength, Metrics/MethodLength
@@ -0,0 +1,113 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ module Collections
3
+ class Transaction < BaseCollection
4
+ ManyToOneSchema = ForestAdminDatasourceToolkit::Schema::Relations::ManyToOneSchema
5
+
6
+ ENUM_DIRECTION = %w[debit credit].freeze
7
+
8
+ client_resource :transaction
9
+
10
+ def initialize(datasource)
11
+ super(datasource, 'MambuTransaction')
12
+ define_schema
13
+ define_relations
14
+ reconcile_filter_operators!
15
+ end
16
+
17
+ def serialize(record)
18
+ a = attrs_of(record)
19
+ {
20
+ 'id' => a['id'],
21
+ 'object' => a['object'],
22
+ 'connected_account_id' => a['connected_account_id'],
23
+ 'category' => a['category'],
24
+ 'direction' => a['direction'],
25
+ 'amount' => a['amount'],
26
+ 'currency' => a['currency'],
27
+ 'description' => a['description'],
28
+ 'structured_reference' => a['structured_reference'],
29
+ 'value_date' => a['value_date'],
30
+ 'booking_date' => a['booking_date'],
31
+ 'internal_account_id' => a['internal_account_id'],
32
+ 'external_account_id' => a['external_account_id'],
33
+ 'uetr' => a['uetr'],
34
+ 'bank_data' => a['bank_data'],
35
+ 'reconciliation_status' => a['reconciliation_status'],
36
+ 'reconciled_amount' => a['reconciled_amount'],
37
+ 'custom_fields' => a['custom_fields'],
38
+ 'created_at' => a['created_at']
39
+ }
40
+ end
41
+
42
+ protected
43
+
44
+ def collection_filters
45
+ {
46
+ 'connected_account_id' => { ops: [Operators::EQUAL, Operators::IN] },
47
+ 'internal_account_id' => { ops: [Operators::EQUAL, Operators::IN] },
48
+ 'external_account_id' => { ops: [Operators::EQUAL, Operators::IN] }
49
+ }
50
+ end
51
+
52
+ def many_to_one_embeds
53
+ [
54
+ { foreign_key: 'connected_account_id', relation_name: 'connected_account',
55
+ collection: 'MambuConnectedAccount' },
56
+ { foreign_key: 'internal_account_id', relation_name: 'internal_account',
57
+ collection: 'MambuInternalAccount' },
58
+ { foreign_key: 'external_account_id', relation_name: 'external_account',
59
+ collection: 'MambuExternalAccount' }
60
+ ]
61
+ end
62
+
63
+ private
64
+
65
+ def define_schema
66
+ add_field('id', ColumnSchema.new(column_type: 'String', is_primary_key: true,
67
+ is_read_only: true, is_sortable: true))
68
+ add_field('object', ColumnSchema.new(column_type: 'String', is_read_only: true, is_sortable: false))
69
+ add_field('connected_account_id', ColumnSchema.new(column_type: 'String',
70
+ is_read_only: true, is_sortable: true))
71
+ add_field('category', ColumnSchema.new(column_type: 'String', is_read_only: true, is_sortable: true))
72
+ add_field('direction', ColumnSchema.new(column_type: 'Enum', enum_values: ENUM_DIRECTION,
73
+ is_read_only: true, is_sortable: false))
74
+ add_field('amount', ColumnSchema.new(column_type: 'Number', is_read_only: true, is_sortable: false))
75
+ add_field('currency', ColumnSchema.new(column_type: 'String', is_read_only: true, is_sortable: false))
76
+ add_field('description', ColumnSchema.new(column_type: 'String', is_read_only: true, is_sortable: false))
77
+ add_field('structured_reference', ColumnSchema.new(column_type: 'String', is_read_only: true,
78
+ is_sortable: false))
79
+ add_field('value_date', ColumnSchema.new(column_type: 'Date', is_read_only: true, is_sortable: true))
80
+ add_field('booking_date', ColumnSchema.new(column_type: 'Date', is_read_only: true, is_sortable: true))
81
+ add_field('internal_account_id', ColumnSchema.new(column_type: 'String',
82
+ is_read_only: true, is_sortable: false))
83
+ add_field('external_account_id', ColumnSchema.new(column_type: 'String',
84
+ is_read_only: true, is_sortable: false))
85
+ add_field('uetr', ColumnSchema.new(column_type: 'String', is_read_only: true, is_sortable: false))
86
+ add_field('bank_data', ColumnSchema.new(column_type: 'Json', is_read_only: true, is_sortable: false))
87
+ add_field('reconciliation_status', ColumnSchema.new(column_type: 'String', is_read_only: true,
88
+ is_sortable: true))
89
+ add_field('reconciled_amount', ColumnSchema.new(column_type: 'Number', is_read_only: true, is_sortable: false))
90
+ add_field('custom_fields', ColumnSchema.new(column_type: 'Json', is_read_only: true, is_sortable: false))
91
+ add_field('created_at', ColumnSchema.new(column_type: 'Date', is_read_only: true, is_sortable: true))
92
+ end
93
+
94
+ def define_relations
95
+ add_field('connected_account', ManyToOneSchema.new(
96
+ foreign_collection: 'MambuConnectedAccount',
97
+ foreign_key: 'connected_account_id',
98
+ foreign_key_target: 'id'
99
+ ))
100
+ add_field('internal_account', ManyToOneSchema.new(
101
+ foreign_collection: 'MambuInternalAccount',
102
+ foreign_key: 'internal_account_id',
103
+ foreign_key_target: 'id'
104
+ ))
105
+ add_field('external_account', ManyToOneSchema.new(
106
+ foreign_collection: 'MambuExternalAccount',
107
+ foreign_key: 'external_account_id',
108
+ foreign_key_target: 'id'
109
+ ))
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,36 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ class Configuration
3
+ DEFAULT_BASE_URL = 'https://api.numeral.io'.freeze
4
+ SANDBOX_BASE_URL = 'https://api.sandbox.numeral.io'.freeze
5
+ API_VERSION = 'v1'.freeze
6
+
7
+ attr_reader :api_key, :base_url, :open_timeout, :timeout
8
+
9
+ def initialize(api_key:, base_url: nil, sandbox: false, open_timeout: 5, timeout: 30)
10
+ @api_key = api_key
11
+ @base_url = base_url || (sandbox ? SANDBOX_BASE_URL : DEFAULT_BASE_URL)
12
+ @open_timeout = open_timeout
13
+ @timeout = timeout
14
+ validate!
15
+ end
16
+
17
+ def url
18
+ "#{@base_url.chomp("/")}/#{API_VERSION}"
19
+ end
20
+
21
+ private
22
+
23
+ def validate!
24
+ missing = []
25
+ missing << 'api_key' if blank?(@api_key)
26
+ return if missing.empty?
27
+
28
+ raise ConfigurationError,
29
+ "ForestAdminDatasourceMambuPayments missing required config: #{missing.join(", ")}"
30
+ end
31
+
32
+ def blank?(value)
33
+ value.nil? || value.to_s.strip.empty?
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,35 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ class Datasource < ForestAdminDatasourceToolkit::Datasource
3
+ attr_reader :client, :configuration
4
+
5
+ def initialize(api_key:, base_url: nil, sandbox: false)
6
+ super()
7
+ @configuration = Configuration.new(api_key: api_key, base_url: base_url, sandbox: sandbox)
8
+ @client = Client.new(@configuration)
9
+
10
+ register_collections
11
+ end
12
+
13
+ private
14
+
15
+ def register_collections
16
+ add_collection(Collections::ConnectedAccount.new(self))
17
+ add_collection(Collections::PaymentOrder.new(self))
18
+ add_collection(Collections::Transaction.new(self))
19
+ add_collection(Collections::Balance.new(self))
20
+ add_collection(Collections::AccountHolder.new(self))
21
+ add_collection(Collections::ExternalAccount.new(self))
22
+ add_collection(Collections::InternalAccount.new(self))
23
+ add_collection(Collections::IncomingPayment.new(self))
24
+ add_collection(Collections::DirectDebitMandate.new(self))
25
+ add_collection(Collections::ExpectedPayment.new(self))
26
+ add_collection(Collections::Event.new(self))
27
+ add_collection(Collections::File.new(self))
28
+ add_collection(Collections::Return.new(self))
29
+ add_collection(Collections::Claim.new(self))
30
+ add_collection(Collections::Reconciliation.new(self))
31
+ add_collection(Collections::PaymentCapture.new(self))
32
+ add_collection(Collections::PayeeVerificationRequest.new(self))
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,31 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ module Plugins
3
+ # Numeral's list endpoints have no free-text search. Forest emulates search
4
+ # by OR-ing a condition over every searchable column, which the
5
+ # ConditionTreeTranslator deliberately rejects (it cannot push an OR to
6
+ # Numeral) — so an operator typing in the search bar would get an error.
7
+ #
8
+ # This plugin turns the search bar off on every Mambu collection, which is
9
+ # the honest behaviour: the API can filter (per `api_filters`) but not
10
+ # search. Install it once at the datasource level:
11
+ #
12
+ # @agent.use(ForestAdminDatasourceMambuPayments::Plugins::DisableSearch, {})
13
+ class DisableSearch < ForestAdminDatasourceCustomizer::Plugins::Plugin
14
+ COLLECTIONS = %w[
15
+ MambuConnectedAccount MambuPaymentOrder MambuTransaction MambuBalance
16
+ MambuAccountHolder MambuExternalAccount MambuInternalAccount
17
+ MambuIncomingPayment MambuDirectDebitMandate MambuExpectedPayment
18
+ MambuEvent MambuFile MambuReturn MambuClaim MambuReconciliation
19
+ MambuPaymentCapture MambuPayeeVerificationRequest
20
+ ].freeze
21
+
22
+ def run(datasource_customizer, _collection_customizer = nil, _options = {})
23
+ Helpers.require_datasource!(datasource_customizer, self.class)
24
+
25
+ COLLECTIONS.each do |name|
26
+ datasource_customizer.customize_collection(name, &:disable_search)
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,94 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ module Plugins
3
+ # Shared helpers for Mambu Payments plugins: input normalization, host
4
+ # record id resolution, and per-id rescue logic for bulk transitions.
5
+ module Helpers
6
+ ActionScope = ForestAdminDatasourceCustomizer::Decorators::Action::Types::ActionScope
7
+
8
+ SCOPE_KEYS = %i[single bulk].freeze
9
+ SCOPES = { single: ActionScope::SINGLE, bulk: ActionScope::BULK }.freeze
10
+
11
+ module_function
12
+
13
+ # Relation plugins must be installed at the datasource level
14
+ # (`@agent.use(plugin, {})`) so they can customize several collections at
15
+ # once. Raises a clear error when a caller installs one on a single
16
+ # collection instead.
17
+ def require_datasource!(datasource_customizer, plugin_class)
18
+ return if datasource_customizer
19
+
20
+ name = plugin_class.is_a?(Class) ? plugin_class.name.split('::').last : plugin_class
21
+ raise ArgumentError,
22
+ "#{name} must be installed at the datasource level via @agent.use(plugin, {})"
23
+ end
24
+
25
+ def normalize_scopes(value)
26
+ list = Array(value).map(&:to_sym).uniq
27
+ list = SCOPE_KEYS if list.empty?
28
+ unknown = list - SCOPE_KEYS
29
+ return list if unknown.empty?
30
+
31
+ raise ForestAdminDatasourceToolkit::Exceptions::ForestException,
32
+ "Unknown scopes: #{unknown.join(", ")}. Allowed: #{SCOPE_KEYS.join(", ")}."
33
+ end
34
+
35
+ def resolve_ids(context, field)
36
+ records = context.get_records([field])
37
+ records = [records].compact unless records.is_a?(Array)
38
+ records.filter_map { |r| r[field] || r[field.to_sym] }
39
+ rescue StandardError => e
40
+ ForestAdminDatasourceMambuPayments.logger.warn(
41
+ "[forest_admin_datasource_mambu_payments] failed to resolve ids from '#{field}': " \
42
+ "#{e.class}: #{e.message}"
43
+ )
44
+ []
45
+ end
46
+
47
+ # Per-id rescue so a single API failure doesn't abort the remaining ids.
48
+ def each_with_rescue(ids, label)
49
+ succeeded = []
50
+ failed = []
51
+ ids.each do |id|
52
+ yield id
53
+ succeeded << id
54
+ rescue StandardError => e
55
+ ForestAdminDatasourceMambuPayments.logger.warn(
56
+ "[forest_admin_datasource_mambu_payments] #{label} failed for ##{id}: #{e.class}: #{e.message}"
57
+ )
58
+ failed << [id, "#{e.class}: #{e.message}"]
59
+ end
60
+ [succeeded, failed]
61
+ end
62
+
63
+ def present?(value)
64
+ !value.nil? && value.to_s != ''
65
+ end
66
+
67
+ def to_int(value)
68
+ return nil unless present?(value)
69
+
70
+ Integer(value.to_s)
71
+ rescue ArgumentError, TypeError
72
+ nil
73
+ end
74
+
75
+ def write_back(context, field, value)
76
+ return :skipped if field.nil? || value.nil?
77
+
78
+ context.collection.update(context.filter, { field => value })
79
+ :ok
80
+ rescue StandardError => e
81
+ ForestAdminDatasourceMambuPayments.logger.warn(
82
+ "[forest_admin_datasource_mambu_payments] write-back to '#{field}' failed: #{e.class}: #{e.message}"
83
+ )
84
+ [:failed, "#{e.class}: #{e.message}"]
85
+ end
86
+
87
+ def write_back_warning(writeback)
88
+ return nil unless writeback.is_a?(Array) && writeback.first == :failed
89
+
90
+ " (warning: could not write the id back to the host record: #{writeback.last})"
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,30 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ module Plugins
3
+ module Messages
4
+ module_function
5
+
6
+ def success(succeeded, failed, noun:, verb_past:)
7
+ [succeeded_phrase(succeeded, noun, verb_past), failed_phrase(failed, noun)].compact.join(' ')
8
+ end
9
+
10
+ def all_failed(failed, noun:, verb:)
11
+ return "Failed to #{verb} #{noun} ##{failed.first.first}: #{failed.first.last}" if failed.size == 1
12
+
13
+ "Failed to #{verb} all #{failed.size} #{noun}s. First error: #{failed.first.last}"
14
+ end
15
+
16
+ def succeeded_phrase(succeeded, noun, verb_past)
17
+ return nil if succeeded.empty?
18
+ return "#{noun.capitalize} ##{succeeded.first} #{verb_past}." if succeeded.size == 1
19
+
20
+ "#{succeeded.size} #{noun}s #{verb_past}."
21
+ end
22
+
23
+ def failed_phrase(failed, _noun)
24
+ return nil if failed.empty?
25
+
26
+ "#{failed.size} failed: #{failed.map(&:first).join(", ")}."
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,56 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ module Plugins
3
+ module Relations
4
+ # Base for AccountHolder links reached transitively through an account
5
+ # relation. The `host` imports `account_holder_id` from a related account,
6
+ # exposes a ManyToOne to AccountHolder, and gets a TwoStepHolderFilter
7
+ # (which rewrites holder filters onto `local_fk`); AccountHolder gets the
8
+ # reciprocal OneToMany. Subclasses configure it declaratively:
9
+ #
10
+ # class LinkAccountHolderToIncomingPayments < HolderLinkPlugin
11
+ # link host: 'MambuIncomingPayment', name: 'incoming_payments',
12
+ # local_fk: 'internal_account_id', intermediate: 'MambuInternalAccount',
13
+ # import_path: 'internal_account:account_holder_id'
14
+ # end
15
+ #
16
+ # Install at the datasource level: @agent.use(plugin, {}).
17
+ class HolderLinkPlugin < ForestAdminDatasourceCustomizer::Plugins::Plugin
18
+ ACCOUNT_HOLDER = 'MambuAccountHolder'.freeze
19
+ FK_NAME = 'account_holder_id'.freeze
20
+
21
+ class << self
22
+ attr_reader :config
23
+ end
24
+
25
+ # rubocop:disable Metrics/ParameterLists
26
+ def self.link(host:, name:, local_fk:, intermediate:, import_path:, many_to_one_name: 'account_holder')
27
+ @config = {
28
+ host: host, name: name, local_fk: local_fk, intermediate: intermediate,
29
+ import_path: import_path, many_to_one_name: many_to_one_name
30
+ }
31
+ end
32
+ # rubocop:enable Metrics/ParameterLists
33
+
34
+ def run(datasource_customizer, _collection_customizer = nil, _options = {})
35
+ Plugins::Helpers.require_datasource!(datasource_customizer, self.class)
36
+
37
+ cfg = self.class.config
38
+ datasource_customizer.customize_collection(cfg[:host]) do |c|
39
+ c.import_field(FK_NAME, path: cfg[:import_path], readonly: true)
40
+ c.add_many_to_one_relation(cfg[:many_to_one_name], ACCOUNT_HOLDER,
41
+ foreign_key: FK_NAME, foreign_key_target: 'id')
42
+ TwoStepHolderFilter.install(c,
43
+ fk_name: FK_NAME,
44
+ local_fk: cfg[:local_fk],
45
+ intermediate_collection: cfg[:intermediate])
46
+ end
47
+
48
+ datasource_customizer.customize_collection(ACCOUNT_HOLDER) do |c|
49
+ c.add_one_to_many_relation(cfg[:name], cfg[:host],
50
+ origin_key: FK_NAME, origin_key_target: 'id')
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,14 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ module Plugins
3
+ module Relations
4
+ # AccountHolder <-> DirectDebitMandate, transitive via the external account
5
+ # (DirectDebitMandate.external_account.account_holder_id).
6
+ # Install at the datasource level: @agent.use(plugin, {}).
7
+ class LinkAccountHolderToDirectDebitMandates < HolderLinkPlugin
8
+ link host: 'MambuDirectDebitMandate', name: 'direct_debit_mandates',
9
+ local_fk: 'external_account_id', intermediate: 'MambuExternalAccount',
10
+ import_path: 'external_account:account_holder_id'
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,14 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ module Plugins
3
+ module Relations
4
+ # AccountHolder <-> IncomingPayment, transitive via the internal account
5
+ # (IncomingPayment.internal_account.account_holder_id).
6
+ # Install at the datasource level: @agent.use(plugin, {}).
7
+ class LinkAccountHolderToIncomingPayments < HolderLinkPlugin
8
+ link host: 'MambuIncomingPayment', name: 'incoming_payments',
9
+ local_fk: 'internal_account_id', intermediate: 'MambuInternalAccount',
10
+ import_path: 'internal_account:account_holder_id'
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,13 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ module Plugins
3
+ module Relations
4
+ # Reciprocal OneToMany on MambuExternalAccount for the native
5
+ # DirectDebitMandate.external_account ManyToOne.
6
+ # Install at the datasource level: @agent.use(plugin, {}).
7
+ class LinkExternalAccountToDirectDebitMandates < OneToManyLinkPlugin
8
+ link host: 'MambuExternalAccount', to: 'MambuDirectDebitMandate',
9
+ name: 'direct_debit_mandates', origin_key: 'external_account_id'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ module Plugins
3
+ module Relations
4
+ # Reciprocal OneToMany on MambuExternalAccount for the native
5
+ # IncomingPayment.external_account ManyToOne.
6
+ # Install at the datasource level: @agent.use(plugin, {}).
7
+ class LinkExternalAccountToIncomingPayments < OneToManyLinkPlugin
8
+ link host: 'MambuExternalAccount', to: 'MambuIncomingPayment',
9
+ name: 'incoming_payments', origin_key: 'external_account_id'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ module Plugins
3
+ module Relations
4
+ # Reciprocal OneToMany on MambuExternalAccount for the native
5
+ # PaymentOrder.external_account ManyToOne (receiving account).
6
+ # Install at the datasource level: @agent.use(plugin, {}).
7
+ class LinkExternalAccountToPaymentOrders < OneToManyLinkPlugin
8
+ link host: 'MambuExternalAccount', to: 'MambuPaymentOrder',
9
+ name: 'payment_orders', origin_key: 'receiving_account_id'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ module Plugins
3
+ module Relations
4
+ # OneToMany on MambuIncomingPayment over Event.related_object_id
5
+ # (events emitted for this incoming payment).
6
+ # Install at the datasource level: @agent.use(plugin, {}).
7
+ class LinkIncomingPaymentToEvents < OneToManyLinkPlugin
8
+ link host: 'MambuIncomingPayment', to: 'MambuEvent',
9
+ name: 'events', origin_key: 'related_object_id'
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,21 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ module Plugins
3
+ module Relations
4
+ # IncomingPayment <-> ExpectedPayment matched through two reconciliations
5
+ # sharing a transaction (cross-pivot resolution).
6
+ # Install at the datasource level: @agent.use(plugin, {}).
7
+ class LinkIncomingPaymentToExpectedPayments < TwoStepLinkPlugin
8
+ link owner: 'MambuIncomingPayment', filtered: 'MambuExpectedPayment',
9
+ name: 'matched_expected_payments', foreign_key: 'incoming_payment_id'
10
+
11
+ def install_source_filter(collection)
12
+ TwoStepCrossReconciliationFilter.install(collection,
13
+ fk_name: 'incoming_payment_id',
14
+ src_payment_type: 'incoming_payment',
15
+ dst_payment_type: 'expected_payment',
16
+ target_field: 'id')
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,12 @@
1
+ module ForestAdminDatasourceMambuPayments
2
+ module Plugins
3
+ module Relations
4
+ # OneToMany on MambuIncomingPayment over Return.related_payment_id.
5
+ # Install at the datasource level: @agent.use(plugin, {}).
6
+ class LinkIncomingPaymentToReturns < OneToManyLinkPlugin
7
+ link host: 'MambuIncomingPayment', to: 'MambuReturn',
8
+ name: 'returns', origin_key: 'related_payment_id'
9
+ end
10
+ end
11
+ end
12
+ end