From 618b631aeefb32404de7203022741d8bc6c4560b Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Thu, 25 Sep 2025 15:36:29 +0300 Subject: [PATCH 1/4] Makes batch limit rollback to respect operation flags In case of pre-processing repair it allows to rollback non-existent hold operations. --- apps/hellgate/src/hg_limiter.erl | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/hellgate/src/hg_limiter.erl b/apps/hellgate/src/hg_limiter.erl index 2ed16f12..38dd63b3 100644 --- a/apps/hellgate/src/hg_limiter.erl +++ b/apps/hellgate/src/hg_limiter.erl @@ -50,6 +50,10 @@ id = ShopID }). +%% Very specific errors to crutch around +-define(POSTING_PLAN_NOT_FOUND(ID), #base_InvalidRequest{errors = [<<"Posting plan not found: ", ID/binary>>]}). +-define(OPERATION_NOT_FOUND, #base_InvalidRequest{errors = [<<"OperationNotFound">>]}). + -spec get_turnover_limits(turnover_selector() | undefined) -> [turnover_limit()]. get_turnover_limits(undefined) -> []; @@ -279,7 +283,13 @@ rollback_payment_limits(TurnoverLimits, Invoice, Payment, Route, Iter, Flags) -> {LegacyTurnoverLimits, BatchTurnoverLimits} = split_turnover_limits_by_available_limiter_api(TurnoverLimits), ok = legacy_rollback_payment_limits(Context, LegacyTurnoverLimits, Invoice, Payment, Route, Iter, Flags), OperationIdSegments = make_route_operation_segments(Invoice, Payment, Route, Iter), - ok = batch_rollback_limits(Context, BatchTurnoverLimits, OperationIdSegments). + IgnoreError = lists:member(ignore_not_found, Flags) orelse lists:member(ignore_business_error, Flags), + try + ok = batch_rollback_limits(Context, BatchTurnoverLimits, OperationIdSegments) + catch + error:(?OPERATION_NOT_FOUND) when IgnoreError =:= true -> + ok + end. batch_rollback_limits(_Context, [], _OperationIdSegments) -> ok; @@ -342,9 +352,6 @@ process_changes(LimitChangesQueues, WithFun, Clock, Context, Flags) -> LimitChangesQueues ). -%% Very specific error to crutch around --define(POSTING_PLAN_NOT_FOUND(ID), #base_InvalidRequest{errors = [<<"Posting plan not found: ", ID/binary>>]}). - process_changes_try_wrap([LimitChange], WithFun, Clock, Context, Flags) -> IgnoreNotFound = lists:member(ignore_not_found, Flags), #limiter_LimitChange{change_id = ChangeID} = LimitChange, From d7f87864ca4806b91fc0ef80261c6982c6aa68fd Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Thu, 25 Sep 2025 15:44:49 +0300 Subject: [PATCH 2/4] Moves try/catch deeper into turnover-limits iteration function --- apps/hellgate/src/hg_limiter.erl | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/apps/hellgate/src/hg_limiter.erl b/apps/hellgate/src/hg_limiter.erl index 38dd63b3..7e401f62 100644 --- a/apps/hellgate/src/hg_limiter.erl +++ b/apps/hellgate/src/hg_limiter.erl @@ -283,20 +283,20 @@ rollback_payment_limits(TurnoverLimits, Invoice, Payment, Route, Iter, Flags) -> {LegacyTurnoverLimits, BatchTurnoverLimits} = split_turnover_limits_by_available_limiter_api(TurnoverLimits), ok = legacy_rollback_payment_limits(Context, LegacyTurnoverLimits, Invoice, Payment, Route, Iter, Flags), OperationIdSegments = make_route_operation_segments(Invoice, Payment, Route, Iter), + ok = batch_rollback_limits(Context, BatchTurnoverLimits, OperationIdSegments, Flags). + +batch_rollback_limits(_Context, [], _OperationIdSegments, _Flags) -> + ok; +batch_rollback_limits(Context, TurnoverLimits, OperationIdSegments, Flags) -> + {LimitRequest, _} = prepare_limit_request(TurnoverLimits, OperationIdSegments), IgnoreError = lists:member(ignore_not_found, Flags) orelse lists:member(ignore_business_error, Flags), try - ok = batch_rollback_limits(Context, BatchTurnoverLimits, OperationIdSegments) + ok = hg_limiter_client:rollback_batch(LimitRequest, Context) catch error:(?OPERATION_NOT_FOUND) when IgnoreError =:= true -> ok end. -batch_rollback_limits(_Context, [], _OperationIdSegments) -> - ok; -batch_rollback_limits(Context, TurnoverLimits, OperationIdSegments) -> - {LimitRequest, _} = prepare_limit_request(TurnoverLimits, OperationIdSegments), - hg_limiter_client:rollback_batch(LimitRequest, Context). - legacy_rollback_payment_limits(Context, TurnoverLimits, Invoice, Payment, Route, Iter, Flags) -> ChangeIDs = [ construct_payment_change_id(Route, Iter, Invoice, Payment), @@ -312,7 +312,7 @@ rollback_shop_limits(TurnoverLimits, Party, Shop, Invoice, Payment, Flags) -> {LegacyTurnoverLimits, BatchTurnoverLimits} = split_turnover_limits_by_available_limiter_api(TurnoverLimits), ok = legacy_rollback_shop_limits(Context, LegacyTurnoverLimits, Party, Shop, Invoice, Payment, Flags), OperationIdSegments = make_shop_operation_segments(Party, Shop, Invoice, Payment), - ok = batch_rollback_limits(Context, BatchTurnoverLimits, OperationIdSegments). + ok = batch_rollback_limits(Context, BatchTurnoverLimits, OperationIdSegments, Flags). legacy_rollback_shop_limits(Context, TurnoverLimits, Party, Shop, Invoice, Payment, Flags) -> ChangeIDs = [construct_shop_change_id(Party, Shop, Invoice, Payment)], From efdad03e9f79debe6f29f4381bbebeea6632d754 Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Thu, 25 Sep 2025 16:10:14 +0300 Subject: [PATCH 3/4] Fixes missing arg --- apps/hellgate/src/hg_limiter.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/hellgate/src/hg_limiter.erl b/apps/hellgate/src/hg_limiter.erl index 7e401f62..d0b8d2b6 100644 --- a/apps/hellgate/src/hg_limiter.erl +++ b/apps/hellgate/src/hg_limiter.erl @@ -325,7 +325,7 @@ rollback_refund_limits(TurnoverLimits, Invoice, Payment, Refund, Route) -> {LegacyTurnoverLimits, BatchTurnoverLimits} = split_turnover_limits_by_available_limiter_api(TurnoverLimits), ok = legacy_rollback_refund_limits(Context, LegacyTurnoverLimits, Invoice, Payment, Refund), OperationIdSegments = make_refund_operation_segments(Invoice, Payment, Refund), - ok = batch_rollback_limits(Context, BatchTurnoverLimits, OperationIdSegments). + ok = batch_rollback_limits(Context, BatchTurnoverLimits, OperationIdSegments, []). legacy_rollback_refund_limits(Context, TurnoverLimits, Invoice, Payment, Refund) -> ChangeIDs = [construct_refund_change_id(Invoice, Payment, Refund)], From 776f5dd5ca33386a44814234e60a300825a46dbf Mon Sep 17 00:00:00 2001 From: Aleksey Kashapov Date: Mon, 29 Sep 2025 16:34:36 +0300 Subject: [PATCH 4/4] Adds non-existent limit rollback testcase --- apps/hellgate/test/hg_invoice_tests_SUITE.erl | 63 +++++++++++++++++++ compose.yaml | 2 +- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/apps/hellgate/test/hg_invoice_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_tests_SUITE.erl index db5ffbc0..79f952a0 100644 --- a/apps/hellgate/test/hg_invoice_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_tests_SUITE.erl @@ -189,6 +189,7 @@ -export([repair_fulfill_session_on_captured_succeeded/1]). -export([repair_fail_routing_succeeded/1]). +-export([repair_fail_routing_not_existent_operation/1]). -export([repair_fail_cash_flow_building_succeeded/1]). -export([consistent_account_balances/1]). @@ -489,6 +490,7 @@ groups() -> ]}, {repair_preproc_w_limits, [], [ repair_fail_routing_succeeded, + repair_fail_routing_not_existent_operation, repair_fail_cash_flow_building_succeeded ]}, {allocation, [parallel], [ @@ -728,6 +730,8 @@ init_per_testcase(Name = repair_fail_routing_succeeded, C) -> fun override_check_limits/5 ), init_per_testcase_(Name, C); +init_per_testcase(Name = repair_fail_routing_not_existent_operation, C) -> + init_per_testcase_(Name, override_terms_limit_reference(?prv(5), C)); init_per_testcase(Name = repair_fail_cash_flow_building_succeeded, C) -> meck:expect( hg_cashflow_utils, @@ -760,6 +764,27 @@ override_domain_fixture(Fixture, C) -> override_domain_fixture(Fixture, Name, C) -> init_per_testcase_(Name, override_domain_fixture(Fixture, C)). +override_terms_limit_reference(ProviderRef, C) -> + override_domain_fixture( + fun(Revision, _C) -> + [ + change_provider_payments_provision_terms(ProviderRef, Revision, fun(PaymentsProvisionTerms) -> + PaymentsProvisionTerms#domain_PaymentsProvisionTerms{ + turnover_limits = + {value, [ + #domain_TurnoverLimit{ + id = <<"NOT_EXISTENT_LIMIT_ID">>, + upper_boundary = ?LIMIT_UPPER_BOUNDARY, + domain_revision = Revision + } + ]} + } + end) + ] + end, + C + ). + init_per_testcase_(Name, C) -> ApiClient = hg_ct_helper:create_client(cfg(root_url, C)), Client = hg_client_invoicing:start_link(ApiClient), @@ -778,6 +803,8 @@ trace_testcase(Name, C) -> end_per_testcase(repair_fail_routing_succeeded, C) -> meck:unload(hg_limiter), end_per_testcase(default, C); +end_per_testcase(repair_fail_routing_not_existent_operation, C) -> + end_per_testcase(default, C); end_per_testcase(repair_fail_cash_flow_building_succeeded, C) -> meck:unload(hg_cashflow_utils), end_per_testcase(default, C); @@ -5798,6 +5825,42 @@ repair_fail_routing_succeeded(C) -> InvoiceID, fail_pre_processing, Client ). +-spec repair_fail_routing_not_existent_operation(config()) -> test_return(). +repair_fail_routing_not_existent_operation(C) -> + RootUrl = cfg(root_url, C), + Client = hg_client_invoicing:start_link(hg_ct_helper:create_client(RootUrl)), + PartyClient = cfg(party_client, C), + #{party_id := PartyID} = cfg(limits, C), + ShopID = hg_ct_helper:create_shop(PartyID, ?cat(8), <<"RUB">>, ?tmpl(1), ?pinst(1), PartyClient), + + %% Invoice + InvoiceParams = make_invoice_params(PartyID, ShopID, <<"rubberduck">>, make_due_date(10), make_cash(10000)), + InvoiceID = create_invoice(InvoiceParams, Client), + ?invoice_created(?invoice_w_status(?invoice_unpaid())) = next_change(InvoiceID, Client), + + %% Payment + PaymentParams = make_payment_params(?pmt_sys(<<"visa-ref">>)), + ?payment_state(?payment(PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), + [ + ?payment_ev(PaymentID, ?payment_started(?payment_w_status(?pending()))), + ?payment_ev(PaymentID, ?shop_limit_initiated()), + ?payment_ev(PaymentID, ?shop_limit_applied()), + ?payment_ev(PaymentID, ?risk_score_changed(_)) + ] = next_changes(InvoiceID, 4, Client), + %% routing broken + timeout = next_change(InvoiceID, 2000, Client), + + %% Repair with rollback limits + ok = repair_invoice_with_scenario(InvoiceID, fail_pre_processing, Client), + + %% Check final status + ?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, _Failure}))) = next_change(InvoiceID, Client), + + %% Check duplicate repair + {exception, {base_InvalidRequest, [<<"No need to repair">>]}} = repair_invoice_with_scenario( + InvoiceID, fail_pre_processing, Client + ). + %% fail cash_flow_building before accounting hold -spec repair_fail_cash_flow_building_succeeded(config()) -> test_return(). repair_fail_cash_flow_building_succeeded(C) -> diff --git a/compose.yaml b/compose.yaml index c407ce86..0c97a35a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -63,7 +63,7 @@ services: retries: 10 limiter: - image: ghcr.io/valitydev/limiter:sha-2271094 + image: ghcr.io/valitydev/limiter:sha-36eb612 command: /opt/limiter/bin/limiter foreground depends_on: machinegun: