From 63bbd5d60e111bd66378ffa84d087861590b9723 Mon Sep 17 00:00:00 2001 From: ttt161 Date: Thu, 9 Jul 2026 11:10:13 +0300 Subject: [PATCH 01/12] change withdrawal body event --- apps/ff_server/src/ff_withdrawal_codec.erl | 34 +++++++++++--- .../src/ff_withdrawal_session_codec.erl | 14 ++++++ .../ff_transfer/src/ff_adapter_withdrawal.erl | 6 ++- .../src/ff_adapter_withdrawal_codec.erl | 12 +++-- apps/ff_transfer/src/ff_withdrawal.erl | 22 ++++++++-- .../ff_transfer/src/ff_withdrawal_session.erl | 44 ++++++++++++++++--- rebar.config | 4 +- rebar.lock | 4 +- 8 files changed, 116 insertions(+), 24 deletions(-) diff --git a/apps/ff_server/src/ff_withdrawal_codec.erl b/apps/ff_server/src/ff_withdrawal_codec.erl index f0eba474..63b6b476 100644 --- a/apps/ff_server/src/ff_withdrawal_codec.erl +++ b/apps/ff_server/src/ff_withdrawal_codec.erl @@ -173,6 +173,20 @@ marshal(session_event, {finished, Result}) -> {finished, #wthd_SessionFinished{result = marshal(session_result, Result)}}; marshal(session_result, success) -> {succeeded, #wthd_SessionSucceeded{}}; +marshal(session_result, {success, SuccessInfo}) when is_list(SuccessInfo) -> + SessionData = lists:foldl( + fun + ({_, undefined}, Acc) -> + Acc; + ({trx_info, TrxInfo}, Acc) -> + Acc#wthd_SessionSucceeded{trx_info = marshal(transaction_info, TrxInfo)}; + ({changed_body, ChangedBody}, Acc) -> + Acc#wthd_SessionSucceeded{changed_body = marshal(cash, ChangedBody)} + end, + #wthd_SessionSucceeded{}, + SuccessInfo + ), + {succeeded, SessionData}; marshal(session_result, {success, TransactionInfo}) -> %% for backward compatibility with events stored in DB - take TransactionInfo here. %% @see ff_adapter_withdrawal:rebind_transaction_info/1 @@ -298,12 +312,22 @@ unmarshal(session_event, #wthd_SessionChange{id = ID, payload = {started, #wthd_ unmarshal(session_event, #wthd_SessionChange{id = ID, payload = {finished, Finished}}) -> #wthd_SessionFinished{result = Result} = Finished, {session_finished, {unmarshal(id, ID), unmarshal(session_result, Result)}}; -unmarshal(session_result, {succeeded, #wthd_SessionSucceeded{trx_info = undefined}}) -> +unmarshal(session_result, {succeeded, #wthd_SessionSucceeded{trx_info = undefined, changed_body = undefined}}) -> success; -unmarshal(session_result, {succeeded, #wthd_SessionSucceeded{trx_info = TransactionInfo}}) -> - %% for backward compatibility with events stored in DB - take TransactionInfo here. - %% @see ff_adapter_withdrawal:rebind_transaction_info/1 - {success, unmarshal(transaction_info, TransactionInfo)}; +unmarshal( + session_result, + {succeeded, #wthd_SessionSucceeded{trx_info = TransactionInfo, changed_body = ChangedBody}} +) -> + SessionData = maps:to_list( + genlib_map:compact(#{ + trx_info => maybe_unmarshal(transaction_info, TransactionInfo), + changed_body => maybe_unmarshal(cash, ChangedBody) + }) + ), + {success, SessionData}; +%% for backward compatibility with events stored in DB - take TransactionInfo here. +%% @see ff_adapter_withdrawal:rebind_transaction_info/1 +%{success, unmarshal(transaction_info, TransactionInfo)}; unmarshal(session_result, {failed, #wthd_SessionFailed{failure = Failure}}) -> {failed, ff_codec:unmarshal(failure, Failure)}; unmarshal(transaction_info, TrxInfo) -> diff --git a/apps/ff_server/src/ff_withdrawal_session_codec.erl b/apps/ff_server/src/ff_withdrawal_session_codec.erl index a7729bcf..82cbdcc0 100644 --- a/apps/ff_server/src/ff_withdrawal_session_codec.erl +++ b/apps/ff_server/src/ff_withdrawal_session_codec.erl @@ -51,6 +51,15 @@ marshal(change, {finished, SessionResult}) -> {finished, marshal(session_result, SessionResult)}; marshal(change, {callback, CallbackChange}) -> {callback, marshal(callback_change, CallbackChange)}; +marshal(change, {changed_body, BodyChange}) -> + #{ + old_body := OldBody, + new_body := NewBody + } = BodyChange, + {changed_body, #wthd_session_BodyChange{ + old_body = marshal(cash, OldBody), + new_body = marshal(cash, NewBody) + }}; marshal(session, Session) -> #{ id := SessionID, @@ -184,6 +193,11 @@ unmarshal(change, {callback, #wthd_session_CallbackChange{tag = Tag, payload = P tag => unmarshal(string, Tag), payload => unmarshal(callback_event, Payload) }}; +unmarshal(change, {changed_body, #wthd_session_BodyChange{old_body = OldBody, new_body = NewBody}}) -> + {changed_body, #{ + old_body => unmarshal(cash, OldBody), + new_body => unmarshal(cash, NewBody) + }}; unmarshal(session, #wthd_session_Session{ id = SessionID, status = SessionStatus, diff --git a/apps/ff_transfer/src/ff_adapter_withdrawal.erl b/apps/ff_transfer/src/ff_adapter_withdrawal.erl index 702c2489..ade89bcb 100644 --- a/apps/ff_transfer/src/ff_adapter_withdrawal.erl +++ b/apps/ff_transfer/src/ff_adapter_withdrawal.erl @@ -82,14 +82,16 @@ -type process_result() :: #{ intent := intent(), next_state => adapter_state(), - transaction_info => transaction_info() + transaction_info => transaction_info(), + changed_body => cash() }. -type handle_callback_result() :: #{ intent := intent(), response := callback_response(), next_state => adapter_state(), - transaction_info => transaction_info() + transaction_info => transaction_info(), + changed_body => cash() }. -type callback() :: ff_withdrawal_callback:process_params(). diff --git a/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl b/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl index ae9c6628..9d61d5df 100644 --- a/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl +++ b/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl @@ -288,24 +288,28 @@ unmarshal(callback, #wthd_provider_Callback{ unmarshal(process_result, #wthd_provider_ProcessResult{ intent = Intent, next_state = NextState, - trx = TransactionInfo + trx = TransactionInfo, + changed_body = ChangedBody }) -> genlib_map:compact(#{ intent => unmarshal(intent, Intent), next_state => maybe_unmarshal(adapter_state, NextState), - transaction_info => maybe_unmarshal(transaction_info, TransactionInfo) + transaction_info => maybe_unmarshal(transaction_info, TransactionInfo), + changed_body => maybe_unmarshal(body, ChangedBody) }); unmarshal(callback_result, #wthd_provider_CallbackResult{ intent = Intent, next_state = NextState, response = Response, - trx = TransactionInfo + trx = TransactionInfo, + changed_body = ChangedBody }) -> genlib_map:compact(#{ intent => unmarshal(intent, Intent), response => unmarshal(callback_response, Response), next_state => maybe_unmarshal(adapter_state, NextState), - transaction_info => maybe_unmarshal(transaction_info, TransactionInfo) + transaction_info => maybe_unmarshal(transaction_info, TransactionInfo), + changed_body => maybe_unmarshal(body, ChangedBody) }); unmarshal(callback_response, #wthd_provider_CallbackResponse{payload = Payload}) -> #{payload => Payload}; diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 2bcfcb0b..309b79bf 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -24,7 +24,8 @@ metadata => metadata(), external_id => id(), validation => withdrawal_validation(), - contact_info => contact_info() + contact_info => contact_info(), + changed_body => body() }. -opaque withdrawal() :: #{ @@ -689,6 +690,7 @@ deduce_activity(Withdrawal) -> p_transfer => p_transfer_status(Withdrawal), validation => withdrawal_validation_status(Withdrawal), session => get_current_session_status(Withdrawal), + %% TODO changed_body status => status(Withdrawal), limit_check => limit_check_processing_status(Withdrawal), active_adjustment => ff_adjustment_utils:is_active(adjustments_index(Withdrawal)) @@ -1835,12 +1837,16 @@ apply_event_({p_transfer, Ev}, T) -> Tr = ff_postings_transfer:apply_event(Ev, p_transfer(T)), R = ff_withdrawal_route_attempt_utils:update_current_p_transfer(Tr, attempts(T)), update_attempts(R, T); -apply_event_({session_started, SessionID}, T) -> +apply_event_({session_started, SessionID}, T0) -> + %% clear changed_body from previous session + T = maps:without([changed_body], T0), Session = #{id => SessionID}, Attempts = attempts(T), R = ff_withdrawal_route_attempt_utils:update_current_session(Session, Attempts), update_attempts(R, T); -apply_event_({session_finished, {SessionID, Result}}, T) -> +apply_event_({session_finished, {SessionID, Result}}, T0) -> + %% maybe update changed_body + T = set_changed_body(Result, T0), Attempts = attempts(T), Session = ff_withdrawal_route_attempt_utils:get_current_session(Attempts), SessionID = maps:get(id, Session), @@ -1861,6 +1867,16 @@ apply_event_({validation, {Part, ValidationResult}}, T) -> apply_event_({adjustment, _Ev} = Event, T) -> apply_adjustment_event(Event, T). +set_changed_body({success, SuccessInfo}, WithdrawalState) when is_list(SuccessInfo) -> + case lists:keyfind(changed_body, 1, SuccessInfo) of + {_, ChangedBody} -> + WithdrawalState#{changed_body => ChangedBody}; + _ -> + WithdrawalState + end; +set_changed_body(_, WithdrawalState) -> + WithdrawalState. + -spec make_state(withdrawal()) -> withdrawal_state(). make_state(#{route := Route} = T) -> Attempts = ff_withdrawal_route_attempt_utils:new(), diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index 035e4380..0bd4bde9 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -44,7 +44,8 @@ callbacks => callbacks_index(), result => session_result(), % For validate outstanding TransactionsInfo - transaction_info => transaction_info() + transaction_info => transaction_info(), + changed_body => body() }. -type session() :: #{ @@ -56,10 +57,21 @@ }. -type transaction_info() :: ff_adapter_withdrawal:transaction_info(). --type session_result() :: success | {success, transaction_info()} | {failed, ff_adapter_withdrawal:failure()}. +-type body() :: ff_accounting:body(). +-type session_result() :: + success + | {success, transaction_info()} + | {success, success_info()} + | {failed, ff_adapter_withdrawal:failure()}. -type status() :: active | {finished, success | {failed, ff_adapter_withdrawal:failure()}}. -type party_id() :: ff_party:id(). +-type success_info() :: [success_info_unit()]. + +-type success_info_unit() :: + {trx_info, transaction_info()} + | {changed_body, body()}. + -type event() :: {created, session()} | {next_state, ff_adapter:state()} @@ -186,12 +198,16 @@ apply_event_({next_state, AdapterState}, Session) -> Session#{adapter_state => AdapterState}; apply_event_({transaction_bound, TransactionInfo}, Session) -> Session#{transaction_info => TransactionInfo}; +apply_event_({changed_body, #{new_body := NewBody}}, Session) -> + Session#{changed_body => NewBody}; apply_event_({finished, success = Result}, Session) -> - Session#{status => {finished, success}, result => Result}; + Session1 = Session#{status => {finished, success}}, + set_session_result_(Result, Session1); apply_event_({finished, {success, TransactionInfo} = Result}, Session) -> %% for backward compatibility with events stored in DB - take TransactionInfo here. %% @see ff_adapter_withdrawal:rebind_transaction_info/1 - Session#{status => {finished, success}, result => Result, transaction_info => TransactionInfo}; + Session1 = Session#{status => {finished, success}, transaction_info => TransactionInfo}, + set_session_result_(Result, Session1); apply_event_({finished, {failed, _} = Result} = Status, Session) -> Session#{status => Status, result => Result}; apply_event_({callback, _Ev} = WrappedEvent, Session) -> @@ -199,6 +215,15 @@ apply_event_({callback, _Ev} = WrappedEvent, Session) -> Callbacks1 = ff_withdrawal_callback_utils:apply_event(WrappedEvent, Callbacks0), set_callbacks_index(Callbacks1, Session). +set_session_result_(success, #{changed_body := ChangedBody} = Session) -> + Session#{result => {success, [{changed_body, ChangedBody}]}}; +set_session_result_(success = Result, Session) -> + Session#{result => Result}; +set_session_result_({success, TrxInfo}, #{changed_body := ChangedBody} = Session) -> + Session#{result => {success, [{changed_body, ChangedBody}, {trx_info, TrxInfo}]}}; +set_session_result_({success, _TrxInfo} = Result, Session) -> + Session#{result => Result}. + -spec process_session(session_state()) -> process_result(). process_session(#{status := {finished, _}, id := ID, result := Result, withdrawal := Withdrawal}) -> % Session has finished, it should notify the withdrawal machine about the fact @@ -231,7 +256,8 @@ process_session(#{status := active, withdrawal := Withdrawal, route := Route} = #{intent := Intent} = ProcessResult, Events0 = process_next_state(ProcessResult, [], ASt), Events1 = process_transaction_info(ProcessResult, Events0, SessionState), - process_adapter_intent(Intent, SessionState, Events1). + Events2 = process_changed_body(ProcessResult, Events1, Withdrawal), + process_adapter_intent(Intent, SessionState, Events2). process_transaction_info(#{transaction_info := TrxInfo}, Events, SessionState) -> ok = assert_transaction_info(TrxInfo, transaction_info(SessionState)), @@ -239,6 +265,11 @@ process_transaction_info(#{transaction_info := TrxInfo}, Events, SessionState) - process_transaction_info(_, Events, _Session) -> Events. +process_changed_body(#{changed_body := NewBody}, Events, #{cash := OldBody} = _Withdrawal) -> + Events ++ [{changed_body, #{old_body => OldBody, new_body => NewBody}}]; +process_changed_body(_Result, Events, _Withdrawal) -> + Events. + %% Only one static TransactionInfo within one session assert_transaction_info(_NewTrxInfo, undefined) -> @@ -291,7 +322,8 @@ do_process_callback(CallbackParams, Callback, Session) -> Events0 = ff_withdrawal_callback_utils:process_response(Response, Callback), Events1 = process_next_state(HandleCallbackResult, Events0, AdapterState), Events2 = process_transaction_info(HandleCallbackResult, Events1, Session), - {ok, {Response, process_adapter_intent(Intent, Session, Events2)}}. + Events3 = process_changed_body(HandleCallbackResult, Events2, Withdrawal), + {ok, {Response, process_adapter_intent(Intent, Session, Events3)}}. make_session_finish_params(Session) -> {_Adapter, AdapterOpts} = get_adapter_with_opts(route(Session)), diff --git a/rebar.config b/rebar.config index 8c367d87..6f82ff7b 100644 --- a/rebar.config +++ b/rebar.config @@ -36,7 +36,7 @@ {woody, {git, "https://github.com/valitydev/woody_erlang.git", {tag, "v1.1.2"}}}, {scoper, {git, "https://github.com/valitydev/scoper.git", {tag, "v1.1.0"}}}, {thrift, {git, "https://github.com/valitydev/thrift_erlang.git", {tag, "v1.0.0"}}}, - {damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.38"}}}, + {damsel, {git, "https://github.com/valitydev/damsel.git", {branch, "ft/change-withdrawal-amount"}}}, {exrates_proto, {git, "https://github.com/valitydev/exrates-proto.git", {branch, "master"}}}, {payproc_errors, {git, "https://github.com/valitydev/payproc-errors-erlang.git", {branch, "master"}}}, {mg_proto, {git, "https://github.com/valitydev/machinegun-proto.git", {branch, "master"}}}, @@ -49,7 +49,7 @@ {herd, {git, "https://github.com/wgnet/herd.git", {tag, "1.3.4"}}}, {progressor, {git, "https://github.com/valitydev/progressor.git", {tag, "v1.0.25"}}}, {machinery, {git, "https://github.com/valitydev/machinery-erlang.git", {tag, "v1.1.22"}}}, - {fistful_proto, {git, "https://github.com/valitydev/fistful-proto.git", {tag, "v2.0.2"}}}, + {fistful_proto, {git, "https://github.com/valitydev/fistful-proto.git", {branch, "ft/change-withdrawal-amount"}}}, {binbase_proto, {git, "https://github.com/valitydev/binbase-proto.git", {branch, "master"}}}, {validator_personal_data_proto, {git, "https://github.com/valitydev/validator-personal-data-proto.git", {branch, "master"}}}, diff --git a/rebar.lock b/rebar.lock index 0a6e64cf..d8d00d03 100644 --- a/rebar.lock +++ b/rebar.lock @@ -31,7 +31,7 @@ {<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2}, {<<"damsel">>, {git,"https://github.com/valitydev/damsel.git", - {ref,"c529750144dea43ada113436762ee1e340f5503d"}}, + {ref,"ba6dc2b3eb6ba3b3cdf33ddb1432629d0b60544c"}}, 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt-client.git", @@ -69,7 +69,7 @@ 0}, {<<"fistful_proto">>, {git,"https://github.com/valitydev/fistful-proto.git", - {ref,"f7ce08e529188f6786caffecc10978c2b28603f6"}}, + {ref,"a9d268b3e14008a689b71362361b021f996103ae"}}, 0}, {<<"genlib">>, {git,"https://github.com/valitydev/genlib.git", From 42f6c4e00d5acd4297c0dde88db6790f4d2c3b43 Mon Sep 17 00:00:00 2001 From: ttt161 Date: Tue, 14 Jul 2026 06:37:50 +0300 Subject: [PATCH 02/12] add support for change withdrawal amount --- apps/ff_server/src/ff_withdrawal_codec.erl | 47 ++++----- .../src/ff_withdrawal_session_codec.erl | 8 +- .../ff_transfer/src/ff_adapter_withdrawal.erl | 4 +- .../src/ff_adapter_withdrawal_codec.erl | 8 +- apps/ff_transfer/src/ff_withdrawal.erl | 99 ++++++++++++++----- .../ff_transfer/src/ff_withdrawal_machine.erl | 8 +- .../ff_transfer/src/ff_withdrawal_session.erl | 56 +++++------ rebar.lock | 4 +- 8 files changed, 127 insertions(+), 107 deletions(-) diff --git a/apps/ff_server/src/ff_withdrawal_codec.erl b/apps/ff_server/src/ff_withdrawal_codec.erl index 63b6b476..2e0f4564 100644 --- a/apps/ff_server/src/ff_withdrawal_codec.erl +++ b/apps/ff_server/src/ff_withdrawal_codec.erl @@ -87,7 +87,8 @@ marshal_withdrawal_state(WithdrawalState, Context) -> metadata = marshal(ctx, ff_withdrawal:metadata(WithdrawalState)), quote = maybe_marshal(quote_state, ff_withdrawal:quote(WithdrawalState)), withdrawal_validation = maybe_marshal(withdrawal_validation, ff_withdrawal:validation(WithdrawalState)), - contact_info = maybe_marshal(contact_info, ff_withdrawal:contact_info(WithdrawalState)) + contact_info = maybe_marshal(contact_info, ff_withdrawal:contact_info(WithdrawalState)), + new_body = maybe_marshal(cash, ff_withdrawal:new_body(WithdrawalState)) }. -spec marshal_event(ff_withdrawal_machine:event()) -> fistful_wthd_thrift:'Event'(). @@ -129,6 +130,11 @@ marshal(change, {adjustment, #{id := ID, payload := Payload}}) -> }}; marshal(change, {validation, {Part, ValidationResult}}) when Part =:= sender; Part =:= receiver -> {validation, {Part, marshal(validation_result, ValidationResult)}}; +marshal(change, {body_changed, #{old_body := OldBody, new_body := NewBody}}) -> + {body_changed, #wthd_BodyChange{ + old_body = marshal(cash, OldBody), + new_body = marshal(cash, NewBody) + }}; marshal(validation_result, {personal, #{validation_id := ValidationID, token := Token, validation_status := Status}}) -> { personal, @@ -173,20 +179,6 @@ marshal(session_event, {finished, Result}) -> {finished, #wthd_SessionFinished{result = marshal(session_result, Result)}}; marshal(session_result, success) -> {succeeded, #wthd_SessionSucceeded{}}; -marshal(session_result, {success, SuccessInfo}) when is_list(SuccessInfo) -> - SessionData = lists:foldl( - fun - ({_, undefined}, Acc) -> - Acc; - ({trx_info, TrxInfo}, Acc) -> - Acc#wthd_SessionSucceeded{trx_info = marshal(transaction_info, TrxInfo)}; - ({changed_body, ChangedBody}, Acc) -> - Acc#wthd_SessionSucceeded{changed_body = marshal(cash, ChangedBody)} - end, - #wthd_SessionSucceeded{}, - SuccessInfo - ), - {succeeded, SessionData}; marshal(session_result, {success, TransactionInfo}) -> %% for backward compatibility with events stored in DB - take TransactionInfo here. %% @see ff_adapter_withdrawal:rebind_transaction_info/1 @@ -269,6 +261,11 @@ unmarshal(change, {adjustment, Change}) -> }}; unmarshal(change, {validation, {Part, ValidationResult}}) when Part =:= sender; Part =:= receiver -> {validation, {Part, unmarshal(validation_result, ValidationResult)}}; +unmarshal(change, {body_changed, #wthd_BodyChange{old_body = OldBody, new_body = NewBody}}) -> + {body_changed, #{ + old_body => unmarshal(cash, OldBody), + new_body => unmarshal(cash, NewBody) + }}; unmarshal(validation_result, {personal, Validation}) -> {personal, #{ validation_id => unmarshal(id, Validation#wthd_PersonalDataValidationResult.validation_id), @@ -312,22 +309,12 @@ unmarshal(session_event, #wthd_SessionChange{id = ID, payload = {started, #wthd_ unmarshal(session_event, #wthd_SessionChange{id = ID, payload = {finished, Finished}}) -> #wthd_SessionFinished{result = Result} = Finished, {session_finished, {unmarshal(id, ID), unmarshal(session_result, Result)}}; -unmarshal(session_result, {succeeded, #wthd_SessionSucceeded{trx_info = undefined, changed_body = undefined}}) -> +unmarshal(session_result, {succeeded, #wthd_SessionSucceeded{trx_info = undefined}}) -> success; -unmarshal( - session_result, - {succeeded, #wthd_SessionSucceeded{trx_info = TransactionInfo, changed_body = ChangedBody}} -) -> - SessionData = maps:to_list( - genlib_map:compact(#{ - trx_info => maybe_unmarshal(transaction_info, TransactionInfo), - changed_body => maybe_unmarshal(cash, ChangedBody) - }) - ), - {success, SessionData}; -%% for backward compatibility with events stored in DB - take TransactionInfo here. -%% @see ff_adapter_withdrawal:rebind_transaction_info/1 -%{success, unmarshal(transaction_info, TransactionInfo)}; +unmarshal(session_result, {succeeded, #wthd_SessionSucceeded{trx_info = TransactionInfo}}) -> + %% for backward compatibility with events stored in DB - take TransactionInfo here. + %% @see ff_adapter_withdrawal:rebind_transaction_info/1 + {success, unmarshal(transaction_info, TransactionInfo)}; unmarshal(session_result, {failed, #wthd_SessionFailed{failure = Failure}}) -> {failed, ff_codec:unmarshal(failure, Failure)}; unmarshal(transaction_info, TrxInfo) -> diff --git a/apps/ff_server/src/ff_withdrawal_session_codec.erl b/apps/ff_server/src/ff_withdrawal_session_codec.erl index 82cbdcc0..f6bee38b 100644 --- a/apps/ff_server/src/ff_withdrawal_session_codec.erl +++ b/apps/ff_server/src/ff_withdrawal_session_codec.erl @@ -51,12 +51,12 @@ marshal(change, {finished, SessionResult}) -> {finished, marshal(session_result, SessionResult)}; marshal(change, {callback, CallbackChange}) -> {callback, marshal(callback_change, CallbackChange)}; -marshal(change, {changed_body, BodyChange}) -> +marshal(change, {body_changed, BodyChange}) -> #{ old_body := OldBody, new_body := NewBody } = BodyChange, - {changed_body, #wthd_session_BodyChange{ + {body_changed, #wthd_session_BodyChange{ old_body = marshal(cash, OldBody), new_body = marshal(cash, NewBody) }}; @@ -193,8 +193,8 @@ unmarshal(change, {callback, #wthd_session_CallbackChange{tag = Tag, payload = P tag => unmarshal(string, Tag), payload => unmarshal(callback_event, Payload) }}; -unmarshal(change, {changed_body, #wthd_session_BodyChange{old_body = OldBody, new_body = NewBody}}) -> - {changed_body, #{ +unmarshal(change, {body_changed, #wthd_session_BodyChange{old_body = OldBody, new_body = NewBody}}) -> + {body_changed, #{ old_body => unmarshal(cash, OldBody), new_body => unmarshal(cash, NewBody) }}; diff --git a/apps/ff_transfer/src/ff_adapter_withdrawal.erl b/apps/ff_transfer/src/ff_adapter_withdrawal.erl index ade89bcb..8a4e12ae 100644 --- a/apps/ff_transfer/src/ff_adapter_withdrawal.erl +++ b/apps/ff_transfer/src/ff_adapter_withdrawal.erl @@ -83,7 +83,7 @@ intent := intent(), next_state => adapter_state(), transaction_info => transaction_info(), - changed_body => cash() + new_body => cash() }. -type handle_callback_result() :: #{ @@ -91,7 +91,7 @@ response := callback_response(), next_state => adapter_state(), transaction_info => transaction_info(), - changed_body => cash() + new_body => cash() }. -type callback() :: ff_withdrawal_callback:process_params(). diff --git a/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl b/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl index 9d61d5df..e02b32bd 100644 --- a/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl +++ b/apps/ff_transfer/src/ff_adapter_withdrawal_codec.erl @@ -289,27 +289,27 @@ unmarshal(process_result, #wthd_provider_ProcessResult{ intent = Intent, next_state = NextState, trx = TransactionInfo, - changed_body = ChangedBody + new_body = NewBody }) -> genlib_map:compact(#{ intent => unmarshal(intent, Intent), next_state => maybe_unmarshal(adapter_state, NextState), transaction_info => maybe_unmarshal(transaction_info, TransactionInfo), - changed_body => maybe_unmarshal(body, ChangedBody) + new_body => maybe_unmarshal(body, NewBody) }); unmarshal(callback_result, #wthd_provider_CallbackResult{ intent = Intent, next_state = NextState, response = Response, trx = TransactionInfo, - changed_body = ChangedBody + new_body = NewBody }) -> genlib_map:compact(#{ intent => unmarshal(intent, Intent), response => unmarshal(callback_response, Response), next_state => maybe_unmarshal(adapter_state, NextState), transaction_info => maybe_unmarshal(transaction_info, TransactionInfo), - changed_body => maybe_unmarshal(body, ChangedBody) + new_body => maybe_unmarshal(body, NewBody) }); unmarshal(callback_response, #wthd_provider_CallbackResponse{payload = Payload}) -> #{payload => Payload}; diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 309b79bf..6db87e52 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -25,7 +25,8 @@ external_id => id(), validation => withdrawal_validation(), contact_info => contact_info(), - changed_body => body() + new_body => body(), + p_transfer_rebuild_needed => boolean() }. -opaque withdrawal() :: #{ @@ -218,6 +219,7 @@ -export([id/1]). -export([party_id/1]). -export([body/1]). +-export([new_body/1]). -export([status/1]). -export([route/1]). -export([attempts/1]). @@ -274,6 +276,7 @@ -type cash_range() :: ff_range:range(cash()). -type failure() :: ff_failure:failure(). -type session_result() :: ff_withdrawal_session:session_result(). +-type session_data() :: ff_withdrawal_session:session_data(). -type adjustment() :: ff_adjustment:adjustment(). -type adjustment_id() :: ff_adjustment:id(). -type adjustments_index() :: ff_adjustment_utils:index(). @@ -377,6 +380,10 @@ status(T) -> route(T) -> maps:get(route, T, undefined). +-spec new_body(withdrawal_state()) -> body() | undefined. +new_body(T) -> + maps:get(new_body, T, undefined). + -spec attempts(withdrawal_state()) -> attempts(). attempts(#{attempts := Attempts}) -> Attempts; @@ -571,9 +578,9 @@ format_activity(Activity) -> %% --spec finalize_session(session_id(), session_result(), withdrawal_state()) -> +-spec finalize_session(session_id(), session_data(), withdrawal_state()) -> {ok, process_result()} | {error, session_not_found | old_session | result_mismatch}. -finalize_session(SessionID, SessionResult, Withdrawal) -> +finalize_session(SessionID, #{result := SessionResult} = SessionData, Withdrawal) -> case get_session_by_id(SessionID, Withdrawal) of #{id := SessionID, result := SessionResult} -> {ok, {idle, []}}; @@ -581,7 +588,7 @@ finalize_session(SessionID, SessionResult, Withdrawal) -> logger:warning("Session result mismatch - current: ~p, new: ~p", [OtherSessionResult, SessionResult]), {error, result_mismatch}; #{id := SessionID} -> - try_finish_session(SessionID, SessionResult, Withdrawal); + try_finish_session(SessionID, SessionData, Withdrawal); undefined -> {error, session_not_found} end. @@ -594,12 +601,16 @@ get_session_by_id(SessionID, Withdrawal) -> [] -> undefined end. --spec try_finish_session(session_id(), session_result(), withdrawal_state()) -> +-spec try_finish_session(session_id(), session_data(), withdrawal_state()) -> {ok, process_result()} | {error, old_session}. -try_finish_session(SessionID, SessionResult, Withdrawal) -> +try_finish_session(SessionID, #{result := SessionResult} = SessionData, Withdrawal) -> case is_current_session(SessionID, Withdrawal) of true -> - {ok, {timeout, [{session_finished, {SessionID, SessionResult}}]}}; + Events = + [ + {session_finished, {SessionID, SessionResult}} + ] ++ body_changed_events(SessionData, Withdrawal), + {ok, {timeout, Events}}; false -> {error, old_session} end. @@ -615,6 +626,11 @@ is_current_session(SessionID, Withdrawal) -> %% Internals +body_changed_events(#{new_body := NewBody}, #{body := OldBody}) -> + [{body_changed, #{new_body => NewBody, old_body => OldBody}}]; +body_changed_events(_, _) -> + []. + -spec do_start_adjustment(adjustment_params(), withdrawal_state()) -> {ok, process_result()} | {error, start_adjustment_error()}. @@ -690,7 +706,8 @@ deduce_activity(Withdrawal) -> p_transfer => p_transfer_status(Withdrawal), validation => withdrawal_validation_status(Withdrawal), session => get_current_session_status(Withdrawal), - %% TODO changed_body + is_body_changed => is_body_changed(Withdrawal), + p_transfer_rebuild_needed => is_p_transfer_rebuilding(Withdrawal), status => status(Withdrawal), limit_check => limit_check_processing_status(Withdrawal), active_adjustment => ff_adjustment_utils:is_active(adjustments_index(Withdrawal)) @@ -720,7 +737,15 @@ do_pending_activity(#{p_transfer := cancelled, limit_check := failed}) -> {fail, limit_check}; do_pending_activity(#{p_transfer := prepared, session := pending}) -> session_sleeping; -do_pending_activity(#{p_transfer := prepared, session := succeeded}) -> +do_pending_activity(#{p_transfer := prepared, session := succeeded, is_body_changed := false}) -> + p_transfer_commit; +do_pending_activity(#{ + p_transfer := prepared, session := succeeded, is_body_changed := true, p_transfer_rebuild_needed := true +}) -> + p_transfer_cancel; +do_pending_activity(#{p_transfer := cancelled, session := succeeded, is_body_changed := true}) -> + p_transfer_start; +do_pending_activity(#{p_transfer := prepared, session := succeeded, is_body_changed := true}) -> p_transfer_commit; do_pending_activity(#{p_transfer := committed, session := succeeded}) -> finish; @@ -966,7 +991,15 @@ construct_session_id(Withdrawal) -> -spec construct_p_transfer_id(withdrawal_state()) -> id(). construct_p_transfer_id(Withdrawal) -> ID = id(Withdrawal), - Attempt = ff_withdrawal_route_attempt_utils:get_attempt(attempts(Withdrawal)), + Attempt0 = ff_withdrawal_route_attempt_utils:get_attempt(attempts(Withdrawal)), + Attempt = + case is_body_changed(Withdrawal) of + true -> + %% add offset for new casflow + Attempt0 + 100; + false -> + Attempt0 + end, SubID = integer_to_binary(Attempt), <<"ff/withdrawal/", ID/binary, "/", SubID/binary>>. @@ -1016,7 +1049,7 @@ make_final_cash_flow(Withdrawal) -> -spec make_final_cash_flow(domain_revision(), withdrawal_state()) -> final_cash_flow(). make_final_cash_flow(DomainRevision, Withdrawal) -> - Body = body(Withdrawal), + Body = final_body(Withdrawal), WalletID = wallet_id(Withdrawal), {ok, Wallet} = fetch_wallet(WalletID, party_id(Withdrawal), DomainRevision), @@ -1836,23 +1869,30 @@ apply_event_({limit_check, Details}, T) -> apply_event_({p_transfer, Ev}, T) -> Tr = ff_postings_transfer:apply_event(Ev, p_transfer(T)), R = ff_withdrawal_route_attempt_utils:update_current_p_transfer(Tr, attempts(T)), - update_attempts(R, T); + T1 = update_attempts(R, T), + case is_body_changed(T) of + true -> + %% transfer rebuilding already initiated + T1#{p_transfer_rebuild_needed => false}; + false -> + T1 + end; apply_event_({session_started, SessionID}, T0) -> - %% clear changed_body from previous session - T = maps:without([changed_body], T0), + %% clear new_body from previous session + T = maps:without([new_body], T0), Session = #{id => SessionID}, Attempts = attempts(T), R = ff_withdrawal_route_attempt_utils:update_current_session(Session, Attempts), update_attempts(R, T); -apply_event_({session_finished, {SessionID, Result}}, T0) -> - %% maybe update changed_body - T = set_changed_body(Result, T0), +apply_event_({session_finished, {SessionID, Result}}, T) -> Attempts = attempts(T), Session = ff_withdrawal_route_attempt_utils:get_current_session(Attempts), SessionID = maps:get(id, Session), UpdSession = Session#{result => Result}, R = ff_withdrawal_route_attempt_utils:update_current_session(UpdSession, Attempts), update_attempts(R, T); +apply_event_({body_changed, #{new_body := NewBody}}, T) -> + T#{new_body => NewBody, p_transfer_rebuild_needed => true}; apply_event_({route_changed, Route}, T) -> Attempts = attempts(T), R = ff_withdrawal_route_attempt_utils:new_route(Route, Attempts), @@ -1867,16 +1907,6 @@ apply_event_({validation, {Part, ValidationResult}}, T) -> apply_event_({adjustment, _Ev} = Event, T) -> apply_adjustment_event(Event, T). -set_changed_body({success, SuccessInfo}, WithdrawalState) when is_list(SuccessInfo) -> - case lists:keyfind(changed_body, 1, SuccessInfo) of - {_, ChangedBody} -> - WithdrawalState#{changed_body => ChangedBody}; - _ -> - WithdrawalState - end; -set_changed_body(_, WithdrawalState) -> - WithdrawalState. - -spec make_state(withdrawal()) -> withdrawal_state(). make_state(#{route := Route} = T) -> Attempts = ff_withdrawal_route_attempt_utils:new(), @@ -1923,6 +1953,21 @@ get_attempt_limit_(undefined) -> get_attempt_limit_({value, Limit}) -> ff_dmsl_codec:unmarshal(attempt_limit, Limit). +is_body_changed(#{new_body := _NewBody}) -> + true; +is_body_changed(_) -> + false. + +is_p_transfer_rebuilding(#{p_transfer_rebuild_needed := true}) -> + true; +is_p_transfer_rebuilding(_) -> + false. + +final_body(#{new_body := NewBody}) -> + NewBody; +final_body(#{body := Body}) -> + Body. + %% Tests -ifdef(TEST). diff --git a/apps/ff_transfer/src/ff_withdrawal_machine.erl b/apps/ff_transfer/src/ff_withdrawal_machine.erl index ef069a72..60f6121b 100644 --- a/apps/ff_transfer/src/ff_withdrawal_machine.erl +++ b/apps/ff_transfer/src/ff_withdrawal_machine.erl @@ -43,7 +43,7 @@ ff_withdrawal:start_adjustment_error() | unknown_withdrawal_error(). --type notify_args() :: {session_finished, session_id(), session_result()}. +-type notify_args() :: {session_finished, session_id(), session_data()}. -type notify_error() :: notfound | failed @@ -53,7 +53,7 @@ | term(). -type session_id() :: ff_withdrawal_session:id(). --type session_result() :: ff_withdrawal_session:session_result(). +-type session_data() :: ff_withdrawal_session:session_data(). -export_type([id/0]). -export_type([st/0]). @@ -195,9 +195,9 @@ process_repair(Scenario, Machine) -> ff_machine_lib:process_repair(?MODULE, Machine, Scenario). -spec process_notification(notify_args(), machine()) -> prg_result(). -process_notification({session_finished, SessionID, SessionResult}, Machine) -> +process_notification({session_finished, SessionID, SessionData}, Machine) -> Withdrawal = prg_machine:collapse(?MODULE, Machine), - case ff_withdrawal:finalize_session(SessionID, SessionResult, Withdrawal) of + case ff_withdrawal:finalize_session(SessionID, SessionData, Withdrawal) of {ok, Result} -> ff_machine_lib:to_prg_result(Result); {error, Reason} -> diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index 0bd4bde9..b63a6780 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -45,7 +45,7 @@ result => session_result(), % For validate outstanding TransactionsInfo transaction_info => transaction_info(), - changed_body => body() + new_body => body() }. -type session() :: #{ @@ -58,19 +58,13 @@ -type transaction_info() :: ff_adapter_withdrawal:transaction_info(). -type body() :: ff_accounting:body(). --type session_result() :: - success - | {success, transaction_info()} - | {success, success_info()} - | {failed, ff_adapter_withdrawal:failure()}. +-type session_result() :: success | {success, transaction_info()} | {failed, ff_adapter_withdrawal:failure()}. -type status() :: active | {finished, success | {failed, ff_adapter_withdrawal:failure()}}. -type party_id() :: ff_party:id(). - --type success_info() :: [success_info_unit()]. - --type success_info_unit() :: - {trx_info, transaction_info()} - | {changed_body, body()}. +-type session_data() :: #{ + result := session_result(), + new_body => body() +}. -type event() :: {created, session()} @@ -128,6 +122,7 @@ -export_type([process_callback_error/0]). -export_type([process_result/0]). -export_type([action/0]). +-export_type([session_data/0]). %% %% Internal types @@ -198,16 +193,14 @@ apply_event_({next_state, AdapterState}, Session) -> Session#{adapter_state => AdapterState}; apply_event_({transaction_bound, TransactionInfo}, Session) -> Session#{transaction_info => TransactionInfo}; -apply_event_({changed_body, #{new_body := NewBody}}, Session) -> - Session#{changed_body => NewBody}; +apply_event_({body_changed, #{new_body := NewBody}}, Session) -> + Session#{new_body => NewBody}; apply_event_({finished, success = Result}, Session) -> - Session1 = Session#{status => {finished, success}}, - set_session_result_(Result, Session1); + Session#{status => {finished, success}, result => Result}; apply_event_({finished, {success, TransactionInfo} = Result}, Session) -> %% for backward compatibility with events stored in DB - take TransactionInfo here. %% @see ff_adapter_withdrawal:rebind_transaction_info/1 - Session1 = Session#{status => {finished, success}, transaction_info => TransactionInfo}, - set_session_result_(Result, Session1); + Session#{status => {finished, success}, result => Result, transaction_info => TransactionInfo}; apply_event_({finished, {failed, _} = Result} = Status, Session) -> Session#{status => Status, result => Result}; apply_event_({callback, _Ev} = WrappedEvent, Session) -> @@ -215,20 +208,15 @@ apply_event_({callback, _Ev} = WrappedEvent, Session) -> Callbacks1 = ff_withdrawal_callback_utils:apply_event(WrappedEvent, Callbacks0), set_callbacks_index(Callbacks1, Session). -set_session_result_(success, #{changed_body := ChangedBody} = Session) -> - Session#{result => {success, [{changed_body, ChangedBody}]}}; -set_session_result_(success = Result, Session) -> - Session#{result => Result}; -set_session_result_({success, TrxInfo}, #{changed_body := ChangedBody} = Session) -> - Session#{result => {success, [{changed_body, ChangedBody}, {trx_info, TrxInfo}]}}; -set_session_result_({success, _TrxInfo} = Result, Session) -> - Session#{result => Result}. - -spec process_session(session_state()) -> process_result(). -process_session(#{status := {finished, _}, id := ID, result := Result, withdrawal := Withdrawal}) -> +process_session(#{status := {finished, _}, id := ID, result := Result, withdrawal := Withdrawal} = St) -> % Session has finished, it should notify the withdrawal machine about the fact WithdrawalID = ff_adapter_withdrawal:id(Withdrawal), - case ff_withdrawal_machine:notify(WithdrawalID, {session_finished, ID, Result}) of + SessionData = genlib_map:compact(#{ + result => Result, + new_body => maps:get(new_body, St, undefined) + }), + case ff_withdrawal_machine:notify(WithdrawalID, {session_finished, ID, SessionData}) of ok -> {suspend, []}; {error, failed} -> @@ -256,7 +244,7 @@ process_session(#{status := active, withdrawal := Withdrawal, route := Route} = #{intent := Intent} = ProcessResult, Events0 = process_next_state(ProcessResult, [], ASt), Events1 = process_transaction_info(ProcessResult, Events0, SessionState), - Events2 = process_changed_body(ProcessResult, Events1, Withdrawal), + Events2 = process_new_body(ProcessResult, Events1, Withdrawal), process_adapter_intent(Intent, SessionState, Events2). process_transaction_info(#{transaction_info := TrxInfo}, Events, SessionState) -> @@ -265,9 +253,9 @@ process_transaction_info(#{transaction_info := TrxInfo}, Events, SessionState) - process_transaction_info(_, Events, _Session) -> Events. -process_changed_body(#{changed_body := NewBody}, Events, #{cash := OldBody} = _Withdrawal) -> - Events ++ [{changed_body, #{old_body => OldBody, new_body => NewBody}}]; -process_changed_body(_Result, Events, _Withdrawal) -> +process_new_body(#{new_body := NewBody}, Events, #{cash := OldBody} = _Withdrawal) -> + Events ++ [{body_changed, #{old_body => OldBody, new_body => NewBody}}]; +process_new_body(_Result, Events, _Withdrawal) -> Events. %% Only one static TransactionInfo within one session @@ -322,7 +310,7 @@ do_process_callback(CallbackParams, Callback, Session) -> Events0 = ff_withdrawal_callback_utils:process_response(Response, Callback), Events1 = process_next_state(HandleCallbackResult, Events0, AdapterState), Events2 = process_transaction_info(HandleCallbackResult, Events1, Session), - Events3 = process_changed_body(HandleCallbackResult, Events2, Withdrawal), + Events3 = process_new_body(HandleCallbackResult, Events2, Withdrawal), {ok, {Response, process_adapter_intent(Intent, Session, Events3)}}. make_session_finish_params(Session) -> diff --git a/rebar.lock b/rebar.lock index d8d00d03..82782cf0 100644 --- a/rebar.lock +++ b/rebar.lock @@ -31,7 +31,7 @@ {<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2}, {<<"damsel">>, {git,"https://github.com/valitydev/damsel.git", - {ref,"ba6dc2b3eb6ba3b3cdf33ddb1432629d0b60544c"}}, + {ref,"f4d6180261ee73c87a17f2a39fdcdee4525f97a6"}}, 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt-client.git", @@ -69,7 +69,7 @@ 0}, {<<"fistful_proto">>, {git,"https://github.com/valitydev/fistful-proto.git", - {ref,"a9d268b3e14008a689b71362361b021f996103ae"}}, + {ref,"5687061a169445d195c64eaace0836893aed42d6"}}, 0}, {<<"genlib">>, {git,"https://github.com/valitydev/genlib.git", From 0104316e5ef967aefba711bee47a73e81efeb2d8 Mon Sep 17 00:00:00 2001 From: ttt161 Date: Tue, 14 Jul 2026 14:35:44 +0300 Subject: [PATCH 03/12] support for change withdrawal amount --- apps/ff_cth/src/ct_domain.erl | 31 +++++ .../test/ff_withdrawal_handler_SUITE.erl | 115 ++++++++++++++++++ apps/fistful/src/ff_postings_transfer.erl | 3 +- apps/fistful/test/ff_ct_provider.erl | 7 ++ apps/fistful/test/ff_ct_provider_handler.erl | 16 ++- rebar.lock | 2 +- 6 files changed, 170 insertions(+), 4 deletions(-) diff --git a/apps/ff_cth/src/ct_domain.erl b/apps/ff_cth/src/ct_domain.erl index dc63f76e..5a9364fb 100644 --- a/apps/ff_cth/src/ct_domain.erl +++ b/apps/ff_cth/src/ct_domain.erl @@ -9,6 +9,7 @@ -export([create_wallet/5]). -export([currency/1]). +-export([currency_data/1]). -export([category/3]). -export([payment_method/1]). -export([payment_system/2]). @@ -164,6 +165,36 @@ withdrawal_terminal(?trm(ID) = Ref, ?prv(ProviderID) = ProviderRef, TermSet) -> } }}. +-spec currency_data(SymCode :: binary()) -> dmsl_domain_thrift:'Currency'(). +currency_data(<<"EUR">> = SymCode) -> + #domain_Currency{ + name = <<"Europe"/utf8>>, + numeric_code = 978, + symbolic_code = SymCode, + exponent = 2 + }; +currency_data(<<"RUB">> = SymCode) -> + #domain_Currency{ + name = <<"Яussian Яuble"/utf8>>, + numeric_code = 643, + symbolic_code = SymCode, + exponent = 2 + }; +currency_data(<<"USD">> = SymCode) -> + #domain_Currency{ + name = <<"U$ Dollar">>, + numeric_code = 840, + symbolic_code = SymCode, + exponent = 2 + }; +currency_data(<<"BTC">> = SymCode) -> + #domain_Currency{ + name = <<"Bitcoin">>, + numeric_code = 999, + symbolic_code = SymCode, + exponent = 10 + }. + -spec currency(?DTP('CurrencyRef')) -> object(). currency(?cur(<<"EUR">> = SymCode) = Ref) -> {currency, #domain_CurrencyObject{ diff --git a/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl b/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl index c5f62112..e941f001 100644 --- a/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl +++ b/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl @@ -9,6 +9,7 @@ -include_lib("fistful_proto/include/fistful_fistful_thrift.hrl"). -include_lib("fistful_proto/include/fistful_fistful_base_thrift.hrl"). -include_lib("fistful_proto/include/fistful_cashflow_thrift.hrl"). +-include_lib("fistful_proto/include/fistful_transfer_thrift.hrl"). -export([all/0]). -export([groups/0]). @@ -44,12 +45,19 @@ -export([create_adjustment_already_has_data_revision_error_test/1]). -export([withdrawal_state_content_test/1]). -export([trace_withdrawal_test/1]). +-export([create_withdrawal_with_changed_body_test/1]). -type config() :: ct_helper:config(). -type test_case_name() :: ct_helper:test_case_name(). -type group_name() :: ct_helper:group_name(). -type test_return() :: _ | no_return(). +-define(posting(Source, Destination, Amount), #cashflow_FinalCashFlowPosting{ + source = #cashflow_FinalCashFlowAccount{account_type = Source}, + destination = #cashflow_FinalCashFlowAccount{account_type = Destination}, + volume = #fistful_base_Cash{amount = Amount} +}). + -spec all() -> [test_case_name() | {group, group_name()}]. all() -> [ @@ -66,6 +74,7 @@ groups() -> create_withdrawal_and_get_session_ok_test, create_withdrawal_ok_test, + create_withdrawal_with_changed_body_test, create_withdrawal_fail_email_test, create_cashlimit_validation_error_test, create_currency_validation_error_test, @@ -284,6 +293,112 @@ create_withdrawal_ok_test(_C) -> FinalWithdrawalState#wthd_WithdrawalState.status ). +-spec create_withdrawal_with_changed_body_test(config()) -> test_return(). +create_withdrawal_with_changed_body_test(_C) -> + Cash = make_cash({1357, <<"RUB">>}), + Ctx = ct_objects:build_default_ctx(), + #{ + party_id := PartyID, + wallet_id := WalletID, + destination_id := DestinationID + } = ct_objects:prepare_standard_environment(Ctx#{body => Cash}), + WithdrawalID = genlib:bsuuid(), + ExternalID = genlib:bsuuid(), + Context = ff_entity_context_codec:marshal(#{<<"NS">> => #{}}), + Metadata = ff_entity_context_codec:marshal(#{<<"metadata">> => #{<<"some key">> => <<"some data">>}}), + ContactInfo = #fistful_base_ContactInfo{ + phone_number = <<"1234567890">>, + email = <<"test@mail.com">> + }, + Params = #wthd_WithdrawalParams{ + id = WithdrawalID, + party_id = PartyID, + wallet_id = WalletID, + destination_id = DestinationID, + body = Cash, + metadata = Metadata, + external_id = ExternalID, + contact_info = ContactInfo + }, + {ok, _WithdrawalState} = call_withdrawal('Create', {Params, Context}), + %% Adapter will change amount on 1246 + succeeded = ct_objects:await_final_withdrawal_status(WithdrawalID), + {ok, #wthd_WithdrawalState{ + effective_final_cash_flow = #cashflow_FinalCashFlow{postings = Postings}, + new_body = #fistful_base_Cash{amount = NewAmount} + }} = call_withdrawal('Get', {WithdrawalID, #'fistful_base_EventRange'{}}), + ?assertEqual(1246, NewAmount), + [ + ?posting({system, settlement}, {provider, settlement}, 10), + ?posting({wallet, receiver_destination}, {system, settlement}, 125), + ?posting({wallet, receiver_destination}, {system, subagent}, 125), + ?posting({wallet, sender_settlement}, {wallet, receiver_destination}, 1246) + ] = lists:sort(Postings), + + Range = {undefined, undefined}, + EncodedRange = ff_codec:marshal(event_range, Range), + {ok, Events} = call_withdrawal('GetEvents', {WithdrawalID, EncodedRange}), + [ + #wthd_Event{change = {created, _}}, + #wthd_Event{change = {status_changed, _}}, + #wthd_Event{change = {resource, _}}, + #wthd_Event{change = {route, _}}, + #wthd_Event{ + change = + {transfer, #wthd_TransferChange{ + payload = {created, _} + }} + }, + #wthd_Event{ + change = + {transfer, #wthd_TransferChange{ + payload = {status_changed, #transfer_StatusChange{status = {created, _}}} + }} + }, + #wthd_Event{ + change = + {transfer, #wthd_TransferChange{ + payload = {status_changed, #transfer_StatusChange{status = {prepared, _}}} + }} + }, + #wthd_Event{change = {limit_check, _}}, + #wthd_Event{change = {session, _}}, + #wthd_Event{change = {session, _}}, + #wthd_Event{change = {body_changed, _}}, + #wthd_Event{ + change = + {transfer, #wthd_TransferChange{ + payload = {status_changed, #transfer_StatusChange{status = {cancelled, _}}} + }} + }, + #wthd_Event{ + change = + {transfer, #wthd_TransferChange{ + payload = {created, _} + }} + }, + #wthd_Event{ + change = + {transfer, #wthd_TransferChange{ + payload = {status_changed, #transfer_StatusChange{status = {created, _}}} + }} + }, + #wthd_Event{ + change = + {transfer, #wthd_TransferChange{ + payload = {status_changed, #transfer_StatusChange{status = {prepared, _}}} + }} + }, + #wthd_Event{ + change = + {transfer, #wthd_TransferChange{ + payload = {status_changed, #transfer_StatusChange{status = {committed, _}}} + }} + }, + #wthd_Event{change = {status_changed, _}} + ] = Events, + ok. + -spec trace_withdrawal_test(config()) -> test_return(). trace_withdrawal_test(_C) -> Cash = make_cash({1000, <<"RUB">>}), diff --git a/apps/fistful/src/ff_postings_transfer.erl b/apps/fistful/src/ff_postings_transfer.erl index 2a260a5f..c09c455c 100644 --- a/apps/fistful/src/ff_postings_transfer.erl +++ b/apps/fistful/src/ff_postings_transfer.erl @@ -173,7 +173,8 @@ cancel(#{status := Status}) -> %% -spec apply_event(event(), ff_maybe:'maybe'(transfer())) -> transfer(). -apply_event({created, Transfer}, undefined) -> +apply_event({created, Transfer}, _) -> + %% transfer must be recreated when withdrawal body changed Transfer; apply_event({status_changed, S}, Transfer) -> Transfer#{status => S}. diff --git a/apps/fistful/test/ff_ct_provider.erl b/apps/fistful/test/ff_ct_provider.erl index 6182ab37..b76c2e45 100644 --- a/apps/fistful/test/ff_ct_provider.erl +++ b/apps/fistful/test/ff_ct_provider.erl @@ -123,6 +123,13 @@ process_withdrawal(#{contact_info := #{email := <<"fail_it@mymail.com">>}}, Stat intent => {finish, {failed, #{code => <<"email_error">>}}}, next_state => State }}; +process_withdrawal(#{body := #wthd_provider_Cash{amount = 1357}}, State, _Options) -> + %% change body scenario + {ok, #{ + intent => {finish, {success, ?TRX_INFO}}, + next_state => State, + new_body => {1246, <<"RUB">>} + }}; process_withdrawal(#{auth_data := #{sender := <<"SenderToken">>, receiver := <<"ReceiverToken">>}}, State, _Options) -> {ok, #{ intent => {finish, {success, ?TRX_INFO}}, diff --git a/apps/fistful/test/ff_ct_provider_handler.erl b/apps/fistful/test/ff_ct_provider_handler.erl index 6f03a470..3f54ea53 100644 --- a/apps/fistful/test/ff_ct_provider_handler.erl +++ b/apps/fistful/test/ff_ct_provider_handler.erl @@ -24,10 +24,12 @@ handle_function('ProcessWithdrawal', {Withdrawal, InternalState, Options}, _Cont #{intent := Intent} = ProcessResult, NewState = maps:get(next_state, ProcessResult, undefined), TransactionInfo = maps:get(transaction_info, ProcessResult, undefined), + NewBody = maps:get(new_body, ProcessResult, undefined), {ok, #wthd_provider_ProcessResult{ intent = encode_intent(Intent), next_state = encode_state(NewState), - trx = encode_trx(TransactionInfo) + trx = encode_trx(TransactionInfo), + new_body = encode_body(NewBody) }}; handle_function('GetQuote', {QuoteParams, Options}, _Context, Opts) -> Handler = get_handler(Opts), @@ -45,11 +47,13 @@ handle_function('HandleCallback', {Callback, Withdrawal, InternalState, Options} #{intent := Intent, response := Response} = CallbackResult, NewState = maps:get(next_state, CallbackResult, undefined), TransactionInfo = maps:get(transaction_info, CallbackResult, undefined), + NewBody = maps:get(new_body, CallbackResult, undefined), {ok, #wthd_provider_CallbackResult{ intent = encode_intent(Intent), next_state = encode_state(NewState), response = encode_callback_response(Response), - trx = encode_trx(TransactionInfo) + trx = encode_trx(TransactionInfo), + new_body = encode_body(NewBody) }}. %% @@ -156,5 +160,13 @@ encode_quote(#{ encode_callback_response(#{payload := Payload}) -> #wthd_provider_CallbackResponse{payload = Payload}. +encode_body(undefined) -> + undefined; +encode_body({Amount, SymCode}) -> + #wthd_provider_Cash{ + amount = Amount, + currency = ct_domain:currency_data(SymCode) + }. + get_handler(Opts) -> proplists:get_value(handler, Opts, ff_ct_provider). diff --git a/rebar.lock b/rebar.lock index 82782cf0..8f5839fb 100644 --- a/rebar.lock +++ b/rebar.lock @@ -31,7 +31,7 @@ {<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2}, {<<"damsel">>, {git,"https://github.com/valitydev/damsel.git", - {ref,"f4d6180261ee73c87a17f2a39fdcdee4525f97a6"}}, + {ref,"1b5ebf8e7ca0f89f09915a1b04d9968cffe794ac"}}, 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt-client.git", From 4e2dc52522229a1be4e184155181b4c54f8e5e3f Mon Sep 17 00:00:00 2001 From: ttt161 Date: Tue, 14 Jul 2026 14:39:46 +0300 Subject: [PATCH 04/12] bump damsel --- rebar.config | 2 +- rebar.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rebar.config b/rebar.config index 6f82ff7b..ab93da35 100644 --- a/rebar.config +++ b/rebar.config @@ -36,7 +36,7 @@ {woody, {git, "https://github.com/valitydev/woody_erlang.git", {tag, "v1.1.2"}}}, {scoper, {git, "https://github.com/valitydev/scoper.git", {tag, "v1.1.0"}}}, {thrift, {git, "https://github.com/valitydev/thrift_erlang.git", {tag, "v1.0.0"}}}, - {damsel, {git, "https://github.com/valitydev/damsel.git", {branch, "ft/change-withdrawal-amount"}}}, + {damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.41"}}}, {exrates_proto, {git, "https://github.com/valitydev/exrates-proto.git", {branch, "master"}}}, {payproc_errors, {git, "https://github.com/valitydev/payproc-errors-erlang.git", {branch, "master"}}}, {mg_proto, {git, "https://github.com/valitydev/machinegun-proto.git", {branch, "master"}}}, diff --git a/rebar.lock b/rebar.lock index 8f5839fb..b6a1a3a2 100644 --- a/rebar.lock +++ b/rebar.lock @@ -31,7 +31,7 @@ {<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2}, {<<"damsel">>, {git,"https://github.com/valitydev/damsel.git", - {ref,"1b5ebf8e7ca0f89f09915a1b04d9968cffe794ac"}}, + {ref,"239638b3689dfa4a20dcb17f3715167957129c30"}}, 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt-client.git", From a81aa388a781fc702e9f5d1372401ee03175d636 Mon Sep 17 00:00:00 2001 From: ttt161 Date: Tue, 14 Jul 2026 20:03:29 +0300 Subject: [PATCH 05/12] bump fistful_proto --- rebar.config | 2 +- rebar.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rebar.config b/rebar.config index ab93da35..ef818768 100644 --- a/rebar.config +++ b/rebar.config @@ -49,7 +49,7 @@ {herd, {git, "https://github.com/wgnet/herd.git", {tag, "1.3.4"}}}, {progressor, {git, "https://github.com/valitydev/progressor.git", {tag, "v1.0.25"}}}, {machinery, {git, "https://github.com/valitydev/machinery-erlang.git", {tag, "v1.1.22"}}}, - {fistful_proto, {git, "https://github.com/valitydev/fistful-proto.git", {branch, "ft/change-withdrawal-amount"}}}, + {fistful_proto, {git, "https://github.com/valitydev/fistful-proto.git", {tag, "v2.0.2"}}}, {binbase_proto, {git, "https://github.com/valitydev/binbase-proto.git", {branch, "master"}}}, {validator_personal_data_proto, {git, "https://github.com/valitydev/validator-personal-data-proto.git", {branch, "master"}}}, diff --git a/rebar.lock b/rebar.lock index b6a1a3a2..4feb3934 100644 --- a/rebar.lock +++ b/rebar.lock @@ -69,7 +69,7 @@ 0}, {<<"fistful_proto">>, {git,"https://github.com/valitydev/fistful-proto.git", - {ref,"5687061a169445d195c64eaace0836893aed42d6"}}, + {ref,"f7ce08e529188f6786caffecc10978c2b28603f6"}}, 0}, {<<"genlib">>, {git,"https://github.com/valitydev/genlib.git", From 2903849146b8dc09302b81740ce135d788e844f0 Mon Sep 17 00:00:00 2001 From: ttt161 Date: Tue, 14 Jul 2026 20:08:44 +0300 Subject: [PATCH 06/12] bump fistful proto --- rebar.config | 2 +- rebar.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rebar.config b/rebar.config index ef818768..e51f793a 100644 --- a/rebar.config +++ b/rebar.config @@ -49,7 +49,7 @@ {herd, {git, "https://github.com/wgnet/herd.git", {tag, "1.3.4"}}}, {progressor, {git, "https://github.com/valitydev/progressor.git", {tag, "v1.0.25"}}}, {machinery, {git, "https://github.com/valitydev/machinery-erlang.git", {tag, "v1.1.22"}}}, - {fistful_proto, {git, "https://github.com/valitydev/fistful-proto.git", {tag, "v2.0.2"}}}, + {fistful_proto, {git, "https://github.com/valitydev/fistful-proto.git", {tag, "v2.0.3"}}}, {binbase_proto, {git, "https://github.com/valitydev/binbase-proto.git", {branch, "master"}}}, {validator_personal_data_proto, {git, "https://github.com/valitydev/validator-personal-data-proto.git", {branch, "master"}}}, diff --git a/rebar.lock b/rebar.lock index 4feb3934..cd3326da 100644 --- a/rebar.lock +++ b/rebar.lock @@ -69,7 +69,7 @@ 0}, {<<"fistful_proto">>, {git,"https://github.com/valitydev/fistful-proto.git", - {ref,"f7ce08e529188f6786caffecc10978c2b28603f6"}}, + {ref,"f67259f5e1a3aaefe189bc74e9d7fefd2eef49b2"}}, 0}, {<<"genlib">>, {git,"https://github.com/valitydev/genlib.git", From 9bcac473df6d3e77f15e9e4a11e44505e593b5ea Mon Sep 17 00:00:00 2001 From: ttt161 Date: Mon, 20 Jul 2026 16:59:41 +0300 Subject: [PATCH 07/12] fix issues, add route limits re-creation --- apps/ff_cth/include/ct_domain.hrl | 1 + apps/ff_cth/src/ct_limiter.erl | 5 ++- apps/ff_cth/src/ct_payment_system.erl | 26 +++++++++++ .../test/ff_withdrawal_handler_SUITE.erl | 15 ++++++- apps/ff_transfer/src/ff_withdrawal.erl | 43 ++++++++++++++++--- .../ff_transfer/src/ff_withdrawal_session.erl | 19 ++++++-- 6 files changed, 96 insertions(+), 13 deletions(-) diff --git a/apps/ff_cth/include/ct_domain.hrl b/apps/ff_cth/include/ct_domain.hrl index 977442f6..f263df48 100644 --- a/apps/ff_cth/include/ct_domain.hrl +++ b/apps/ff_cth/include/ct_domain.hrl @@ -14,6 +14,7 @@ -define(LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID3, <<"ID5">>). -define(LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID4, <<"ID6">>). -define(LIMIT_TURNOVER_NUM_SENDER_ID1, <<"ID7">>). +-define(LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID999, <<"ID999">>). -define(glob(), #domain_GlobalsRef{}). -define(cur(ID), #domain_CurrencyRef{symbolic_code = ID}). diff --git a/apps/ff_cth/src/ct_limiter.erl b/apps/ff_cth/src/ct_limiter.erl index 52b90424..fd377f1b 100644 --- a/apps/ff_cth/src/ct_limiter.erl +++ b/apps/ff_cth/src/ct_limiter.erl @@ -27,7 +27,8 @@ init_per_suite(Config) -> {limit_config, limiter_mk_config_object_amount(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID2)}, {limit_config, limiter_mk_config_object_amount(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID3)}, {limit_config, limiter_mk_config_object_amount(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID4)}, - {limit_config, limiter_mk_config_object_num(?LIMIT_TURNOVER_NUM_SENDER_ID1, SenderScopes)} + {limit_config, limiter_mk_config_object_num(?LIMIT_TURNOVER_NUM_SENDER_ID1, SenderScopes)}, + {limit_config, limiter_mk_config_object_amount(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID999)} ], dmt_client:create_author(genlib:unique(), genlib:unique()) ), @@ -63,6 +64,8 @@ maybe_uninitialized_limit(LimitID, {exception, _}) -> description = undefined }. +maybe_marshal_withdrawal(undefined) -> + undefined; maybe_marshal_withdrawal(#wthd_domain_Withdrawal{} = Withdrawal) -> Withdrawal; maybe_marshal_withdrawal(Withdrawal) -> diff --git a/apps/ff_cth/src/ct_payment_system.erl b/apps/ff_cth/src/ct_payment_system.erl index 4191a6d4..b117a849 100644 --- a/apps/ff_cth/src/ct_payment_system.erl +++ b/apps/ff_cth/src/ct_payment_system.erl @@ -535,6 +535,14 @@ domain_config(Config, Options) -> condition(cost_in, {940000, <<"RUB">>}), ?ruleset(?PAYINST1_ROUTING_POLICIES + 33) ), + delegate( + condition(cost_in, {1357, <<"RUB">>}), + ?ruleset(?PAYINST1_ROUTING_POLICIES + 41) + ), + delegate( + condition(cost_in, {1246, <<"RUB">>}), + ?ruleset(?PAYINST1_ROUTING_POLICIES + 41) + ), delegate( {condition, {payment_tool, @@ -739,6 +747,13 @@ domain_config(Config, Options) -> ]} ), + routing_ruleset( + ?ruleset(?PAYINST1_ROUTING_POLICIES + 41), + {candidates, [ + candidate({constant, true}, ?trm(4100)) + ]} + ), + routing_ruleset( ?ruleset(?PAYINST1_ROUTING_PROHIBITIONS), <<"PayInst1 Withdrawal Prohibitions">>, @@ -1252,6 +1267,17 @@ domain_config(Config, Options) -> } ), + ct_domain:withdrawal_terminal(?trm(4100), ?prv(1), #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + turnover_limit = + {value, [ + ?trnvrlimit(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID999, 1000000000, Config) + ]} + } + } + }), + ct_domain:currency(?cur(<<"RUB">>)), ct_domain:currency(?cur(<<"USD">>)), ct_domain:currency(?cur(<<"EUR">>)), diff --git a/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl b/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl index e941f001..2378e12e 100644 --- a/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl +++ b/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl @@ -10,6 +10,7 @@ -include_lib("fistful_proto/include/fistful_fistful_base_thrift.hrl"). -include_lib("fistful_proto/include/fistful_cashflow_thrift.hrl"). -include_lib("fistful_proto/include/fistful_transfer_thrift.hrl"). +-include_lib("ff_cth/include/ct_domain.hrl"). -export([all/0]). -export([groups/0]). @@ -294,14 +295,20 @@ create_withdrawal_ok_test(_C) -> ). -spec create_withdrawal_with_changed_body_test(config()) -> test_return(). -create_withdrawal_with_changed_body_test(_C) -> +create_withdrawal_with_changed_body_test(C) -> Cash = make_cash({1357, <<"RUB">>}), Ctx = ct_objects:build_default_ctx(), + LimitsRev = ct_helper:cfg('$limits_domain_revision', C), #{ party_id := PartyID, wallet_id := WalletID, - destination_id := DestinationID + destination_id := DestinationID, + withdrawal_id := PreviousWithdrawalID } = ct_objects:prepare_standard_environment(Ctx#{body => Cash}), + + PreviousWithdrawal = get_withdrawal(PreviousWithdrawalID), + Limit0 = ct_limiter:get_limit_amount(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID999, LimitsRev, PreviousWithdrawal, C), + WithdrawalID = genlib:bsuuid(), ExternalID = genlib:bsuuid(), Context = ff_entity_context_codec:marshal(#{<<"NS">> => #{}}), @@ -335,6 +342,10 @@ create_withdrawal_with_changed_body_test(_C) -> ?posting({wallet, sender_settlement}, {wallet, receiver_destination}, 1246) ] = lists:sort(Postings), + Withdrawal = get_withdrawal(WithdrawalID), + Limit1 = ct_limiter:get_limit_amount(?LIMIT_TURNOVER_AMOUNT_PAYTOOL_ID999, LimitsRev, Withdrawal, C), + ?assertEqual(1246, Limit1 - Limit0), + Range = {undefined, undefined}, EncodedRange = ff_codec:marshal(event_range, Range), {ok, Events} = call_withdrawal('GetEvents', {WithdrawalID, EncodedRange}), diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 6db87e52..fa40113b 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -89,6 +89,7 @@ | {validation, {sender | receiver, validation_result()}} | {session_started, session_id()} | {session_finished, {session_id(), session_result()}} + | {body_changed, #{old_body := body(), new_body := body()}} | {status_changed, status()} | wrapped_adjustment_event(). @@ -313,6 +314,7 @@ -type activity() :: routing | p_transfer_start + | p_transfer_recreate | p_transfer_prepare | session_starting | session_sleeping @@ -744,7 +746,7 @@ do_pending_activity(#{ }) -> p_transfer_cancel; do_pending_activity(#{p_transfer := cancelled, session := succeeded, is_body_changed := true}) -> - p_transfer_start; + p_transfer_recreate; do_pending_activity(#{p_transfer := prepared, session := succeeded, is_body_changed := true}) -> p_transfer_commit; do_pending_activity(#{p_transfer := committed, session := succeeded}) -> @@ -764,13 +766,31 @@ do_process_transfer(routing, Withdrawal) -> process_routing(Withdrawal); do_process_transfer(p_transfer_start, Withdrawal) -> process_p_transfer_creation(Withdrawal); +do_process_transfer(p_transfer_recreate, Withdrawal) -> + %% route limits already rolled back when p_transfer was cancelled + %% new route limits with new amount are required + Route = route(Withdrawal), + DomainRevision = final_domain_revision(Withdrawal), + IterWithMultiplication = ff_withdrawal_route_attempt_utils:get_index(attempts(Withdrawal)) * 1000, + {Varset, _Context} = make_routing_varset_and_context(Withdrawal), + {ok, #domain_ProvisionTermSet{ + wallet = #domain_WalletProvisionTerms{ + withdrawals = #domain_WithdrawalProvisionTerms{ + turnover_limit = SelectorTurnoverLimits + } + } + }} = compute_provider_terminal_terms(Route, Varset, DomainRevision), + TurnoverLimits = ff_limiter:get_turnover_limits(SelectorTurnoverLimits), + WithdrawalWithNewBody = Withdrawal#{body => final_body(Withdrawal)}, + ok = ff_limiter:hold_withdrawal_limits(TurnoverLimits, WithdrawalWithNewBody, Route, IterWithMultiplication), + process_p_transfer_creation(Withdrawal); do_process_transfer(p_transfer_prepare, Withdrawal) -> ok = do_rollback_routing([route(Withdrawal)], Withdrawal), Tr = ff_withdrawal_route_attempt_utils:get_current_p_transfer(attempts(Withdrawal)), {ok, Events} = ff_postings_transfer:prepare(Tr), {timeout, [{p_transfer, Ev} || Ev <- Events]}; do_process_transfer(p_transfer_commit, Withdrawal) -> - ok = commit_routes_limits([route(Withdrawal)], Withdrawal), + ok = commit_routes_limits([route(Withdrawal)], Withdrawal#{body => final_body(Withdrawal)}), Tr = ff_withdrawal_route_attempt_utils:get_current_p_transfer(attempts(Withdrawal)), {ok, Events} = ff_postings_transfer:commit(Tr), DomainRevision = final_domain_revision(Withdrawal), @@ -862,7 +882,15 @@ rollback_routes_limits(Routes, Varset, Context) -> ff_withdrawal_routing:rollback_routes_limits(Routes, Varset, Context). commit_routes_limits(Routes, Withdrawal) -> - {Varset, Context} = make_routing_varset_and_context(Withdrawal), + {Varset, Context0} = make_routing_varset_and_context(Withdrawal), + %% if body was changed then iteration value needs to be updated + Context = + case is_body_changed(Withdrawal) of + true -> + update_context_iteration(1000, Context0); + false -> + Context0 + end, ff_withdrawal_routing:commit_routes_limits(Routes, Varset, Context). make_routing_varset_and_context(Withdrawal) -> @@ -888,6 +916,9 @@ make_routing_varset_and_context(Withdrawal) -> }, {build_party_varset(VarsetParams), Context}. +update_context_iteration(Multiplicator, #{iteration := Iter} = Context) -> + Context#{iteration => Iter * Multiplicator}. + -spec validate_quote_route(route(), quote_state()) -> {ok, valid} | {error, InconsistentQuote} when InconsistentQuote :: {inconsistent_quote_route, {provider_id, provider_id()} | {terminal_id, terminal_id()}}. validate_quote_route(Route, #{route := QuoteRoute}) -> @@ -995,8 +1026,8 @@ construct_p_transfer_id(Withdrawal) -> Attempt = case is_body_changed(Withdrawal) of true -> - %% add offset for new casflow - Attempt0 + 100; + %% ID for new casflow with multiplication + Attempt0 * 1000; false -> Attempt0 end, @@ -1063,7 +1094,7 @@ make_final_cash_flow(DomainRevision, Withdrawal) -> Resource = destination_resource(Withdrawal), VarsetParams = genlib_map:compact(#{ - body => body(Withdrawal), + body => Body, wallet_id => WalletID, wallet => Wallet, party_id => party_id(Withdrawal), diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index b63a6780..9e31a4d8 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -70,6 +70,7 @@ {created, session()} | {next_state, ff_adapter:state()} | {transaction_bound, transaction_info()} + | {body_changed, #{old_body := body(), new_body := body()}} | {finished, session_result()} | wrapped_callback_event(). @@ -244,7 +245,7 @@ process_session(#{status := active, withdrawal := Withdrawal, route := Route} = #{intent := Intent} = ProcessResult, Events0 = process_next_state(ProcessResult, [], ASt), Events1 = process_transaction_info(ProcessResult, Events0, SessionState), - Events2 = process_new_body(ProcessResult, Events1, Withdrawal), + Events2 = process_new_body(ProcessResult, Events1, SessionState), process_adapter_intent(Intent, SessionState, Events2). process_transaction_info(#{transaction_info := TrxInfo}, Events, SessionState) -> @@ -253,9 +254,19 @@ process_transaction_info(#{transaction_info := TrxInfo}, Events, SessionState) - process_transaction_info(_, Events, _Session) -> Events. -process_new_body(#{new_body := NewBody}, Events, #{cash := OldBody} = _Withdrawal) -> - Events ++ [{body_changed, #{old_body => OldBody, new_body => NewBody}}]; -process_new_body(_Result, Events, _Withdrawal) -> +process_new_body(#{new_body := NewBody}, Events, #{new_body := PreviousBody} = _SessionState) when + NewBody =:= PreviousBody +-> + %% body already updated + Events; +process_new_body(#{new_body := NewBody}, Events, #{withdrawal := #{cash := OldBody}} = _SessionState) -> + case OldBody < NewBody of + true -> + erlang:error({new_body_is_big, NewBody, OldBody}); + false -> + Events ++ [{body_changed, #{old_body => OldBody, new_body => NewBody}}] + end; +process_new_body(_Result, Events, _SessionState) -> Events. %% Only one static TransactionInfo within one session From f82389136cb05ea982dc32bf94913277227214ec Mon Sep 17 00:00:00 2001 From: ttt161 Date: Mon, 20 Jul 2026 17:11:42 +0300 Subject: [PATCH 08/12] cleanup --- apps/ff_cth/src/ct_limiter.erl | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/ff_cth/src/ct_limiter.erl b/apps/ff_cth/src/ct_limiter.erl index fd377f1b..f47672a7 100644 --- a/apps/ff_cth/src/ct_limiter.erl +++ b/apps/ff_cth/src/ct_limiter.erl @@ -64,8 +64,6 @@ maybe_uninitialized_limit(LimitID, {exception, _}) -> description = undefined }. -maybe_marshal_withdrawal(undefined) -> - undefined; maybe_marshal_withdrawal(#wthd_domain_Withdrawal{} = Withdrawal) -> Withdrawal; maybe_marshal_withdrawal(Withdrawal) -> From f1772edb3701829eb43dcf1f41339df30c14ecdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Wed, 22 Jul 2026 14:37:10 +0300 Subject: [PATCH 09/12] Refactor process_new_body to validate new body and handle unchanged cases; update apply_event to handle undefined status; fix comment typo in construct_p_transfer_id --- apps/ff_transfer/src/ff_withdrawal.erl | 10 ++++---- .../ff_transfer/src/ff_withdrawal_session.erl | 23 ++++++++++++++----- apps/fistful/src/ff_postings_transfer.erl | 4 +++- 3 files changed, 26 insertions(+), 11 deletions(-) diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index fa40113b..880542f5 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -9,6 +9,7 @@ -type id() :: binary(). -define(ACTUAL_FORMAT_VERSION, 4). +-define(BODY_CHANGED_ATTEMPT_MULTIPLIER, 1000). -opaque withdrawal_state() :: #{ id := id(), @@ -771,7 +772,8 @@ do_process_transfer(p_transfer_recreate, Withdrawal) -> %% new route limits with new amount are required Route = route(Withdrawal), DomainRevision = final_domain_revision(Withdrawal), - IterWithMultiplication = ff_withdrawal_route_attempt_utils:get_index(attempts(Withdrawal)) * 1000, + IterWithMultiplication = + ff_withdrawal_route_attempt_utils:get_index(attempts(Withdrawal)) * ?BODY_CHANGED_ATTEMPT_MULTIPLIER, {Varset, _Context} = make_routing_varset_and_context(Withdrawal), {ok, #domain_ProvisionTermSet{ wallet = #domain_WalletProvisionTerms{ @@ -887,7 +889,7 @@ commit_routes_limits(Routes, Withdrawal) -> Context = case is_body_changed(Withdrawal) of true -> - update_context_iteration(1000, Context0); + update_context_iteration(?BODY_CHANGED_ATTEMPT_MULTIPLIER, Context0); false -> Context0 end, @@ -1026,8 +1028,8 @@ construct_p_transfer_id(Withdrawal) -> Attempt = case is_body_changed(Withdrawal) of true -> - %% ID for new casflow with multiplication - Attempt0 * 1000; + %% ID for new cashflow with multiplication + Attempt0 * ?BODY_CHANGED_ATTEMPT_MULTIPLIER; false -> Attempt0 end, diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index 9e31a4d8..739d8355 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -260,15 +260,26 @@ process_new_body(#{new_body := NewBody}, Events, #{new_body := PreviousBody} = _ %% body already updated Events; process_new_body(#{new_body := NewBody}, Events, #{withdrawal := #{cash := OldBody}} = _SessionState) -> - case OldBody < NewBody of - true -> - erlang:error({new_body_is_big, NewBody, OldBody}); - false -> - Events ++ [{body_changed, #{old_body => OldBody, new_body => NewBody}}] + case validate_new_body(OldBody, NewBody) of + ok -> + Events ++ [{body_changed, #{old_body => OldBody, new_body => NewBody}}]; + unchanged -> + Events end; process_new_body(_Result, Events, _SessionState) -> Events. +validate_new_body({Amount, Currency}, {Amount, Currency}) -> + unchanged; +validate_new_body({OldAmount, Currency}, {NewAmount, Currency}) when + is_integer(NewAmount), NewAmount > 0, NewAmount < OldAmount +-> + ok; +validate_new_body(OldBody, NewBody) -> + %% Adapter may have already paid out; do not finish session as business + %% failure (that can trigger route retry). Hard-fail like trx mismatch. + erlang:error({invalid_new_body, NewBody, OldBody}). + %% Only one static TransactionInfo within one session assert_transaction_info(_NewTrxInfo, undefined) -> @@ -321,7 +332,7 @@ do_process_callback(CallbackParams, Callback, Session) -> Events0 = ff_withdrawal_callback_utils:process_response(Response, Callback), Events1 = process_next_state(HandleCallbackResult, Events0, AdapterState), Events2 = process_transaction_info(HandleCallbackResult, Events1, Session), - Events3 = process_new_body(HandleCallbackResult, Events2, Withdrawal), + Events3 = process_new_body(HandleCallbackResult, Events2, Session), {ok, {Response, process_adapter_intent(Intent, Session, Events3)}}. make_session_finish_params(Session) -> diff --git a/apps/fistful/src/ff_postings_transfer.erl b/apps/fistful/src/ff_postings_transfer.erl index c09c455c..c22845df 100644 --- a/apps/fistful/src/ff_postings_transfer.erl +++ b/apps/fistful/src/ff_postings_transfer.erl @@ -173,7 +173,9 @@ cancel(#{status := Status}) -> %% -spec apply_event(event(), ff_maybe:'maybe'(transfer())) -> transfer(). -apply_event({created, Transfer}, _) -> +apply_event({created, Transfer}, undefined) -> + Transfer; +apply_event({created, Transfer}, #{status := cancelled}) -> %% transfer must be recreated when withdrawal body changed Transfer; apply_event({status_changed, S}, Transfer) -> From ec8e0d1e9a75c2402d6d9701c20fe73c1204447f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 23 Jul 2026 10:02:09 +0300 Subject: [PATCH 10/12] Add support for body change in withdrawal adjustments - Introduced new body change handling in the adjustment process. - Updated validation to prevent changing to the same body or increasing the body amount. - Added tests for various scenarios involving body changes, including successful and failed adjustments. - Refactored related functions to accommodate the new body change logic. --- apps/ff_cth/src/ct_domain.erl | 39 +---- .../src/ff_withdrawal_adjustment_codec.erl | 39 ++++- apps/ff_server/src/ff_withdrawal_handler.erl | 8 + .../test/ff_withdrawal_handler_SUITE.erl | 73 ++++++++ apps/ff_transfer/src/ff_adjustment.erl | 9 +- apps/ff_transfer/src/ff_withdrawal.erl | 97 +++++++++-- .../ff_transfer/src/ff_withdrawal_session.erl | 56 +++++- .../test/ff_withdrawal_adjustment_SUITE.erl | 161 +++++++++++++++++- elvis.config | 3 +- rebar.config | 3 +- rebar.lock | 2 +- 11 files changed, 419 insertions(+), 71 deletions(-) diff --git a/apps/ff_cth/src/ct_domain.erl b/apps/ff_cth/src/ct_domain.erl index 5a9364fb..91f59716 100644 --- a/apps/ff_cth/src/ct_domain.erl +++ b/apps/ff_cth/src/ct_domain.erl @@ -196,45 +196,10 @@ currency_data(<<"BTC">> = SymCode) -> }. -spec currency(?DTP('CurrencyRef')) -> object(). -currency(?cur(<<"EUR">> = SymCode) = Ref) -> +currency(?cur(SymCode) = Ref) -> {currency, #domain_CurrencyObject{ ref = Ref, - data = #domain_Currency{ - name = <<"Europe"/utf8>>, - numeric_code = 978, - symbolic_code = SymCode, - exponent = 2 - } - }}; -currency(?cur(<<"RUB">> = SymCode) = Ref) -> - {currency, #domain_CurrencyObject{ - ref = Ref, - data = #domain_Currency{ - name = <<"Яussian Яuble"/utf8>>, - numeric_code = 643, - symbolic_code = SymCode, - exponent = 2 - } - }}; -currency(?cur(<<"USD">> = SymCode) = Ref) -> - {currency, #domain_CurrencyObject{ - ref = Ref, - data = #domain_Currency{ - name = <<"U$ Dollar">>, - numeric_code = 840, - symbolic_code = SymCode, - exponent = 2 - } - }}; -currency(?cur(<<"BTC">> = SymCode) = Ref) -> - {currency, #domain_CurrencyObject{ - ref = Ref, - data = #domain_Currency{ - name = <<"Bitcoin">>, - numeric_code = 999, - symbolic_code = SymCode, - exponent = 10 - } + data = currency_data(SymCode) }}. -spec category(?DTP('CategoryRef'), binary(), ?DTP('CategoryType')) -> object(). diff --git a/apps/ff_server/src/ff_withdrawal_adjustment_codec.erl b/apps/ff_server/src/ff_withdrawal_adjustment_codec.erl index ff8922d4..c39223c9 100644 --- a/apps/ff_server/src/ff_withdrawal_adjustment_codec.erl +++ b/apps/ff_server/src/ff_withdrawal_adjustment_codec.erl @@ -50,7 +50,10 @@ marshal(changes_plan, Plan) -> #wthd_adj_ChangesPlan{ new_cash_flow = maybe_marshal(cash_flow_change_plan, maps:get(new_cash_flow, Plan, undefined)), new_status = maybe_marshal(status_change_plan, maps:get(new_status, Plan, undefined)), - new_domain_revision = maybe_marshal(domain_revision_change_plan, maps:get(new_domain_revision, Plan, undefined)) + new_domain_revision = maybe_marshal( + domain_revision_change_plan, maps:get(new_domain_revision, Plan, undefined) + ), + new_body = maybe_marshal(body_change_plan, maps:get(new_body, Plan, undefined)) }; marshal(cash_flow_change_plan, Plan) -> OldCashFlow = ff_cash_flow_codec:marshal(final_cash_flow, maps:get(old_cash_flow_inverted, Plan)), @@ -67,14 +70,22 @@ marshal(domain_revision_change_plan, Plan) -> #wthd_adj_DataRevisionChangePlan{ new_domain_revision = ff_codec:marshal(domain_revision, maps:get(new_domain_revision, Plan)) }; +marshal(body_change_plan, Plan) -> + #wthd_adj_BodyChangePlan{ + new_body = marshal(cash, maps:get(new_body, Plan)) + }; marshal(change_request, {change_status, Status}) -> {change_status, #wthd_adj_ChangeStatusRequest{ new_status = ff_withdrawal_status_codec:marshal(status, Status) }}; marshal(change_request, {change_cash_flow, DomainRevision}) -> - {change_status, #wthd_adj_ChangeCashFlowRequest{ + {change_cash_flow, #wthd_adj_ChangeCashFlowRequest{ domain_revision = ff_codec:marshal(domain_revision, DomainRevision) }}; +marshal(change_request, {change_body, NewBody}) -> + {change_body, #wthd_adj_ChangeBodyRequest{ + new_body = marshal(cash, NewBody) + }}; marshal(T, V) -> ff_codec:marshal(T, V). @@ -111,7 +122,8 @@ unmarshal(changes_plan, Plan) -> new_status => maybe_unmarshal(status_change_plan, Plan#wthd_adj_ChangesPlan.new_status), new_domain_revision => maybe_unmarshal( domain_revision_change_plan, Plan#wthd_adj_ChangesPlan.new_domain_revision - ) + ), + new_body => maybe_unmarshal(body_change_plan, Plan#wthd_adj_ChangesPlan.new_body) }); unmarshal(cash_flow_change_plan, Plan) -> OldCashFlow = Plan#wthd_adj_CashFlowChangePlan.old_cash_flow_inverted, @@ -130,12 +142,18 @@ unmarshal(domain_revision_change_plan, Plan) -> #{ new_domain_revision => ff_codec:unmarshal(domain_revision, DomainRevision) }; +unmarshal(body_change_plan, Plan) -> + #{ + new_body => unmarshal(cash, Plan#wthd_adj_BodyChangePlan.new_body) + }; unmarshal(change_request, {change_status, Request}) -> Status = Request#wthd_adj_ChangeStatusRequest.new_status, {change_status, ff_withdrawal_status_codec:unmarshal(status, Status)}; unmarshal(change_request, {change_cash_flow, Request}) -> DomainRevision = Request#wthd_adj_ChangeCashFlowRequest.domain_revision, {change_cash_flow, ff_codec:unmarshal(domain_revision, DomainRevision)}; +unmarshal(change_request, {change_body, Request}) -> + {change_body, unmarshal(cash, Request#wthd_adj_ChangeBodyRequest.new_body)}; unmarshal(T, V) -> ff_codec:unmarshal(T, V). @@ -197,7 +215,8 @@ adjustment_codec_test() -> new_status => #{ new_status => succeeded }, - new_domain_revision => #{new_domain_revision => 123} + new_domain_revision => #{new_domain_revision => 123}, + new_body => #{new_body => {50, <<"RUB">>}} }, Adjustment = #{ @@ -220,6 +239,16 @@ adjustment_codec_test() -> {p_transfer, {created, Transfer}}, {status_changed, pending} ], - ?assertEqual(Changes, [unmarshal(change, marshal(change, C)) || C <- Changes]). + ?assertEqual(Changes, [unmarshal(change, marshal(change, C)) || C <- Changes]), + + ChangeRequests = [ + {change_status, succeeded}, + {change_cash_flow, 123}, + {change_body, {50, <<"RUB">>}} + ], + ?assertEqual( + ChangeRequests, + [unmarshal(change_request, marshal(change_request, R)) || R <- ChangeRequests] + ). -endif. diff --git a/apps/ff_server/src/ff_withdrawal_handler.erl b/apps/ff_server/src/ff_withdrawal_handler.erl index 2588f39c..a4e6022f 100644 --- a/apps/ff_server/src/ff_withdrawal_handler.erl +++ b/apps/ff_server/src/ff_withdrawal_handler.erl @@ -176,5 +176,13 @@ handle_function_('CreateAdjustment', {ID, MarshaledParams}, _Opts) -> {error, {invalid_cash_flow_change, {already_has_domain_revision, DomainRevision}}} -> woody_error:raise(business, #wthd_AlreadyHasDataRevision{ domain_revision = ff_withdrawal_codec:marshal(domain_revision, DomainRevision) + }); + {error, {invalid_body_change, {already_has_body, Body}}} -> + woody_error:raise(business, #wthd_AlreadyHasBody{ + body = ff_codec:marshal(cash, Body) + }); + {error, {invalid_body_change, {invalid_operation_amount, Body}}} -> + woody_error:raise(business, #fistful_InvalidOperationAmount{ + amount = ff_codec:marshal(cash, Body) }) end. diff --git a/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl b/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl index 2378e12e..f7b65406 100644 --- a/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl +++ b/apps/ff_server/test/ff_withdrawal_handler_SUITE.erl @@ -44,6 +44,9 @@ -export([create_adjustment_unavailable_status_error_test/1]). -export([create_adjustment_already_has_status_error_test/1]). -export([create_adjustment_already_has_data_revision_error_test/1]). +-export([create_adjustment_already_has_body_error_test/1]). +-export([create_adjustment_invalid_operation_amount_error_test/1]). +-export([create_adjustment_change_body_ok_test/1]). -export([withdrawal_state_content_test/1]). -export([trace_withdrawal_test/1]). -export([create_withdrawal_with_changed_body_test/1]). @@ -92,6 +95,9 @@ groups() -> create_adjustment_unavailable_status_error_test, create_adjustment_already_has_status_error_test, create_adjustment_already_has_data_revision_error_test, + create_adjustment_already_has_body_error_test, + create_adjustment_invalid_operation_amount_error_test, + create_adjustment_change_body_ok_test, withdrawal_state_content_test ]} ]. @@ -844,6 +850,73 @@ create_adjustment_already_has_data_revision_error_test(_C) -> }, ?assertEqual({exception, ExpectedError}, Result). +-spec create_adjustment_already_has_body_error_test(config()) -> test_return(). +create_adjustment_already_has_body_error_test(_C) -> + #{ + withdrawal_id := WithdrawalID + } = ct_objects:prepare_standard_environment(ct_objects:build_default_ctx()), + Body = make_cash({100, <<"RUB">>}), + Params = #wthd_adj_AdjustmentParams{ + id = genlib:bsuuid(), + change = + {change_body, #wthd_adj_ChangeBodyRequest{ + new_body = Body + }} + }, + Result = call_withdrawal('CreateAdjustment', {WithdrawalID, Params}), + ExpectedError = #wthd_AlreadyHasBody{ + body = Body + }, + ?assertEqual({exception, ExpectedError}, Result). + +-spec create_adjustment_invalid_operation_amount_error_test(config()) -> test_return(). +create_adjustment_invalid_operation_amount_error_test(_C) -> + #{ + withdrawal_id := WithdrawalID + } = ct_objects:prepare_standard_environment(ct_objects:build_default_ctx()), + Body = make_cash({150, <<"RUB">>}), + Params = #wthd_adj_AdjustmentParams{ + id = genlib:bsuuid(), + change = + {change_body, #wthd_adj_ChangeBodyRequest{ + new_body = Body + }} + }, + Result = call_withdrawal('CreateAdjustment', {WithdrawalID, Params}), + ExpectedError = #fistful_InvalidOperationAmount{ + amount = Body + }, + ?assertEqual({exception, ExpectedError}, Result). + +-spec create_adjustment_change_body_ok_test(config()) -> test_return(). +create_adjustment_change_body_ok_test(_C) -> + #{ + withdrawal_id := WithdrawalID + } = ct_objects:prepare_standard_environment(ct_objects:build_default_ctx()), + AdjustmentID = genlib:bsuuid(), + NewBody = make_cash({50, <<"RUB">>}), + Params = #wthd_adj_AdjustmentParams{ + id = AdjustmentID, + change = + {change_body, #wthd_adj_ChangeBodyRequest{ + new_body = NewBody + }} + }, + {ok, AdjustmentState} = call_withdrawal('CreateAdjustment', {WithdrawalID, Params}), + ExpectedAdjustment = get_adjustment(WithdrawalID, AdjustmentID), + ?assertEqual(AdjustmentID, AdjustmentState#wthd_adj_AdjustmentState.id), + ?assertEqual( + ff_withdrawal_adjustment_codec:marshal(changes_plan, ff_adjustment:changes_plan(ExpectedAdjustment)), + AdjustmentState#wthd_adj_AdjustmentState.changes_plan + ), + ?assertMatch( + #wthd_adj_ChangesPlan{ + new_body = #wthd_adj_BodyChangePlan{new_body = NewBody}, + new_cash_flow = #wthd_adj_CashFlowChangePlan{} + }, + AdjustmentState#wthd_adj_AdjustmentState.changes_plan + ). + -spec withdrawal_state_content_test(config()) -> test_return(). withdrawal_state_content_test(_C) -> #{ diff --git a/apps/ff_transfer/src/ff_adjustment.erl b/apps/ff_transfer/src/ff_adjustment.erl index 82439cac..8a544741 100644 --- a/apps/ff_transfer/src/ff_adjustment.erl +++ b/apps/ff_transfer/src/ff_adjustment.erl @@ -27,7 +27,8 @@ -type changes() :: #{ new_cash_flow => cash_flow_change(), new_status => status_change(), - new_domain_revision => #{new_domain_revision := domain_revision()} + new_domain_revision => #{new_domain_revision := domain_revision()}, + new_body => body_change() }. -type cash_flow_change() :: #{ @@ -39,6 +40,10 @@ new_status := target_status() }. +-type body_change() :: #{ + new_body := body() +}. + -type status() :: pending | succeeded. @@ -56,6 +61,7 @@ -export_type([changes/0]). -export_type([cash_flow_change/0]). -export_type([status_change/0]). +-export_type([body_change/0]). -export_type([status/0]). -export_type([adjustment/0]). -export_type([params/0]). @@ -90,6 +96,7 @@ %% Internal types -type target_status() :: term(). +-type body() :: ff_cash:cash(). -type final_cash_flow() :: ff_cash_flow:final_cash_flow(). -type p_transfer() :: ff_postings_transfer:transfer(). -type action() :: prg_action:t(). diff --git a/apps/ff_transfer/src/ff_withdrawal.erl b/apps/ff_transfer/src/ff_withdrawal.erl index 880542f5..5e36f2f5 100644 --- a/apps/ff_transfer/src/ff_withdrawal.erl +++ b/apps/ff_transfer/src/ff_withdrawal.erl @@ -166,7 +166,8 @@ -type adjustment_change() :: {change_status, status()} - | {change_cash_flow, domain_revision()}. + | {change_cash_flow, domain_revision()} + | {change_body, body()}. -type start_adjustment_error() :: invalid_withdrawal_status_error() @@ -175,6 +176,7 @@ | {invalid_cash_flow_change, {already_has_domain_revision, domain_revision()} | {unavailable_status, status()}} + | invalid_body_change_error() | ff_adjustment:create_error(). -type unknown_adjustment_error() :: ff_adjustment_utils:unknown_adjustment_error(). @@ -186,6 +188,10 @@ -type invalid_withdrawal_status_error() :: {invalid_withdrawal_status, status()}. +-type invalid_body_change_error() :: + {invalid_body_change, {already_has_body, body()}} + | {invalid_body_change, {invalid_operation_amount, body()}}. + -type action() :: prg_action:t(). -export_type([withdrawal/0]). @@ -629,8 +635,13 @@ is_current_session(SessionID, Withdrawal) -> %% Internals -body_changed_events(#{new_body := NewBody}, #{body := OldBody}) -> - [{body_changed, #{new_body => NewBody, old_body => OldBody}}]; +body_changed_events(#{new_body := NewBody}, Withdrawal) -> + case final_body(Withdrawal) of + NewBody -> + []; + OldBody -> + [{body_changed, #{new_body => NewBody, old_body => OldBody}}] + end; body_changed_events(_, _) -> []. @@ -770,11 +781,13 @@ do_process_transfer(p_transfer_start, Withdrawal) -> do_process_transfer(p_transfer_recreate, Withdrawal) -> %% route limits already rolled back when p_transfer was cancelled %% new route limits with new amount are required + WithdrawalWithNewBody = Withdrawal#{body => final_body(Withdrawal)}, Route = route(Withdrawal), DomainRevision = final_domain_revision(Withdrawal), IterWithMultiplication = ff_withdrawal_route_attempt_utils:get_index(attempts(Withdrawal)) * ?BODY_CHANGED_ATTEMPT_MULTIPLIER, - {Varset, _Context} = make_routing_varset_and_context(Withdrawal), + %% terms/limits selectors may depend on cost — use the changed body + {Varset, _Context} = make_routing_varset_and_context(WithdrawalWithNewBody), {ok, #domain_ProvisionTermSet{ wallet = #domain_WalletProvisionTerms{ withdrawals = #domain_WithdrawalProvisionTerms{ @@ -783,7 +796,6 @@ do_process_transfer(p_transfer_recreate, Withdrawal) -> } }} = compute_provider_terminal_terms(Route, Varset, DomainRevision), TurnoverLimits = ff_limiter:get_turnover_limits(SelectorTurnoverLimits), - WithdrawalWithNewBody = Withdrawal#{body => final_body(Withdrawal)}, ok = ff_limiter:hold_withdrawal_limits(TurnoverLimits, WithdrawalWithNewBody, Route, IterWithMultiplication), process_p_transfer_creation(Withdrawal); do_process_transfer(p_transfer_prepare, Withdrawal) -> @@ -1544,7 +1556,8 @@ validate_adjustment_start(Params, Withdrawal) -> valid = unwrap(validate_no_pending_adjustment(Withdrawal)), valid = unwrap(validate_withdrawal_finish(Withdrawal)), valid = unwrap(validate_status_change(Params, Withdrawal)), - valid = unwrap(validate_domain_revision_change(Params, Withdrawal)) + valid = unwrap(validate_domain_revision_change(Params, Withdrawal)), + valid = unwrap(validate_body_change(Params, Withdrawal)) end). -spec validate_withdrawal_finish(withdrawal_state()) -> @@ -1635,6 +1648,28 @@ validate_current_status({failed, _Failure} = Status) -> validate_current_status(Status) -> {error, {unavailable_status, Status}}. +-spec validate_body_change(adjustment_params(), withdrawal_state()) -> + {ok, valid} + | {error, invalid_body_change_error()}. +validate_body_change(#{change := {change_body, NewBody}}, Withdrawal) -> + do(fun() -> + valid = unwrap(invalid_body_change, validate_new_body(NewBody, final_body(Withdrawal))) + end); +validate_body_change(_Params, _Withdrawal) -> + {ok, valid}. + +-spec validate_new_body(body(), body()) -> + {ok, valid} + | {error, {already_has_body, body()} | {invalid_operation_amount, body()}}. +validate_new_body({Amount, Currency}, {Amount, Currency}) -> + {error, {already_has_body, {Amount, Currency}}}; +validate_new_body({NewAmount, Currency}, {OldAmount, Currency}) when + is_integer(NewAmount), NewAmount > 0, NewAmount < OldAmount +-> + {ok, valid}; +validate_new_body(NewBody, _OldBody) -> + {error, {invalid_operation_amount, NewBody}}. + %% Adjustment helpers -spec apply_adjustment_event(wrapped_adjustment_event(), withdrawal_state()) -> withdrawal_state(). @@ -1665,7 +1700,9 @@ make_adjustment_change({change_status, NewStatus}, Withdrawal) -> CurrentStatus = status(Withdrawal), make_change_status_params(CurrentStatus, NewStatus, Withdrawal); make_adjustment_change({change_cash_flow, NewDomainRevision}, Withdrawal) -> - make_change_cash_flow_params(NewDomainRevision, Withdrawal). + make_change_cash_flow_params(NewDomainRevision, Withdrawal); +make_adjustment_change({change_body, NewBody}, Withdrawal) -> + make_change_body_params(NewBody, Withdrawal). -spec make_change_status_params(status(), status(), withdrawal_state()) -> ff_adjustment:changes(). make_change_status_params(succeeded, {failed, _} = NewStatus, Withdrawal) -> @@ -1712,6 +1749,27 @@ make_change_cash_flow_params(NewDomainRevision, Withdrawal) -> } }. +-spec make_change_body_params(body(), withdrawal_state()) -> ff_adjustment:changes(). +make_change_body_params(NewBody, #{status := {failed, _}}) -> + #{ + new_body => #{ + new_body => NewBody + } + }; +make_change_body_params(NewBody, #{status := succeeded} = Withdrawal) -> + CurrentCashFlow = effective_final_cash_flow(Withdrawal), + %% body_changed is not applied yet; inject new body before CF recalculation + NewCashFlow = make_final_cash_flow(Withdrawal#{new_body => NewBody}), + #{ + new_body => #{ + new_body => NewBody + }, + new_cash_flow => #{ + old_cash_flow_inverted => ff_cash_flow:inverse(CurrentCashFlow), + new_cash_flow => NewCashFlow + } + }. + -spec process_adjustment(withdrawal_state()) -> process_result(). process_adjustment(Withdrawal) -> #{ @@ -1719,7 +1777,7 @@ process_adjustment(Withdrawal) -> events := Events0, changes := Changes } = ff_adjustment_utils:process_adjustments(adjustments_index(Withdrawal)), - Events1 = Events0 ++ handle_adjustment_changes(Changes), + Events1 = Events0 ++ handle_adjustment_changes(Changes, Withdrawal), handle_child_result({Action, Events1}, Withdrawal). -spec process_route_change(withdrawal_state(), fail_type()) -> process_result(). @@ -1818,10 +1876,10 @@ do_process_route_change(Routes, Withdrawal, Reason) -> {timeout, Events} end. --spec handle_adjustment_changes(ff_adjustment:changes()) -> [event()]. -handle_adjustment_changes(Changes) -> - StatusChange = maps:get(new_status, Changes, undefined), - handle_adjustment_status_change(StatusChange). +-spec handle_adjustment_changes(ff_adjustment:changes(), withdrawal_state()) -> [event()]. +handle_adjustment_changes(Changes, Withdrawal) -> + handle_adjustment_status_change(maps:get(new_status, Changes, undefined)) ++ + handle_adjustment_body_change(maps:get(new_body, Changes, undefined), Withdrawal). -spec handle_adjustment_status_change(ff_adjustment:status_change() | undefined) -> [event()]. handle_adjustment_status_change(undefined) -> @@ -1829,6 +1887,17 @@ handle_adjustment_status_change(undefined) -> handle_adjustment_status_change(#{new_status := Status}) -> [{status_changed, Status}]. +-spec handle_adjustment_body_change(ff_adjustment:body_change() | undefined, withdrawal_state()) -> [event()]. +handle_adjustment_body_change(undefined, _Withdrawal) -> + []; +handle_adjustment_body_change(#{new_body := NewBody}, Withdrawal) -> + case final_body(Withdrawal) of + NewBody -> + []; + OldBody -> + [{body_changed, #{old_body => OldBody, new_body => NewBody}}] + end. + -spec save_adjustable_info(event(), withdrawal_state()) -> withdrawal_state(). save_adjustable_info({p_transfer, {status_changed, committed}}, Withdrawal) -> CashFlow = ff_postings_transfer:final_cash_flow(p_transfer(Withdrawal)), @@ -1911,8 +1980,8 @@ apply_event_({p_transfer, Ev}, T) -> T1 end; apply_event_({session_started, SessionID}, T0) -> - %% clear new_body from previous session - T = maps:without([new_body], T0), + %% clear body-change leftovers from a previous session before starting a new one + T = maps:without([new_body, p_transfer_rebuild_needed], T0), Session = #{id => SessionID}, Attempts = attempts(T), R = ff_withdrawal_route_attempt_utils:update_current_session(Session, Attempts), diff --git a/apps/ff_transfer/src/ff_withdrawal_session.erl b/apps/ff_transfer/src/ff_withdrawal_session.erl index 739d8355..fab233e8 100644 --- a/apps/ff_transfer/src/ff_withdrawal_session.erl +++ b/apps/ff_transfer/src/ff_withdrawal_session.erl @@ -215,7 +215,8 @@ process_session(#{status := {finished, _}, id := ID, result := Result, withdrawa WithdrawalID = ff_adapter_withdrawal:id(Withdrawal), SessionData = genlib_map:compact(#{ result => Result, - new_body => maps:get(new_body, St, undefined) + %% propagate body change only on success; failed session may retry another route + new_body => session_new_body_for_notify(Result, St) }), case ff_withdrawal_machine:notify(WithdrawalID, {session_finished, ID, SessionData}) of ok -> @@ -254,21 +255,25 @@ process_transaction_info(#{transaction_info := TrxInfo}, Events, SessionState) - process_transaction_info(_, Events, _Session) -> Events. -process_new_body(#{new_body := NewBody}, Events, #{new_body := PreviousBody} = _SessionState) when - NewBody =:= PreviousBody --> - %% body already updated - Events; -process_new_body(#{new_body := NewBody}, Events, #{withdrawal := #{cash := OldBody}} = _SessionState) -> - case validate_new_body(OldBody, NewBody) of +process_new_body(#{new_body := NewBody}, Events, #{withdrawal := #{cash := OriginalBody}} = SessionState) -> + %% validate against current effective body (previous change or original cash) + CurrentBody = maps:get(new_body, SessionState, OriginalBody), + case validate_new_body(CurrentBody, NewBody) of ok -> - Events ++ [{body_changed, #{old_body => OldBody, new_body => NewBody}}]; + Events ++ [{body_changed, #{old_body => CurrentBody, new_body => NewBody}}]; unchanged -> Events end; process_new_body(_Result, Events, _SessionState) -> Events. +session_new_body_for_notify(success, St) -> + maps:get(new_body, St, undefined); +session_new_body_for_notify({success, _TransactionInfo}, St) -> + maps:get(new_body, St, undefined); +session_new_body_for_notify(_FailedResult, _St) -> + undefined. + validate_new_body({Amount, Currency}, {Amount, Currency}) -> unchanged; validate_new_body({OldAmount, Currency}, {NewAmount, Currency}) when @@ -424,3 +429,36 @@ create_adapter_withdrawal( -spec set_callbacks_index(callbacks_index(), session_state()) -> session_state(). set_callbacks_index(Callbacks, Session) -> Session#{callbacks => Callbacks}. + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +-spec test() -> _. + +-spec session_new_body_for_notify_test() -> _. +session_new_body_for_notify_test() -> + St = #{new_body => {50, <<"RUB">>}}, + ?assertEqual({50, <<"RUB">>}, session_new_body_for_notify(success, St)), + ?assertEqual({50, <<"RUB">>}, session_new_body_for_notify({success, #{id => <<"trx">>}}, St)), + ?assertEqual(undefined, session_new_body_for_notify({failed, #{code => <<"x">>}}, St)). + +-spec process_new_body_validates_against_current_body_test() -> _. +process_new_body_validates_against_current_body_test() -> + SessionState = #{ + withdrawal => #{cash => {100, <<"RUB">>}}, + new_body => {80, <<"RUB">>} + }, + ?assertEqual( + [{body_changed, #{old_body => {80, <<"RUB">>}, new_body => {50, <<"RUB">>}}}], + process_new_body(#{new_body => {50, <<"RUB">>}}, [], SessionState) + ), + ?assertEqual( + [], + process_new_body(#{new_body => {80, <<"RUB">>}}, [], SessionState) + ), + ?assertError( + {invalid_new_body, {90, <<"RUB">>}, {80, <<"RUB">>}}, + process_new_body(#{new_body => {90, <<"RUB">>}}, [], SessionState) + ). + +-endif. diff --git a/apps/ff_transfer/test/ff_withdrawal_adjustment_SUITE.erl b/apps/ff_transfer/test/ff_withdrawal_adjustment_SUITE.erl index e876c504..64d965b6 100644 --- a/apps/ff_transfer/test/ff_withdrawal_adjustment_SUITE.erl +++ b/apps/ff_transfer/test/ff_withdrawal_adjustment_SUITE.erl @@ -29,6 +29,13 @@ -export([adjustment_can_not_change_domain_revision_to_same/1]). -export([adjustment_can_not_change_domain_revision_with_failed_status/1]). -export([adjustment_can_change_domain_revision_test/1]). +-export([adjustment_fail_change_body_succeed_test/1]). +-export([adjustment_can_change_body_on_succeeded_test/1]). +-export([adjustment_change_cash_flow_then_change_body_test/1]). +-export([adjustment_change_body_then_change_cash_flow_test/1]). +-export([adjustment_can_not_change_body_to_same/1]). +-export([adjustment_can_not_increase_body/1]). +-export([adjustment_can_not_change_body_on_pending/1]). %% Internal types @@ -65,10 +72,17 @@ groups() -> no_pending_withdrawal_adjustments_test, unknown_withdrawal_test, adjustment_can_not_change_domain_revision_to_same, - adjustment_can_not_change_domain_revision_with_failed_status + adjustment_can_not_change_domain_revision_with_failed_status, + adjustment_fail_change_body_succeed_test, + adjustment_can_change_body_on_succeeded_test, + adjustment_can_not_change_body_to_same, + adjustment_can_not_increase_body, + adjustment_can_not_change_body_on_pending ]}, {non_parallel, [], [ - adjustment_can_change_domain_revision_test + adjustment_can_change_domain_revision_test, + adjustment_change_cash_flow_then_change_body_test, + adjustment_change_body_then_change_cash_flow_test ]} ]. @@ -378,6 +392,149 @@ adjustment_can_change_domain_revision_test(C) -> ?assertEqual(?FINAL_BALANCE(0, <<"RUB">>), get_wallet_balance(WalletID)), ?assertEqual(?FINAL_BALANCE(80, <<"RUB">>), get_destination_balance(DestinationID)). +-spec adjustment_fail_change_body_succeed_test(config()) -> test_return(). +adjustment_fail_change_body_succeed_test(C) -> + #{ + withdrawal_id := WithdrawalID, + wallet_id := WalletID, + destination_id := DestinationID + } = prepare_standard_environment({100, <<"RUB">>}, C), + ?assertEqual(?FINAL_BALANCE(0, <<"RUB">>), get_wallet_balance(WalletID)), + ?assertEqual(?FINAL_BALANCE(80, <<"RUB">>), get_destination_balance(DestinationID)), + _ = process_adjustment(WithdrawalID, #{ + change => {change_status, {failed, #{code => <<"test">>}}} + }), + ?assertEqual(?FINAL_BALANCE(100, <<"RUB">>), get_wallet_balance(WalletID)), + ?assertEqual(?FINAL_BALANCE(0, <<"RUB">>), get_destination_balance(DestinationID)), + AdjustmentID = process_adjustment(WithdrawalID, #{ + change => {change_body, {50, <<"RUB">>}} + }), + ?assertMatch(succeeded, get_adjustment_status(WithdrawalID, AdjustmentID)), + ?assertEqual({50, <<"RUB">>}, ff_withdrawal:new_body(get_withdrawal(WithdrawalID))), + ?assertMatch({failed, _}, get_withdrawal_status(WithdrawalID)), + ?assertEqual(?FINAL_BALANCE(100, <<"RUB">>), get_wallet_balance(WalletID)), + ?assertEqual(?FINAL_BALANCE(0, <<"RUB">>), get_destination_balance(DestinationID)), + _ = process_adjustment(WithdrawalID, #{ + change => {change_status, succeeded} + }), + ?assertEqual(succeeded, get_withdrawal_status(WithdrawalID)), + ?assertEqual(?FINAL_BALANCE(50, <<"RUB">>), get_wallet_balance(WalletID)), + ?assertEqual(?FINAL_BALANCE(40, <<"RUB">>), get_destination_balance(DestinationID)). + +-spec adjustment_can_change_body_on_succeeded_test(config()) -> test_return(). +adjustment_can_change_body_on_succeeded_test(C) -> + #{ + withdrawal_id := WithdrawalID, + wallet_id := WalletID, + destination_id := DestinationID + } = prepare_standard_environment({100, <<"RUB">>}, C), + ?assertEqual(?FINAL_BALANCE(0, <<"RUB">>), get_wallet_balance(WalletID)), + ?assertEqual(?FINAL_BALANCE(80, <<"RUB">>), get_destination_balance(DestinationID)), + AdjustmentID = process_adjustment(WithdrawalID, #{ + change => {change_body, {50, <<"RUB">>}} + }), + ?assertMatch(succeeded, get_adjustment_status(WithdrawalID, AdjustmentID)), + ?assertEqual({50, <<"RUB">>}, ff_withdrawal:new_body(get_withdrawal(WithdrawalID))), + ?assertEqual(succeeded, get_withdrawal_status(WithdrawalID)), + Plan = ff_adjustment:changes_plan(get_adjustment(WithdrawalID, AdjustmentID)), + ?assertMatch(#{new_body := #{new_body := {50, <<"RUB">>}}, new_cash_flow := #{}}, Plan), + ?assertEqual(?FINAL_BALANCE(50, <<"RUB">>), get_wallet_balance(WalletID)), + ?assertEqual(?FINAL_BALANCE(40, <<"RUB">>), get_destination_balance(DestinationID)). + +-spec adjustment_change_cash_flow_then_change_body_test(config()) -> test_return(). +adjustment_change_cash_flow_then_change_body_test(C) -> + #{ + withdrawal_id := WithdrawalID, + wallet_id := WalletID, + destination_id := DestinationID, + party_id := PartyID + } = prepare_standard_environment({100, <<"RUB">>}, C), + _OtherWalletToChangeDomain = ct_objects:create_wallet( + PartyID, <<"RUB">>, #domain_TermSetHierarchyRef{id = 1}, #domain_PaymentInstitutionRef{id = 1} + ), + _ = process_adjustment(WithdrawalID, #{ + change => {change_cash_flow, ct_domain_config:head()} + }), + ?assertEqual(succeeded, get_withdrawal_status(WithdrawalID)), + AdjustmentID = process_adjustment(WithdrawalID, #{ + change => {change_body, {50, <<"RUB">>}} + }), + ?assertMatch(succeeded, get_adjustment_status(WithdrawalID, AdjustmentID)), + ?assertEqual({50, <<"RUB">>}, ff_withdrawal:new_body(get_withdrawal(WithdrawalID))), + ?assertEqual(?FINAL_BALANCE(50, <<"RUB">>), get_wallet_balance(WalletID)), + ?assertEqual(?FINAL_BALANCE(40, <<"RUB">>), get_destination_balance(DestinationID)). + +-spec adjustment_change_body_then_change_cash_flow_test(config()) -> test_return(). +adjustment_change_body_then_change_cash_flow_test(C) -> + #{ + withdrawal_id := WithdrawalID, + wallet_id := WalletID, + destination_id := DestinationID, + party_id := PartyID + } = prepare_standard_environment({100, <<"RUB">>}, C), + _ = process_adjustment(WithdrawalID, #{ + change => {change_body, {50, <<"RUB">>}} + }), + ?assertEqual({50, <<"RUB">>}, ff_withdrawal:new_body(get_withdrawal(WithdrawalID))), + ?assertEqual(?FINAL_BALANCE(50, <<"RUB">>), get_wallet_balance(WalletID)), + ?assertEqual(?FINAL_BALANCE(40, <<"RUB">>), get_destination_balance(DestinationID)), + _OtherWalletToChangeDomain = ct_objects:create_wallet( + PartyID, <<"RUB">>, #domain_TermSetHierarchyRef{id = 1}, #domain_PaymentInstitutionRef{id = 1} + ), + AdjustmentID = process_adjustment(WithdrawalID, #{ + change => {change_cash_flow, ct_domain_config:head()} + }), + ?assertMatch(succeeded, get_adjustment_status(WithdrawalID, AdjustmentID)), + ?assertEqual({50, <<"RUB">>}, ff_withdrawal:new_body(get_withdrawal(WithdrawalID))), + ?assertEqual(?FINAL_BALANCE(50, <<"RUB">>), get_wallet_balance(WalletID)), + ?assertEqual(?FINAL_BALANCE(40, <<"RUB">>), get_destination_balance(DestinationID)). + +-spec adjustment_can_not_change_body_to_same(config()) -> test_return(). +adjustment_can_not_change_body_to_same(C) -> + #{ + withdrawal_id := WithdrawalID + } = prepare_standard_environment({100, <<"RUB">>}, C), + Result = ff_withdrawal_machine:start_adjustment(WithdrawalID, #{ + id => genlib:bsuuid(), + change => {change_body, {100, <<"RUB">>}} + }), + ?assertMatch({error, {invalid_body_change, {already_has_body, {100, <<"RUB">>}}}}, Result). + +-spec adjustment_can_not_increase_body(config()) -> test_return(). +adjustment_can_not_increase_body(C) -> + #{ + withdrawal_id := WithdrawalID + } = prepare_standard_environment({100, <<"RUB">>}, C), + Result = ff_withdrawal_machine:start_adjustment(WithdrawalID, #{ + id => genlib:bsuuid(), + change => {change_body, {150, <<"RUB">>}} + }), + ?assertMatch({error, {invalid_body_change, {invalid_operation_amount, {150, <<"RUB">>}}}}, Result). + +-spec adjustment_can_not_change_body_on_pending(config()) -> test_return(). +adjustment_can_not_change_body_on_pending(C) -> + #{ + wallet_id := WalletID, + destination_id := DestinationID, + party_id := PartyID + } = prepare_standard_environment({100, <<"RUB">>}, C), + {ok, Events0} = ff_withdrawal:create(#{ + id => genlib:bsuuid(), + wallet_id => WalletID, + destination_id => DestinationID, + party_id => PartyID, + body => {100, <<"RUB">>} + }), + Withdrawal1 = lists:foldl(fun ff_withdrawal:apply_event/2, undefined, Events0), + Result = ff_withdrawal:start_adjustment( + #{ + id => genlib:bsuuid(), + change => {change_body, {50, <<"RUB">>}} + }, + Withdrawal1 + ), + ?assertMatch({error, {invalid_withdrawal_status, pending}}, Result). + %% Utils prepare_standard_environment({_Amount, Currency} = WithdrawalCash, _C) -> diff --git a/elvis.config b/elvis.config index bff14eb7..666bef17 100644 --- a/elvis.config +++ b/elvis.config @@ -115,7 +115,8 @@ hg_invoice_helper, hg_invoice_tests_SUITE, hg_invoice_template_tests_SUITE, - hg_direct_recurrent_tests_SUITE + hg_direct_recurrent_tests_SUITE, + ff_withdrawal_adjustment_SUITE ] }}, {elvis_style, no_debug_call, #{}}, diff --git a/rebar.config b/rebar.config index e51f793a..ca9aa313 100644 --- a/rebar.config +++ b/rebar.config @@ -49,7 +49,8 @@ {herd, {git, "https://github.com/wgnet/herd.git", {tag, "1.3.4"}}}, {progressor, {git, "https://github.com/valitydev/progressor.git", {tag, "v1.0.25"}}}, {machinery, {git, "https://github.com/valitydev/machinery-erlang.git", {tag, "v1.1.22"}}}, - {fistful_proto, {git, "https://github.com/valitydev/fistful-proto.git", {tag, "v2.0.3"}}}, + {fistful_proto, + {git, "https://github.com/valitydev/fistful-proto.git", {branch, "XYZ-327/ft/withdrawal_cash_adj"}}}, {binbase_proto, {git, "https://github.com/valitydev/binbase-proto.git", {branch, "master"}}}, {validator_personal_data_proto, {git, "https://github.com/valitydev/validator-personal-data-proto.git", {branch, "master"}}}, diff --git a/rebar.lock b/rebar.lock index cd3326da..7ec2d36a 100644 --- a/rebar.lock +++ b/rebar.lock @@ -69,7 +69,7 @@ 0}, {<<"fistful_proto">>, {git,"https://github.com/valitydev/fistful-proto.git", - {ref,"f67259f5e1a3aaefe189bc74e9d7fefd2eef49b2"}}, + {ref,"e1ad3bc3281a9379a1eea30daffeeb7f309fdc3e"}}, 0}, {<<"genlib">>, {git,"https://github.com/valitydev/genlib.git", From c9061e19aa85395bb26a2530c49ca4d13f56d508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 23 Jul 2026 11:50:01 +0300 Subject: [PATCH 11/12] bumped fistful_proto --- rebar.config | 2 +- rebar.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/rebar.config b/rebar.config index ca9aa313..c1d4eb8c 100644 --- a/rebar.config +++ b/rebar.config @@ -50,7 +50,7 @@ {progressor, {git, "https://github.com/valitydev/progressor.git", {tag, "v1.0.25"}}}, {machinery, {git, "https://github.com/valitydev/machinery-erlang.git", {tag, "v1.1.22"}}}, {fistful_proto, - {git, "https://github.com/valitydev/fistful-proto.git", {branch, "XYZ-327/ft/withdrawal_cash_adj"}}}, + {git, "https://github.com/valitydev/fistful-proto.git", {branch, "master"}}}, {binbase_proto, {git, "https://github.com/valitydev/binbase-proto.git", {branch, "master"}}}, {validator_personal_data_proto, {git, "https://github.com/valitydev/validator-personal-data-proto.git", {branch, "master"}}}, diff --git a/rebar.lock b/rebar.lock index 7ec2d36a..f46a494f 100644 --- a/rebar.lock +++ b/rebar.lock @@ -69,7 +69,7 @@ 0}, {<<"fistful_proto">>, {git,"https://github.com/valitydev/fistful-proto.git", - {ref,"e1ad3bc3281a9379a1eea30daffeeb7f309fdc3e"}}, + {ref,"94b75cef84a6e798887a7e20a66674ce24f3111e"}}, 0}, {<<"genlib">>, {git,"https://github.com/valitydev/genlib.git", From a99d422411ccd1a1c14fbbac7ddad8ad676cce92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Thu, 23 Jul 2026 12:03:08 +0300 Subject: [PATCH 12/12] fixed --- rebar.config | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/rebar.config b/rebar.config index c1d4eb8c..b074aa78 100644 --- a/rebar.config +++ b/rebar.config @@ -49,8 +49,7 @@ {herd, {git, "https://github.com/wgnet/herd.git", {tag, "1.3.4"}}}, {progressor, {git, "https://github.com/valitydev/progressor.git", {tag, "v1.0.25"}}}, {machinery, {git, "https://github.com/valitydev/machinery-erlang.git", {tag, "v1.1.22"}}}, - {fistful_proto, - {git, "https://github.com/valitydev/fistful-proto.git", {branch, "master"}}}, + {fistful_proto, {git, "https://github.com/valitydev/fistful-proto.git", {branch, "master"}}}, {binbase_proto, {git, "https://github.com/valitydev/binbase-proto.git", {branch, "master"}}}, {validator_personal_data_proto, {git, "https://github.com/valitydev/validator-personal-data-proto.git", {branch, "master"}}},