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
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# IncomingPayment <-> Transaction through the Reconciliation pivot
|
|
5
|
+
# (Reconciliation.payment_id + payment_type = incoming_payment).
|
|
6
|
+
# Install at the datasource level: @agent.use(plugin, {}).
|
|
7
|
+
class LinkIncomingPaymentToTransactions < TwoStepLinkPlugin
|
|
8
|
+
link owner: 'MambuIncomingPayment', filtered: 'MambuTransaction',
|
|
9
|
+
name: 'transactions', foreign_key: 'incoming_payment_id'
|
|
10
|
+
|
|
11
|
+
def install_source_filter(collection)
|
|
12
|
+
TwoStepReconciliationFilter.install(collection,
|
|
13
|
+
fk_name: 'incoming_payment_id',
|
|
14
|
+
payment_type: 'incoming_payment',
|
|
15
|
+
target_field: 'id')
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# InternalAccount <-> Balance, transitive via
|
|
5
|
+
# InternalAccount.connected_account_ids vs Balance.connected_account_id.
|
|
6
|
+
# Install at the datasource level: @agent.use(plugin, {}).
|
|
7
|
+
class LinkInternalAccountToBalances < TwoStepLinkPlugin
|
|
8
|
+
link owner: 'MambuInternalAccount', filtered: 'MambuBalance',
|
|
9
|
+
name: 'balances', foreign_key: 'internal_account_id'
|
|
10
|
+
|
|
11
|
+
def install_source_filter(collection)
|
|
12
|
+
TwoStepConnectedAccountFilter.install(collection, target_field: 'connected_account_id')
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# Reciprocal OneToMany on MambuInternalAccount for the native
|
|
5
|
+
# IncomingPayment.internal_account ManyToOne.
|
|
6
|
+
# Install at the datasource level: @agent.use(plugin, {}).
|
|
7
|
+
class LinkInternalAccountToIncomingPayments < OneToManyLinkPlugin
|
|
8
|
+
link host: 'MambuInternalAccount', to: 'MambuIncomingPayment',
|
|
9
|
+
name: 'incoming_payments', origin_key: 'internal_account_id'
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# InternalAccount <-> PaymentOrder, transitive via
|
|
5
|
+
# InternalAccount.connected_account_ids vs PaymentOrder.connected_account_id.
|
|
6
|
+
# Install at the datasource level: @agent.use(plugin, {}).
|
|
7
|
+
class LinkInternalAccountToPaymentOrders < TwoStepLinkPlugin
|
|
8
|
+
link owner: 'MambuInternalAccount', filtered: 'MambuPaymentOrder',
|
|
9
|
+
name: 'payment_orders', foreign_key: 'internal_account_id'
|
|
10
|
+
|
|
11
|
+
def install_source_filter(collection)
|
|
12
|
+
TwoStepConnectedAccountFilter.install(collection, target_field: 'connected_account_id')
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_payment_order_to_events.rb
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# OneToMany on MambuPaymentOrder over Event.related_object_id
|
|
5
|
+
# (events emitted for this payment order).
|
|
6
|
+
# Install at the datasource level: @agent.use(plugin, {}).
|
|
7
|
+
class LinkPaymentOrderToEvents < OneToManyLinkPlugin
|
|
8
|
+
link host: 'MambuPaymentOrder', to: 'MambuEvent',
|
|
9
|
+
name: 'events', origin_key: 'related_object_id'
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# PaymentOrder <-> AccountHolder of the receiving external account.
|
|
5
|
+
# Named `receiving_account_holder` to disambiguate from the originating side.
|
|
6
|
+
# Install at the datasource level: @agent.use(plugin, {}).
|
|
7
|
+
class LinkPaymentOrderToReceivingAccountHolder < HolderLinkPlugin
|
|
8
|
+
link host: 'MambuPaymentOrder', name: 'payment_orders',
|
|
9
|
+
local_fk: 'receiving_account_id', intermediate: 'MambuExternalAccount',
|
|
10
|
+
import_path: 'external_account:account_holder_id',
|
|
11
|
+
many_to_one_name: 'receiving_account_holder'
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/forest_admin_datasource_mambu_payments/plugins/relations/link_payment_order_to_returns.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# OneToMany on MambuPaymentOrder over Return.related_payment_id.
|
|
5
|
+
# Install at the datasource level: @agent.use(plugin, {}).
|
|
6
|
+
class LinkPaymentOrderToReturns < OneToManyLinkPlugin
|
|
7
|
+
link host: 'MambuPaymentOrder', to: 'MambuReturn',
|
|
8
|
+
name: 'returns', origin_key: 'related_payment_id'
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# Exposes a navigable PaymentOrder <-> Transaction link.
|
|
5
|
+
# Transaction has no native payment_order_id; the relation is mediated by
|
|
6
|
+
# MambuReconciliation (Reconciliation.payment_id + payment_type discriminator).
|
|
7
|
+
# See TwoStepReconciliationFilter for the OneToMany filter rewrite.
|
|
8
|
+
#
|
|
9
|
+
# Install at the datasource level:
|
|
10
|
+
# @agent.use(
|
|
11
|
+
# ForestAdminDatasourceMambuPayments::Plugins::Relations::LinkPaymentOrderToTransactions,
|
|
12
|
+
# {}
|
|
13
|
+
# )
|
|
14
|
+
class LinkPaymentOrderToTransactions < ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
15
|
+
ComputedDefinition = ForestAdminDatasourceCustomizer::Decorators::Computed::ComputedDefinition
|
|
16
|
+
|
|
17
|
+
PAYMENT_ORDER = 'MambuPaymentOrder'.freeze
|
|
18
|
+
TRANSACTION = 'MambuTransaction'.freeze
|
|
19
|
+
FK_NAME = 'payment_order_id'.freeze
|
|
20
|
+
PAYMENT_TYPE = 'payment_order'.freeze
|
|
21
|
+
ONE_TO_MANY_NAME = 'transactions'.freeze
|
|
22
|
+
|
|
23
|
+
def run(datasource_customizer, _collection_customizer = nil, _options = {})
|
|
24
|
+
Plugins::Helpers.require_datasource!(datasource_customizer, self.class)
|
|
25
|
+
|
|
26
|
+
datasource_customizer.customize_collection(TRANSACTION) do |c|
|
|
27
|
+
# Virtual column: Transaction has no native payment_order_id.
|
|
28
|
+
# Reverse lookup would require scanning all reconciliations — kept
|
|
29
|
+
# nil per record; only EQUAL/IN filters are rewritten via the
|
|
30
|
+
# TwoStepReconciliationFilter below.
|
|
31
|
+
c.add_field(FK_NAME, ComputedDefinition.new(
|
|
32
|
+
column_type: 'String',
|
|
33
|
+
dependencies: ['id'],
|
|
34
|
+
values: proc { |records, _ctx| records.map { nil } }
|
|
35
|
+
))
|
|
36
|
+
TwoStepReconciliationFilter.install(c,
|
|
37
|
+
fk_name: FK_NAME,
|
|
38
|
+
payment_type: PAYMENT_TYPE,
|
|
39
|
+
target_field: 'id')
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
datasource_customizer.customize_collection(PAYMENT_ORDER) do |c|
|
|
43
|
+
c.add_one_to_many_relation(ONE_TO_MANY_NAME, TRANSACTION,
|
|
44
|
+
origin_key: FK_NAME,
|
|
45
|
+
origin_key_target: 'id')
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# Base for the "simple" reciprocal OneToMany links: the target already
|
|
5
|
+
# carries a native foreign key, so the plugin only declares the relation
|
|
6
|
+
# on the host. Subclasses configure it declaratively:
|
|
7
|
+
#
|
|
8
|
+
# class LinkExternalAccountToIncomingPayments < OneToManyLinkPlugin
|
|
9
|
+
# link host: 'MambuExternalAccount', to: 'MambuIncomingPayment',
|
|
10
|
+
# name: 'incoming_payments', origin_key: 'external_account_id'
|
|
11
|
+
# end
|
|
12
|
+
#
|
|
13
|
+
# Install at the datasource level: @agent.use(plugin, {}).
|
|
14
|
+
class OneToManyLinkPlugin < ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
15
|
+
class << self
|
|
16
|
+
attr_reader :config
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.link(host:, to:, name:, origin_key:)
|
|
20
|
+
@config = { host: host, to: to, name: name, origin_key: origin_key }
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def run(datasource_customizer, _collection_customizer = nil, _options = {})
|
|
24
|
+
Plugins::Helpers.require_datasource!(datasource_customizer, self.class)
|
|
25
|
+
|
|
26
|
+
cfg = self.class.config
|
|
27
|
+
datasource_customizer.customize_collection(cfg[:host]) do |c|
|
|
28
|
+
c.add_one_to_many_relation(cfg[:name], cfg[:to],
|
|
29
|
+
origin_key: cfg[:origin_key], origin_key_target: 'id')
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# Shared machinery for the "two-step" relation filters. Forest's native
|
|
5
|
+
# OneToMany navigation emits a leaf on a virtual foreign key; these filters
|
|
6
|
+
# pre-resolve that key against an intermediate collection and rewrite the
|
|
7
|
+
# predicate as `target_field IN [resolved ids]`.
|
|
8
|
+
#
|
|
9
|
+
# Centralising it here keeps the four concrete filters in sync: the
|
|
10
|
+
# match-nothing sentinel, the EQUAL/IN normalisation, and — crucially —
|
|
11
|
+
# the *paginated* intermediate read all live in one place. A single
|
|
12
|
+
# un-paginated `list` would silently cap resolution at one API page and
|
|
13
|
+
# drop matching records for large relations.
|
|
14
|
+
module PivotResolution
|
|
15
|
+
Operators = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Operators
|
|
16
|
+
ConditionTreeLeaf = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes::ConditionTreeLeaf
|
|
17
|
+
ConditionTreeBranch = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes::ConditionTreeBranch
|
|
18
|
+
Filter = ForestAdminDatasourceToolkit::Components::Query::Filter
|
|
19
|
+
Projection = ForestAdminDatasourceToolkit::Components::Query::Projection
|
|
20
|
+
Page = ForestAdminDatasourceToolkit::Components::Query::Page
|
|
21
|
+
|
|
22
|
+
# All-zero UUID: guaranteed not to exist in Numeral, so the native list
|
|
23
|
+
# returns []. Expresses "match nothing" without tripping the empty-IN
|
|
24
|
+
# guard in ConditionTreeTranslator.
|
|
25
|
+
NO_MATCH_SENTINEL = '00000000-0000-0000-0000-000000000000'.freeze
|
|
26
|
+
|
|
27
|
+
# Only EQUAL/IN are rewritten — the operators Forest's OneToMany
|
|
28
|
+
# navigation actually emits.
|
|
29
|
+
SUPPORTED_OPS = [Operators::EQUAL, Operators::IN].freeze
|
|
30
|
+
|
|
31
|
+
# Upper bound on resolved ids. The host collection walks Numeral's cursor
|
|
32
|
+
# pages under the hood; we ask for one large window so it fetches them all
|
|
33
|
+
# in O(n / page) rather than re-walking per offset. A relation resolving
|
|
34
|
+
# to more than this is logged rather than silently truncated.
|
|
35
|
+
MAX_RESOLVED = 10_000
|
|
36
|
+
|
|
37
|
+
module_function
|
|
38
|
+
|
|
39
|
+
def normalize(value, operator)
|
|
40
|
+
values = operator == Operators::IN ? Array(value) : [value]
|
|
41
|
+
values.compact.reject { |v| v.to_s.empty? }.uniq
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def no_match(target_field)
|
|
45
|
+
ConditionTreeLeaf.new(target_field, Operators::EQUAL, NO_MATCH_SENTINEL)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def and_branch(*leaves)
|
|
49
|
+
ConditionTreeBranch.new('And', leaves)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Lists every row of `collection_name` matching `condition_tree` and
|
|
53
|
+
# returns the unique non-empty values of `field` (handles both scalar
|
|
54
|
+
# columns and array columns such as InternalAccount.connected_account_ids).
|
|
55
|
+
# One large-window request lets the collection's cursor pagination fetch
|
|
56
|
+
# all matching rows in a single forward walk.
|
|
57
|
+
def collect(context, collection_name, condition_tree, field)
|
|
58
|
+
rows = context.datasource.get_collection(collection_name).list(
|
|
59
|
+
Filter.new(condition_tree: condition_tree, page: Page.new(offset: 0, limit: MAX_RESOLVED)),
|
|
60
|
+
Projection.new([field])
|
|
61
|
+
)
|
|
62
|
+
if rows.size >= MAX_RESOLVED
|
|
63
|
+
ForestAdminDatasourceMambuPayments.logger.warn(
|
|
64
|
+
"[forest_admin_datasource_mambu_payments] #{collection_name} relation resolution hit the " \
|
|
65
|
+
"#{MAX_RESOLVED}-row cap on '#{field}'; results may be truncated."
|
|
66
|
+
)
|
|
67
|
+
end
|
|
68
|
+
rows.flat_map { |row| Array(row[field]) }.compact.reject { |v| v.to_s.empty? }.uniq
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# Two-step pre-resolution for `internal_account_id` filters on host
|
|
5
|
+
# collections that link to InternalAccount transitively via the array
|
|
6
|
+
# column `InternalAccount.connected_account_ids` (not a scalar FK).
|
|
7
|
+
# Resolves the holder ids to the set of connected_account ids, then
|
|
8
|
+
# rewrites the predicate against a real field on the host collection
|
|
9
|
+
# (`id` for ConnectedAccount, `connected_account_id` for resources
|
|
10
|
+
# scoped by connected account).
|
|
11
|
+
module TwoStepConnectedAccountFilter
|
|
12
|
+
Operators = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Operators
|
|
13
|
+
ConditionTreeLeaf = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes::ConditionTreeLeaf
|
|
14
|
+
|
|
15
|
+
INTERNAL_ACCOUNT = 'MambuInternalAccount'.freeze
|
|
16
|
+
ARRAY_FIELD = 'connected_account_ids'.freeze
|
|
17
|
+
FK_NAME = 'internal_account_id'.freeze
|
|
18
|
+
|
|
19
|
+
def self.install(collection_customizer, target_field:)
|
|
20
|
+
PivotResolution::SUPPORTED_OPS.each do |operator|
|
|
21
|
+
collection_customizer.replace_field_operator(FK_NAME, operator) do |value, context|
|
|
22
|
+
ia_ids = PivotResolution.normalize(value, operator)
|
|
23
|
+
next PivotResolution.no_match(target_field) if ia_ids.empty?
|
|
24
|
+
|
|
25
|
+
ca_ids = PivotResolution.collect(
|
|
26
|
+
context, INTERNAL_ACCOUNT,
|
|
27
|
+
ConditionTreeLeaf.new('id', Operators::IN, ia_ids), ARRAY_FIELD
|
|
28
|
+
)
|
|
29
|
+
next PivotResolution.no_match(target_field) if ca_ids.empty?
|
|
30
|
+
|
|
31
|
+
ConditionTreeLeaf.new(target_field, Operators::IN, ca_ids)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# Resolves "matched payment" filters that cross the Reconciliation pivot
|
|
5
|
+
# twice via the shared transaction. Used when two payment resources only
|
|
6
|
+
# know about each other through reconciliations against the same
|
|
7
|
+
# Transaction (e.g. IncomingPayment <-> ExpectedPayment).
|
|
8
|
+
#
|
|
9
|
+
# Chain (for a `matched_X_id` filter installed on host collection Y):
|
|
10
|
+
# Reconciliation WHERE payment_id IN [x_ids] AND payment_type = src
|
|
11
|
+
# -> transaction_ids
|
|
12
|
+
# Reconciliation WHERE transaction_id IN [tx_ids] AND payment_type = dst
|
|
13
|
+
# -> y_ids
|
|
14
|
+
# The predicate is then rewritten as `target_field IN y_ids` on the host.
|
|
15
|
+
module TwoStepCrossReconciliationFilter
|
|
16
|
+
Operators = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Operators
|
|
17
|
+
ConditionTreeLeaf = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes::ConditionTreeLeaf
|
|
18
|
+
|
|
19
|
+
RECONCILIATION = 'MambuReconciliation'.freeze
|
|
20
|
+
|
|
21
|
+
def self.install(collection_customizer, fk_name:, src_payment_type:, dst_payment_type:, target_field:)
|
|
22
|
+
PivotResolution::SUPPORTED_OPS.each do |operator|
|
|
23
|
+
collection_customizer.replace_field_operator(fk_name, operator) do |value, context|
|
|
24
|
+
src_ids = PivotResolution.normalize(value, operator)
|
|
25
|
+
next PivotResolution.no_match(target_field) if src_ids.empty?
|
|
26
|
+
|
|
27
|
+
tx_ids = TwoStepCrossReconciliationFilter.resolve(context, 'payment_id', src_ids,
|
|
28
|
+
src_payment_type, 'transaction_id')
|
|
29
|
+
next PivotResolution.no_match(target_field) if tx_ids.empty?
|
|
30
|
+
|
|
31
|
+
dst_ids = TwoStepCrossReconciliationFilter.resolve(context, 'transaction_id', tx_ids,
|
|
32
|
+
dst_payment_type, 'payment_id')
|
|
33
|
+
next PivotResolution.no_match(target_field) if dst_ids.empty?
|
|
34
|
+
|
|
35
|
+
ConditionTreeLeaf.new(target_field, Operators::IN, dst_ids)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# One hop across the reconciliation pivot: rows where `where_field IN ids`
|
|
41
|
+
# and `payment_type = type`, projected onto `select_field`.
|
|
42
|
+
def self.resolve(context, where_field, ids, payment_type, select_field)
|
|
43
|
+
PivotResolution.collect(
|
|
44
|
+
context, RECONCILIATION,
|
|
45
|
+
PivotResolution.and_branch(
|
|
46
|
+
ConditionTreeLeaf.new(where_field, Operators::IN, ids),
|
|
47
|
+
ConditionTreeLeaf.new('payment_type', Operators::EQUAL, payment_type)
|
|
48
|
+
),
|
|
49
|
+
select_field
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# Two-step pre-resolution for `account_holder_id` filters on host
|
|
5
|
+
# collections that link to AccountHolder transitively. Default
|
|
6
|
+
# `import_field` would emit a nested leaf the native list rejects;
|
|
7
|
+
# we pre-list the intermediate collection and rewrite as
|
|
8
|
+
# `local_fk IN (ids)`.
|
|
9
|
+
module TwoStepHolderFilter
|
|
10
|
+
Operators = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Operators
|
|
11
|
+
ConditionTreeLeaf = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes::ConditionTreeLeaf
|
|
12
|
+
|
|
13
|
+
def self.install(collection_customizer, fk_name:, local_fk:, intermediate_collection:)
|
|
14
|
+
PivotResolution::SUPPORTED_OPS.each do |operator|
|
|
15
|
+
collection_customizer.replace_field_operator(fk_name, operator) do |value, context|
|
|
16
|
+
holder_ids = PivotResolution.normalize(value, operator)
|
|
17
|
+
next PivotResolution.no_match(local_fk) if holder_ids.empty?
|
|
18
|
+
|
|
19
|
+
fk_ids = PivotResolution.collect(
|
|
20
|
+
context, intermediate_collection,
|
|
21
|
+
ConditionTreeLeaf.new(fk_name, Operators::IN, holder_ids), 'id'
|
|
22
|
+
)
|
|
23
|
+
next PivotResolution.no_match(local_fk) if fk_ids.empty?
|
|
24
|
+
|
|
25
|
+
ConditionTreeLeaf.new(local_fk, Operators::IN, fk_ids)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# Base for relations whose foreign key is not native: the `filtered`
|
|
5
|
+
# collection gets a virtual (always-nil) FK column plus a two-step
|
|
6
|
+
# operator filter that rewrites EQUAL/IN predicates, and the `owner`
|
|
7
|
+
# collection gets the reciprocal OneToMany. Subclasses declare the shape
|
|
8
|
+
# and provide the concrete filter install:
|
|
9
|
+
#
|
|
10
|
+
# class LinkInternalAccountToBalances < TwoStepLinkPlugin
|
|
11
|
+
# link owner: 'MambuInternalAccount', filtered: 'MambuBalance',
|
|
12
|
+
# name: 'balances', fk: 'internal_account_id'
|
|
13
|
+
# def install_source_filter(collection)
|
|
14
|
+
# TwoStepConnectedAccountFilter.install(collection, target_field: 'connected_account_id')
|
|
15
|
+
# end
|
|
16
|
+
# end
|
|
17
|
+
#
|
|
18
|
+
# Install at the datasource level: @agent.use(plugin, {}).
|
|
19
|
+
class TwoStepLinkPlugin < ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
20
|
+
ComputedDefinition = ForestAdminDatasourceCustomizer::Decorators::Computed::ComputedDefinition
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
attr_reader :config
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def self.link(owner:, filtered:, name:, foreign_key:)
|
|
27
|
+
@config = { owner: owner, filtered: filtered, name: name, fk: foreign_key }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# The virtual FK is nil per record: a reverse lookup would require
|
|
31
|
+
# scanning the pivot/intermediate collection. Only EQUAL/IN filters are
|
|
32
|
+
# meaningful, and those are rewritten by the source filter.
|
|
33
|
+
def self.virtual_fk
|
|
34
|
+
ComputedDefinition.new(
|
|
35
|
+
column_type: 'String',
|
|
36
|
+
dependencies: ['id'],
|
|
37
|
+
values: proc { |records, _ctx| records.map { nil } }
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def run(datasource_customizer, _collection_customizer = nil, _options = {})
|
|
42
|
+
Plugins::Helpers.require_datasource!(datasource_customizer, self.class)
|
|
43
|
+
|
|
44
|
+
cfg = self.class.config
|
|
45
|
+
plugin = self
|
|
46
|
+
datasource_customizer.customize_collection(cfg[:filtered]) do |c|
|
|
47
|
+
c.add_field(cfg[:fk], TwoStepLinkPlugin.virtual_fk)
|
|
48
|
+
plugin.install_source_filter(c)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
datasource_customizer.customize_collection(cfg[:owner]) do |c|
|
|
52
|
+
c.add_one_to_many_relation(cfg[:name], cfg[:filtered],
|
|
53
|
+
origin_key: cfg[:fk], origin_key_target: 'id')
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Installs the operator filter that rewrites the virtual FK predicate.
|
|
58
|
+
def install_source_filter(_collection)
|
|
59
|
+
raise NotImplementedError, "#{self.class} must implement #install_source_filter"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
data/lib/forest_admin_datasource_mambu_payments/plugins/relations/two_step_reconciliation_filter.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module Relations
|
|
4
|
+
# Two-step pre-resolution for `payment_order_id` / `incoming_payment_id`
|
|
5
|
+
# / ... virtual filters on Transaction (and other resources that link to
|
|
6
|
+
# payments via the Reconciliation pivot, not via a native FK).
|
|
7
|
+
# Resolves the payment ids to the set of transaction_ids through
|
|
8
|
+
# `Reconciliation.payment_id` + `Reconciliation.payment_type`, then
|
|
9
|
+
# rewrites the predicate against the host's real id field.
|
|
10
|
+
module TwoStepReconciliationFilter
|
|
11
|
+
Operators = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Operators
|
|
12
|
+
ConditionTreeLeaf = ForestAdminDatasourceToolkit::Components::Query::ConditionTree::Nodes::ConditionTreeLeaf
|
|
13
|
+
|
|
14
|
+
RECONCILIATION = 'MambuReconciliation'.freeze
|
|
15
|
+
|
|
16
|
+
def self.install(collection_customizer, fk_name:, payment_type:, target_field:)
|
|
17
|
+
PivotResolution::SUPPORTED_OPS.each do |operator|
|
|
18
|
+
collection_customizer.replace_field_operator(fk_name, operator) do |value, context|
|
|
19
|
+
payment_ids = PivotResolution.normalize(value, operator)
|
|
20
|
+
next PivotResolution.no_match(target_field) if payment_ids.empty?
|
|
21
|
+
|
|
22
|
+
tx_ids = PivotResolution.collect(
|
|
23
|
+
context, RECONCILIATION,
|
|
24
|
+
PivotResolution.and_branch(
|
|
25
|
+
ConditionTreeLeaf.new('payment_id', Operators::IN, payment_ids),
|
|
26
|
+
ConditionTreeLeaf.new('payment_type', Operators::EQUAL, payment_type)
|
|
27
|
+
),
|
|
28
|
+
'transaction_id'
|
|
29
|
+
)
|
|
30
|
+
next PivotResolution.no_match(target_field) if tx_ids.empty?
|
|
31
|
+
|
|
32
|
+
ConditionTreeLeaf.new(target_field, Operators::IN, tx_ids)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/forest_admin_datasource_mambu_payments/plugins/smart_actions/approve_payment_order.rb
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module SmartActions
|
|
4
|
+
# Approves a payment order in status `pending_approval`. The Numeral API
|
|
5
|
+
# rejects approval for orders in any other status, so per-id rescue keeps
|
|
6
|
+
# one bad id from aborting a bulk approval.
|
|
7
|
+
class ApprovePaymentOrder < ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
8
|
+
BaseAction = ForestAdminDatasourceCustomizer::Decorators::Action::BaseAction
|
|
9
|
+
ActionScope = ForestAdminDatasourceCustomizer::Decorators::Action::Types::ActionScope
|
|
10
|
+
|
|
11
|
+
NAMES = { single: 'Approve Mambu payment order',
|
|
12
|
+
bulk: 'Approve selected Mambu payment orders' }.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, 'ApprovePaymentOrder plugin requires :datasource' unless datasource
|
|
18
|
+
raise ArgumentError, 'ApprovePaymentOrder plugin requires :record_id_field' unless record_id_field
|
|
19
|
+
raise ArgumentError, 'ApprovePaymentOrder 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
|
+
next result_builder.error(message: "No Mambu payment order id found in '#{record_id_field}'.") if ids.empty?
|
|
36
|
+
|
|
37
|
+
succeeded, failed = Helpers.each_with_rescue(ids, 'approve_payment_order') do |id|
|
|
38
|
+
datasource.client.approve_payment_order(id)
|
|
39
|
+
end
|
|
40
|
+
finalize(result_builder, succeeded, failed)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def finalize(result_builder, succeeded, failed)
|
|
45
|
+
if succeeded.empty?
|
|
46
|
+
return result_builder.error(message: Messages.all_failed(failed, noun: 'payment order',
|
|
47
|
+
verb: 'approve'))
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
result_builder.success(message: Messages.success(succeeded, failed, noun: 'payment order',
|
|
51
|
+
verb_past: 'approved'))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
module ForestAdminDatasourceMambuPayments
|
|
2
|
+
module Plugins
|
|
3
|
+
module SmartActions
|
|
4
|
+
# Cancels a payment order. The optional `reason` is only used by Numeral
|
|
5
|
+
# for SEPA direct debit cancelations before settlement; for other payment
|
|
6
|
+
# types it is accepted and ignored.
|
|
7
|
+
class CancelPaymentOrder < ForestAdminDatasourceCustomizer::Plugins::Plugin
|
|
8
|
+
BaseAction = ForestAdminDatasourceCustomizer::Decorators::Action::BaseAction
|
|
9
|
+
ActionScope = ForestAdminDatasourceCustomizer::Decorators::Action::Types::ActionScope
|
|
10
|
+
FieldType = ForestAdminDatasourceCustomizer::Decorators::Action::Types::FieldType
|
|
11
|
+
|
|
12
|
+
NAMES = { single: 'Cancel Mambu payment order',
|
|
13
|
+
bulk: 'Cancel selected Mambu payment orders' }.freeze
|
|
14
|
+
|
|
15
|
+
def run(_datasource_customizer, collection_customizer = nil, options = {})
|
|
16
|
+
datasource = options[:datasource]
|
|
17
|
+
record_id_field = options[:record_id_field]
|
|
18
|
+
raise ArgumentError, 'CancelPaymentOrder plugin requires :datasource' unless datasource
|
|
19
|
+
raise ArgumentError, 'CancelPaymentOrder plugin requires :record_id_field' unless record_id_field
|
|
20
|
+
raise ArgumentError, 'CancelPaymentOrder plugin requires a collection' unless collection_customizer
|
|
21
|
+
|
|
22
|
+
Helpers.normalize_scopes(options[:scopes]).each do |scope_key|
|
|
23
|
+
collection_customizer.add_action(NAMES[scope_key], build_action(datasource, scope_key, record_id_field))
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def build_action(datasource, scope_key, record_id_field)
|
|
30
|
+
BaseAction.new(scope: Helpers::SCOPES[scope_key], form: form, &executor(datasource, record_id_field))
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def form
|
|
34
|
+
[{ type: FieldType::STRING, label: 'Reason',
|
|
35
|
+
description: 'Optional reason code (SEPA direct debit only).' }]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def executor(datasource, record_id_field)
|
|
39
|
+
lambda do |context, result_builder|
|
|
40
|
+
ids = Helpers.resolve_ids(context, record_id_field)
|
|
41
|
+
next result_builder.error(message: "No Mambu payment order id found in '#{record_id_field}'.") if ids.empty?
|
|
42
|
+
|
|
43
|
+
payload = {}
|
|
44
|
+
reason = context.form_values['Reason']
|
|
45
|
+
payload['reason'] = reason if Helpers.present?(reason)
|
|
46
|
+
|
|
47
|
+
succeeded, failed = Helpers.each_with_rescue(ids, 'cancel_payment_order') do |id|
|
|
48
|
+
datasource.client.cancel_payment_order(id, payload)
|
|
49
|
+
end
|
|
50
|
+
finalize(result_builder, succeeded, failed)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def finalize(result_builder, succeeded, failed)
|
|
55
|
+
if succeeded.empty?
|
|
56
|
+
return result_builder.error(message: Messages.all_failed(failed, noun: 'payment order',
|
|
57
|
+
verb: 'cancel'))
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
result_builder.success(message: Messages.success(succeeded, failed, noun: 'payment order',
|
|
61
|
+
verb_past: 'canceled'))
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|