From 527084a7820cf63bd4b400a94bb3e7a5dda75894 Mon Sep 17 00:00:00 2001 From: ttt161 Date: Fri, 19 Jun 2026 10:40:16 +0300 Subject: [PATCH 1/8] base implementation for multicurrency payments --- apps/hellgate/include/hg_invoice_payment.hrl | 3 +- apps/hellgate/include/payment_events.hrl | 8 + apps/hellgate/src/hellgate.app.src | 1 + apps/hellgate/src/hg_accounting.erl | 7 + apps/hellgate/src/hg_cashflow.erl | 2 + apps/hellgate/src/hg_cashflow_utils.erl | 60 +++++- apps/hellgate/src/hg_currency_converter.erl | 39 ++++ apps/hellgate/src/hg_exrates.erl | 42 ++++ apps/hellgate/src/hg_invoice_payment.erl | 183 ++++++++++++++++-- .../src/hg_invoice_payment_chargeback.erl | 26 ++- .../src/hg_invoice_payment_refund.erl | 61 ++++-- .../src/hg_invoice_registered_payment.erl | 2 + apps/hellgate/src/hg_invoice_repair.erl | 1 + apps/hellgate/test/hg_dummy_exrates.erl | 31 +++ apps/hg_proto/src/hg_proto.erl | 4 +- config/sys.config | 3 +- rebar.config | 4 +- rebar.lock | 6 +- 18 files changed, 443 insertions(+), 40 deletions(-) create mode 100644 apps/hellgate/src/hg_currency_converter.erl create mode 100644 apps/hellgate/src/hg_exrates.erl create mode 100644 apps/hellgate/test/hg_dummy_exrates.erl diff --git a/apps/hellgate/include/hg_invoice_payment.hrl b/apps/hellgate/include/hg_invoice_payment.hrl index 78eb97a7..0997d164 100644 --- a/apps/hellgate/include/hg_invoice_payment.hrl +++ b/apps/hellgate/include/hg_invoice_payment.hrl @@ -32,7 +32,8 @@ allocation :: undefined | hg_allocation:allocation(), route_limits = #{} :: hg_routing:limits(), route_scores = #{} :: hg_routing:scores(), - shop_limit_status = undefined :: undefined | initialized | finalized + shop_limit_status = undefined :: undefined | initialized | finalized, + exchange_context :: undefined | hg_invoice_payment:exchange_context() }). -record(refund_st, { diff --git a/apps/hellgate/include/payment_events.hrl b/apps/hellgate/include/payment_events.hrl index 5c0962b0..4dba0b36 100644 --- a/apps/hellgate/include/payment_events.hrl +++ b/apps/hellgate/include/payment_events.hrl @@ -76,6 +76,14 @@ } ). +-define(payment_currency_changed(SourceCurrency, DestinationCurrency, ExchangeRate), + {invoice_payment_currency_changed, #payproc_InvoicePaymentCurrencyChanged{ + source_currency = SourceCurrency, + destination_currency = DestinationCurrency, + exchange_rate = ExchangeRate + }} +). + -define(cash_flow_changed(CashFlow), {invoice_payment_cash_flow_changed, #payproc_InvoicePaymentCashFlowChanged{ cash_flow = CashFlow diff --git a/apps/hellgate/src/hellgate.app.src b/apps/hellgate/src/hellgate.app.src index d5b33e3e..fb17556b 100644 --- a/apps/hellgate/src/hellgate.app.src +++ b/apps/hellgate/src/hellgate.app.src @@ -12,6 +12,7 @@ progressor, hg_progressor, hg_proto, + exrates_proto, routing, cowboy, prometheus, diff --git a/apps/hellgate/src/hg_accounting.erl b/apps/hellgate/src/hg_accounting.erl index 189db6ad..8b1bd6d1 100644 --- a/apps/hellgate/src/hg_accounting.erl +++ b/apps/hellgate/src/hg_accounting.erl @@ -16,6 +16,7 @@ -export([collect_provider_account_map/4]). -export([collect_system_account_map/4]). -export([collect_external_account_map/4]). +-export([empty_log/0]). -export([hold/2]). -export([plan/2]). @@ -74,6 +75,12 @@ max_available_amount => amount() }. +-spec empty_log() -> posting_plan_log(). +empty_log() -> + #'accounter_PostingPlanLog'{ + affected_accounts = #{} + }. + -spec get_account(account_id()) -> account(). get_account(AccountID) -> Account = do_get_account(AccountID), diff --git a/apps/hellgate/src/hg_cashflow.erl b/apps/hellgate/src/hg_cashflow.erl index d5f8631b..c0a18bc3 100644 --- a/apps/hellgate/src/hg_cashflow.erl +++ b/apps/hellgate/src/hg_cashflow.erl @@ -12,6 +12,8 @@ -include_lib("damsel/include/dmsl_domain_thrift.hrl"). -export_type([final_cash_flow/0]). +-export_type([cash_flow/0]). +-export_type([cash_volume/0]). -type account() :: dmsl_domain_thrift:'CashFlowAccount'(). -type account_id() :: dmsl_domain_thrift:'AccountID'(). diff --git a/apps/hellgate/src/hg_cashflow_utils.erl b/apps/hellgate/src/hg_cashflow_utils.erl index 387f25e4..6063a0c3 100644 --- a/apps/hellgate/src/hg_cashflow_utils.erl +++ b/apps/hellgate/src/hg_cashflow_utils.erl @@ -20,13 +20,16 @@ revision := revision(), merchant_terms => dmsl_domain_thrift:'PaymentsServiceTerms'(), refund => refund(), - allocation => hg_allocation:allocation() + allocation => hg_allocation:allocation(), + exchange_context => hg_invoice_payment:exchange_context() | undefined }. -export_type([cash_flow_context/0]). -export([collect_cashflow/1]). -export([collect_cashflow/2]). +-export([convert_cashflow/2]). +-export([convert_volume/2]). -type party() :: dmsl_domain_thrift:'PartyConfig'(). -type party_config_ref() :: dmsl_domain_thrift:'PartyConfigRef'(). @@ -39,6 +42,8 @@ -type revision() :: hg_domain:revision(). -type payment_institution() :: hg_payment_institution:t(). -type final_cash_flow() :: hg_cashflow:final_cash_flow(). +-type cash_flow() :: hg_cashflow:cash_flow(). +-type cash_volume() :: hg_cashflow:cash_volume(). -spec collect_cashflow(cash_flow_context()) -> final_cash_flow(). collect_cashflow(#{shop := {_, Shop}, varset := VS, revision := Revision} = Context) -> @@ -116,10 +121,61 @@ construct_transaction_cashflow( construct_provider_cashflow(PaymentInstitution, #{provision_terms := ProvisionTerms} = Context) -> ProviderCashflowSelector = get_provider_cashflow_selector(ProvisionTerms), - ProviderCashflow = get_selector_value(provider_payment_cash_flow, ProviderCashflowSelector), + ProviderCashflow0 = get_selector_value(provider_payment_cash_flow, ProviderCashflowSelector), + ExchangeContext = maps:get(exchange_context, Context, undefined), + ProviderCashflow = maybe_convert_cashflow(ExchangeContext, ProviderCashflow0), AccountMap = hg_accounting:collect_account_map(make_collect_account_context(PaymentInstitution, Context)), construct_final_cashflow(ProviderCashflow, #{operation_amount => get_amount(Context)}, AccountMap). +maybe_convert_cashflow(undefined, ProviderCashflow) -> + ProviderCashflow; +maybe_convert_cashflow(ExchangeContext, ProviderCashflow) -> + convert_cashflow(ExchangeContext, ProviderCashflow). + +-spec convert_cashflow(hg_invoice_payment:exchange_context(), cash_flow()) -> cash_flow(). +convert_cashflow(ExchangeContext, ProviderCashflow) -> + lists:foldr( + fun(#domain_CashFlowPosting{volume = CashVolume} = P, Acc) -> + [P#domain_CashFlowPosting{volume = convert_volume(ExchangeContext, CashVolume)} | Acc] + end, + [], + ProviderCashflow + ). + +-spec convert_volume(hg_invoice_payment:exchange_context(), cash_volume()) -> cash_volume(). +convert_volume(_ExchangeContext, {share, _} = CashVolume) -> + CashVolume; +convert_volume(ExchangeContext, {product, {Kind, CashVolumeList}}) -> + {product, {Kind, convert_volumes(ExchangeContext, CashVolumeList)}}; +convert_volume( + #{source := PaymentCurrency, destination := TerminalCurrency} = ExchangeContext, + {fixed, #domain_CashVolumeFixed{ + cash = + #domain_Cash{ + currency = #domain_CurrencyRef{symbolic_code = FeeCurrency} + } = Cash + }} = CashVolume +) -> + case FeeCurrency of + PaymentCurrency -> + CashVolume; + TerminalCurrency -> + %% reverse conversion needed + ReConvertedCash = hg_currency_converter:reverse_convert_cash(ExchangeContext, Cash), + {fixed, #domain_CashVolumeFixed{ + cash = ReConvertedCash + }} + end. + +convert_volumes(ExchangeContext, CashVolumeList) -> + lists:foldr( + fun(CashVolume, Acc) -> + [convert_volume(ExchangeContext, CashVolume) | Acc] + end, + [], + CashVolumeList + ). + construct_final_cashflow(Cashflow, Context, AccountMap) -> hg_cashflow:finalize(Cashflow, Context, AccountMap). diff --git a/apps/hellgate/src/hg_currency_converter.erl b/apps/hellgate/src/hg_currency_converter.erl new file mode 100644 index 00000000..172f2914 --- /dev/null +++ b/apps/hellgate/src/hg_currency_converter.erl @@ -0,0 +1,39 @@ +-module(hg_currency_converter). + +-include_lib("hellgate/include/domain.hrl"). + +-export([convert_cash/2]). +-export([reverse_convert_cash/2]). + +-type cash() :: dmsl_domain_thrift:'Cash'(). + +-spec convert_cash(hg_invoice_payment:exchange_context(), cash()) -> cash(). +convert_cash( + #{source := SourceCurrency, destination := DestinationCurrency, rate := Rate}, + #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = SourceCurrency}} +) -> + %% Example: + %% Amount: 1000, Src: RUB, Dst: USD, Rate: {P=100, Q=1} (1 USD = 100 RUB) + %% ConvertedAmountRational = {1000,100} + %% ConvertedAmount = 10 + #base_Rational{p = P, q = Q} = Rate, + RateRational = genlib_rational:new(P, Q), + AmountRational = genlib_rational:new(Amount), + ConvertedAmountRational = genlib_rational:dvd(AmountRational, RateRational), + Rounding = application:get_env(hellgate, exchange_rounding_method, round_half_away_from_zero), + ConvertedAmount = genlib_rational:round(ConvertedAmountRational, Rounding), + #domain_Cash{amount = ConvertedAmount, currency = #domain_CurrencyRef{symbolic_code = DestinationCurrency}}. + +-spec reverse_convert_cash(hg_invoice_payment:exchange_context(), cash()) -> cash(). +reverse_convert_cash( + #{source := SourceCurrency, destination := DestinationCurrency, rate := Rate}, + #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = DestinationCurrency}} +) -> + %% We do not use two-way exchange rates for currency pairs + #base_Rational{p = P, q = Q} = Rate, + RateRational = genlib_rational:new(P, Q), + AmountRational = genlib_rational:new(Amount), + ReConvertedAmountRational = genlib_rational:mul(AmountRational, RateRational), + Rounding = application:get_env(hellgate, exchange_rounding_method, round_half_away_from_zero), + ReConvertedAmount = genlib_rational:round(ReConvertedAmountRational, Rounding), + #domain_Cash{amount = ReConvertedAmount, currency = #domain_CurrencyRef{symbolic_code = SourceCurrency}}. diff --git a/apps/hellgate/src/hg_exrates.erl b/apps/hellgate/src/hg_exrates.erl new file mode 100644 index 00000000..2d682220 --- /dev/null +++ b/apps/hellgate/src/hg_exrates.erl @@ -0,0 +1,42 @@ +-module(hg_exrates). + +-include_lib("exrates_proto/include/exrates_service_thrift.hrl"). +-include_lib("exrates_proto/include/exrates_base_thrift.hrl"). + +-export([get_exchange_rate/2]). + +-type currency_symbolic_code() :: binary(). +-type rate() :: #{ + p := integer(), + q := integer() +}. + +-define(RATES_SERVICE, rate_boss). + +-spec get_exchange_rate(currency_symbolic_code(), currency_symbolic_code()) -> + {ok, rate()} | {error, _Reason}. +get_exchange_rate(SourceCurrency, DestinationCurrency) -> + Args = #'service_GetCurrencyExchangeRateRequest'{ + currency_data = #'service_CurrencyData'{ + source_currency = SourceCurrency, + destination_currency = DestinationCurrency + } + }, + case issue_call('GetExchangeRateData', {Args}) of + {ok, #'service_GetCurrencyExchangeRateResult'{ + exchange_rate = #base_Rational{p = P, q = Q} + }} -> + {ok, #{p => P, q => Q}}; + {exception, #'service_ExRateNotFound'{}} -> + {error, not_found} + end. + +issue_call(Func, Args) -> + try hg_woody_wrapper:call(?RATES_SERVICE, Func, Args) of + Result -> + Result + catch + error:{woody_error, _ErrorType} = Reason:_St -> + logger:error("exchange rates error: ~p", [Reason]), + {error, Reason} + end. diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index 493c5d6d..b91a00a8 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -57,6 +57,7 @@ -export([get_invoice/1]). -export([get_origin/1]). -export([get_risk_score/1]). +-export([get_exchange_context/1]). -export([construct_payment_info/2]). -export([set_repair_scenario/2]). @@ -129,6 +130,7 @@ -export_type([change_opts/0]). -export_type([action/0]). -export_type([cashflow_context/0]). +-export_type([exchange_context/0]). -type activity() :: payment_activity() @@ -155,6 +157,7 @@ | risk_scoring | routing | routing_failure + | exchange_context_building | cash_flow_building | processing_session | processing_accounter @@ -232,7 +235,14 @@ varset := hg_varset:varset(), revision := hg_domain:revision(), merchant_terms => dmsl_domain_thrift:'PaymentsServiceTerms'(), - allocation => hg_allocation:allocation() | undefined + allocation => hg_allocation:allocation() | undefined, + exchange_context => exchange_context() | undefined +}. + +-type exchange_context() :: #{ + source := dmsl_domain_thrift:'CurrencySymbolicCode'(), + destination := dmsl_domain_thrift:'CurrencySymbolicCode'(), + rate := dmsl_base_thrift:'Rational'() }. %% @@ -273,6 +283,10 @@ get_candidate_routes(#st{candidate_routes = undefined}) -> get_candidate_routes(#st{candidate_routes = Routes}) -> Routes. +-spec get_exchange_context(st()) -> exchange_context() | undefined. +get_exchange_context(#st{exchange_context = ExchangeContext}) -> + ExchangeContext. + -spec get_adjustments(st()) -> [adjustment()]. get_adjustments(#st{adjustments = As}) -> As. @@ -997,6 +1011,7 @@ partial_capture(St0, Reason, Cost, Cart, Opts, MerchantTerms, Timestamp, Allocat Route = get_route(St), ProviderTerms = hg_party:get_route_payment_terms(Route, VS, Revision), ok = validate_provider_holds_terms(ProviderTerms), + ExchangeContext = get_exchange_context(St), Context = #{ provision_terms => ProviderTerms, merchant_terms => MerchantTerms, @@ -1005,7 +1020,8 @@ partial_capture(St0, Reason, Cost, Cart, Opts, MerchantTerms, Timestamp, Allocat timestamp => Timestamp, varset => VS, revision => Revision, - allocation => Allocation + allocation => Allocation, + exchange_context => ExchangeContext }, FinalCashflow = calculate_cashflow(Context, Opts), Changes = start_partial_capture(Reason, Cost, Cart, FinalCashflow, Allocation), @@ -1406,6 +1422,7 @@ create_cash_flow_adjustment(Timestamp, Params, DomainRevision, St, Opts) -> {Payment1, AdditionalEvents} = maybe_inject_new_cost_amount( Payment, Params#payproc_InvoicePaymentAdjustmentParams.scenario ), + ExchangeContext = get_exchange_context(St), Context = #{ provision_terms => get_provider_terminal_terms(Route, VS, NewRevision), route => Route, @@ -1413,7 +1430,8 @@ create_cash_flow_adjustment(Timestamp, Params, DomainRevision, St, Opts) -> timestamp => Timestamp, varset => VS, revision => NewRevision, - allocation => Allocation + allocation => Allocation, + exchange_context => ExchangeContext }, NewCashFlow = case Payment of @@ -1558,6 +1576,7 @@ get_cash_flow_for_target_status({captured, Captured}, St0, Opts) -> St = St0#st{payment = Payment2}, Revision = Payment2#domain_InvoicePayment.domain_revision, VS = collect_validation_varset(St, Opts), + ExchangeContext = get_exchange_context(St), Context = #{ provision_terms => get_provider_terminal_terms(Route, VS, Revision), route => Route, @@ -1565,7 +1584,8 @@ get_cash_flow_for_target_status({captured, Captured}, St0, Opts) -> timestamp => Timestamp, varset => VS, revision => Revision, - allocation => Allocation + allocation => Allocation, + exchange_context => ExchangeContext }, calculate_cashflow(Context, Opts); get_cash_flow_for_target_status({cancelled, _}, _St, _Opts) -> @@ -1769,6 +1789,8 @@ process_timeout({payment, risk_scoring}, Action, St) -> process_risk_score(Action, St); process_timeout({payment, routing}, Action, St) -> process_routing(Action, St); +process_timeout({payment, exchange_context_building}, Action, St) -> + process_currency_exchange(Action, St); process_timeout({payment, cash_flow_building}, Action, St) -> process_cash_flow_building(Action, St); process_timeout({payment, Step}, _Action, St) when @@ -1806,7 +1828,8 @@ process_refund(ID, #st{opts = Options0, payment = Payment, repair_scenario = Sce Options1 = Options0#{ payment => Payment, payment_info => PaymentInfo, - repair_scenario => RepairScenario + repair_scenario => RepairScenario, + exchange_context => get_exchange_context(St) }, Refund = try_get_refund_state(ID, St), {Step, {Events0, Action}} = hg_invoice_payment_refund:process(Options1, Refund), @@ -2155,6 +2178,60 @@ mk_static_error([_ | _] = Codes) -> mk_static_error_(#payproc_error_GeneralFailu mk_static_error_(T, []) -> T; mk_static_error_(Sub, [Code | Codes]) -> mk_static_error_({Code, Sub}, Codes). +-spec process_currency_exchange(action(), st()) -> machine_result(). +process_currency_exchange(Action, St) -> + Route = get_route(St), + Revision = get_payment_revision(St), + VS = get_varset(St, #{risk_score => get_risk_score(St)}), + case build_exchange_context(Route, VS, Revision, St) of + {ok, undefined} -> + %% no currency conversion required + %% go to cashflow building + process_cash_flow_building(Action, St#st{activity = {payment, cash_flow_building}}); + {ok, #{ + source := Src, + destination := Dst, + rate := Rate + }} -> + {next, {[?payment_currency_changed(Src, Dst, Rate)], hg_machine_action:set_timeout(0, Action)}}; + {error, Reason} -> + FailureReason = genlib:format("Payment currency conversion error: ~p", [Reason]), + Failure = payproc_errors:construct( + 'PaymentFailure', + {authorization_failed, {unknown, #payproc_error_GeneralFailure{}}}, + FailureReason + ), + {next, {[?payment_rollback_started({failure, Failure})], hg_machine_action:set_timeout(0, Action)}} + end. + +%build_exchange_context(#{provider_ref := ProviderRef, terminal_ref := TerminalRef}, VS, Revision, St) -> +build_exchange_context(Route, VS, Revision, St) -> + %Route = ?route(ProviderRef, TerminalRef), + #domain_Cash{ + currency = #domain_CurrencyRef{symbolic_code = PaymentCurrency} + } = get_payment_cost(get_payment(St)), + #domain_PaymentsProvisionTerms{ + currencies = {value, [#domain_CurrencyRef{symbolic_code = TerminalCurrency}]} + } = get_provider_terminal_terms(Route, VS, Revision), + case PaymentCurrency =:= TerminalCurrency of + true -> + {ok, undefined}; + false -> + get_exchange_context(PaymentCurrency, TerminalCurrency) + end. + +get_exchange_context(SrcCurrency, DstCurrency) -> + case hg_exrates:get_exchange_rate(SrcCurrency, DstCurrency) of + {ok, #{p := P, q := Q}} -> + {ok, #{ + source => SrcCurrency, + destination => DstCurrency, + rate => #base_Rational{p = P, q = Q} + }}; + {error, _} = Error -> + Error + end. + -spec process_cash_flow_building(action(), st()) -> machine_result(). process_cash_flow_building(Action, St) -> Route = get_route(St), @@ -2166,6 +2243,7 @@ process_cash_flow_building(Action, St) -> VS1 = collect_validation_varset(get_party_config_ref(Opts), get_shop_obj(Opts, Revision), Payment, VS0), ProviderTerms = get_provider_terminal_terms(Route, VS1, Revision), Allocation = get_allocation(St), + ExchangeContext = get_exchange_context(St), Context = #{ provision_terms => ProviderTerms, route => Route, @@ -2173,7 +2251,8 @@ process_cash_flow_building(Action, St) -> timestamp => Timestamp, varset => VS1, revision => Revision, - allocation => Allocation + allocation => Allocation, + exchange_context => ExchangeContext }, FinalCashflow = calculate_cashflow(Context, Opts), _ = rollback_unused_payment_limits(St), @@ -2264,7 +2343,12 @@ process_accounter_update(Action, #st{partial_cash_flow = FinalCashflow, capture_ handle_callback({refund, ID}, Payload, _Session0, St) -> PaymentInfo = construct_payment_info(St, get_opts(St)), Refund = try_get_refund_state(ID, St), - {Resp, {Step, {Events0, Action}}} = hg_invoice_payment_refund:process_callback(Payload, PaymentInfo, Refund), + Options = #{ + exchange_context => get_exchange_context(St) + }, + {Resp, {Step, {Events0, Action}}} = hg_invoice_payment_refund:process_callback( + Payload, PaymentInfo, Refund, Options + ), Events1 = hg_invoice_payment_refund:wrap_events(Events0, Refund), {Resp, {Step, {Events1, Action}}}; handle_callback(Activity, Payload, Session0, St) -> @@ -2377,6 +2461,7 @@ process_result({payment, processing_accounter}, Action, #st{new_cash = Cost} = S MerchantTerms = get_merchant_payments_terms(Opts, Revision, Timestamp, VS), Route = get_route(St1), ProviderTerms = hg_party:get_route_payment_terms(Route, VS, Revision), + ExchangeContext = get_exchange_context(St1), Context = #{ provision_terms => ProviderTerms, merchant_terms => MerchantTerms, @@ -2384,7 +2469,8 @@ process_result({payment, processing_accounter}, Action, #st{new_cash = Cost} = S payment => Payment1, timestamp => Timestamp, varset => VS, - revision => Revision + revision => Revision, + exchange_context => ExchangeContext }, FinalCashflow = calculate_cashflow(Context, Opts), %% Hold limits (only for chosen route) for new cashflow @@ -2898,6 +2984,10 @@ commit_payment_cashflow(St) -> end ). +rollback_payment_cashflow(#st{cash_flow = undefined}) -> + %% This case is required to handle the situation where cascading operations is required, + %% but an error occurred before the cashflow were calculated + hg_accounting:empty_log(); rollback_payment_cashflow(St) -> Plan = get_cashflow_plan(St), do_try_with_ids( @@ -2969,7 +3059,7 @@ construct_payment_info(St, Opts) -> St, #proxy_provider_PaymentInfo{ shop = construct_proxy_shop(get_shop_obj(Opts, Revision)), - invoice = construct_proxy_invoice(get_invoice(Opts)), + invoice = construct_proxy_invoice(get_invoice(Opts), St), payment = construct_proxy_payment(Payment, get_trx(St), St) } ). @@ -2978,12 +3068,14 @@ construct_payment_info(idle, _Target, _St, PaymentInfo) -> PaymentInfo; construct_payment_info( {payment, _Step}, - Target = ?captured(), - _St, + _Target = ?captured(_, Cost), + St, PaymentInfo ) -> + ExchangeContext = get_exchange_context(St), + {ConvertedCost, _OriginalCost} = maybe_convert_cash(ExchangeContext, Cost), PaymentInfo#proxy_provider_PaymentInfo{ - capture = construct_proxy_capture(Target) + capture = construct_proxy_capture(ConvertedCost) }; construct_payment_info({payment, _Step}, _Target, _St, PaymentInfo) -> PaymentInfo; @@ -3007,6 +3099,8 @@ construct_proxy_payment( ) -> ContactInfo = get_contact_info(Payer), PaymentTool = get_payer_payment_tool(Payer), + ExchangeContext = get_exchange_context(St), + {ConvertedCost, OriginalCost} = maybe_convert_cash(ExchangeContext, Cost), #proxy_provider_InvoicePayment{ id = ID, created_at = CreatedAt, @@ -3014,7 +3108,8 @@ construct_proxy_payment( payment_resource = construct_payment_resource(Payer, St), payment_service = hg_payment_tool:get_payment_service(PaymentTool, Revision), payer_session_info = PayerSessionInfo, - cost = construct_proxy_cash(Cost), + cost = construct_proxy_cash(ConvertedCost), + original_cost = construct_proxy_cash(OriginalCost), contact_info = ContactInfo, make_recurrent = MakeRecurrent, skip_recurrent = SkipRecurrent, @@ -3057,14 +3152,18 @@ construct_proxy_invoice( due = Due, details = Details, cost = Cost - } + }, + St ) -> + ExchangeContext = get_exchange_context(St), + {ConvertedCost, OriginalCost} = maybe_convert_cash(ExchangeContext, Cost), #proxy_provider_Invoice{ id = InvoiceID, created_at = CreatedAt, due = Due, details = Details, - cost = construct_proxy_cash(Cost) + cost = construct_proxy_cash(ConvertedCost), + original_cost = construct_proxy_cash(OriginalCost) }. construct_proxy_shop( @@ -3085,6 +3184,8 @@ construct_proxy_shop( location = Location }. +construct_proxy_cash(undefined) -> + undefined; construct_proxy_cash(#domain_Cash{ amount = Amount, currency = CurrencyRef @@ -3094,11 +3195,28 @@ construct_proxy_cash(#domain_Cash{ currency = hg_domain:get({currency, CurrencyRef}) }. -construct_proxy_capture(?captured(_, Cost)) -> +construct_proxy_capture(Cost) -> #proxy_provider_InvoicePaymentCapture{ cost = construct_proxy_cash(Cost) }. +maybe_convert_cash(undefined, Cost) -> + {Cost, undefined}; +maybe_convert_cash( + #{source := PaymentCurrency} = ExchangeContext, + #domain_Cash{currency = #domain_CurrencyRef{symbolic_code = PaymentCurrency}} = OriginalCash +) -> + ConvertedCash = hg_currency_converter:convert_cash(ExchangeContext, OriginalCash), + {ConvertedCash, OriginalCash}. + +maybe_reverse_convert_cash(undefined, Cost) -> + Cost; +maybe_reverse_convert_cash( + #{destination := TerminalCurrency} = ExchangeContext, + #domain_Cash{currency = #domain_CurrencyRef{symbolic_code = TerminalCurrency}} = TerminalCash +) -> + hg_currency_converter:reverse_convert_cash(ExchangeContext, TerminalCash). + %% get_party_obj(#{party := Party, party_config_ref := PartyConfigRef}) -> @@ -3276,13 +3394,29 @@ merge_change( cash_flow = undefined, %% `trx` from previous session (if any) also must be considered obsolete. trx = undefined, + %% exchange_context from previous route must be considered obsolete + exchange_context = undefined, routes = [Route | Routes], candidate_routes = ordsets:to_list(Candidates), - activity = {payment, cash_flow_building}, + activity = {payment, exchange_context_building}, route_scores = hg_maybe:apply(fun(S) -> maps:merge(RouteScores, S) end, Scores, RouteScores), route_limits = hg_maybe:apply(fun(L) -> maps:merge(RouteLimits, L) end, Limits, RouteLimits), payment = Payment1 }; +merge_change( + Change = ?payment_currency_changed(SourceCurrency, DestinationCurrency, ExchangeRate), + #st{} = St, + Opts +) -> + _ = validate_transition([{payment, routing}], Change, St, Opts), + St#st{ + activity = {payment, cash_flow_building}, + exchange_context = #{ + source => SourceCurrency, + destination => DestinationCurrency, + rate => ExchangeRate + } + }; merge_change(Change = ?payment_capture_started(Data), #st{} = St, Opts) -> _ = validate_transition([{payment, S} || S <- [flow_waiting]], Change, St, Opts), St#st{ @@ -3295,6 +3429,7 @@ merge_change(Change = ?cash_flow_changed(CashFlow), #st{activity = Activity} = S [ {payment, S} || S <- [ + exchange_context_building, cash_flow_building, processing_capture, processing_accounter @@ -3310,7 +3445,10 @@ merge_change(Change = ?cash_flow_changed(CashFlow), #st{activity = Activity} = S case Activity of {payment, processing_accounter} -> St#st{new_cash = undefined, new_cash_flow = CashFlow}; - {payment, cash_flow_building} -> + {payment, PaymentActivity} when + PaymentActivity =:= exchange_context_building; + PaymentActivity =:= cash_flow_building + -> St#st{ cash_flow = CashFlow, activity = {payment, processing_session} @@ -3336,12 +3474,15 @@ merge_change(Change = ?cash_changed(_OldCash, NewCash), #st{} = St, Opts) -> Opts ), Payment0 = get_payment(St), - Payment1 = Payment0#domain_InvoicePayment{changed_cost = NewCash}, - St#st{new_cash = NewCash, new_cash_provided = true, payment = Payment1}; + ExchangeContext = get_exchange_context(St), + ReConvertedNewCash = maybe_reverse_convert_cash(ExchangeContext, NewCash), + Payment1 = Payment0#domain_InvoicePayment{changed_cost = ReConvertedNewCash}, + St#st{new_cash = ReConvertedNewCash, new_cash_provided = true, payment = Payment1}; merge_change(Change = ?payment_rollback_started(Failure), St, Opts) -> _ = validate_transition( [ {payment, shop_limit_finalizing}, + {payment, exchange_context_building}, {payment, cash_flow_building}, {payment, processing_session} ], @@ -3353,6 +3494,8 @@ merge_change(Change = ?payment_rollback_started(Failure), St, Opts) -> case St of #st{shop_limit_status = initialized} -> {payment, shop_limit_failure}; + #st{activity = {payment, exchange_context_building}} -> + {payment, processing_failure}; #st{cash_flow = undefined} -> {payment, routing_failure}; _ -> diff --git a/apps/hellgate/src/hg_invoice_payment_chargeback.erl b/apps/hellgate/src/hg_invoice_payment_chargeback.erl index 80adbf8e..9d0931fe 100644 --- a/apps/hellgate/src/hg_invoice_payment_chargeback.erl +++ b/apps/hellgate/src/hg_invoice_payment_chargeback.erl @@ -420,6 +420,7 @@ build_chargeback_final_cash_flow(State, Opts) -> Body = get_body(State), Payment = get_opts_payment(Opts), Invoice = get_opts_invoice(Opts), + ExchangeContext = get_opts_exchange_context(Opts), Route = get_opts_route(Opts), Party = get_opts_party(Opts), PartyConfigRef = get_opts_party_config_ref(Opts), @@ -430,8 +431,11 @@ build_chargeback_final_cash_flow(State, Opts) -> PaymentsTerms = hg_party:get_route_payment_terms(Route, VS, Revision), ProviderTerms = get_provider_chargeback_terms(PaymentsTerms, Payment), ServiceCashFlow = get_chargeback_service_cash_flow(ServiceTerms), - ProviderCashFlow = get_chargeback_provider_cash_flow(ProviderTerms), - ProviderFees = collect_chargeback_provider_fees(ProviderTerms), + ProviderCashFlow = maybe_convert_cashflow( + ExchangeContext, + get_chargeback_provider_cash_flow(ProviderTerms) + ), + ProviderFees = maybe_convert_fees(ExchangeContext, collect_chargeback_provider_fees(ProviderTerms)), PaymentInstitutionRef = Shop#domain_ShopConfig.payment_institution, PaymentInst = hg_payment_institution:compute_payment_institution(PaymentInstitutionRef, VS, Revision), Provider = get_route_provider(Route, Revision), @@ -466,6 +470,21 @@ build_provider_cash_flow_context(State, Fees) -> maps:merge(ComputedFees, #{operation_amount => get_body(State)}) end. +maybe_convert_cashflow(undefined, CashFlow) -> + CashFlow; +maybe_convert_cashflow(ExchangeContext, CashFlow) -> + hg_cashflow_utils:convert_cashflow(ExchangeContext, CashFlow). + +maybe_convert_fees(undefined, Fees) -> + Fees; +maybe_convert_fees(ExchangeContext, Fees) -> + maps:map( + fun(_Const, Volume) -> + hg_cashflow_utils:convert_volume(ExchangeContext, Volume) + end, + Fees + ). + get_chargeback_service_cash_flow( #domain_PaymentChargebackServiceTerms{fees = {value, V}} ) -> @@ -733,6 +752,9 @@ get_opts_payment_state(#{payment_state := PaymentState}) -> get_opts_payment(#{payment_state := PaymentState}) -> hg_invoice_payment:get_payment(PaymentState). +get_opts_exchange_context(#{payment_state := PaymentState}) -> + hg_invoice_payment:get_exchange_context(PaymentState). + get_opts_route(#{payment_state := PaymentState}) -> hg_invoice_payment:get_route(PaymentState). diff --git a/apps/hellgate/src/hg_invoice_payment_refund.erl b/apps/hellgate/src/hg_invoice_payment_refund.erl index 6c18a766..cf6e3af0 100644 --- a/apps/hellgate/src/hg_invoice_payment_refund.erl +++ b/apps/hellgate/src/hg_invoice_payment_refund.erl @@ -73,6 +73,7 @@ -export([process/2]). -export([process_callback/3]). +-export([process_callback/4]). -export([deduce_activity/1]). %% Internal types @@ -124,7 +125,8 @@ invoice_id := invoice_id(), payment_id := payment_id(), repair_scenario => repair_scenario(), - payment_info => payment_info() + payment_info => payment_info(), + exchange_context => hg_invoice_payment:exchange_context() | undefined }. -type options() :: #{ @@ -135,7 +137,9 @@ payment => payment(), repair_scenario => repair_scenario(), - payment_info => payment_info() + payment_info => payment_info(), + + exchange_context => hg_invoice_payment:exchange_context() | undefined }. -type repair_scenario() :: {result, proxy_result()}. @@ -224,6 +228,11 @@ process(Options, Refund0) -> -spec process_callback(callback(), payment_info(), t()) -> {callback_response(), machine_result()}. process_callback(Payload, PaymentInfo0, Refund) -> + process_callback(Payload, PaymentInfo0, Refund, #{}). + +-spec process_callback(callback(), payment_info(), t(), options()) -> {callback_response(), machine_result()}. +process_callback(Payload, PaymentInfo0, Refund0, Options) -> + Refund = inject_context(Options, Refund0), PaymentInfo1 = construct_payment_info(PaymentInfo0, Refund), Session0 = hg_session:set_payment_info(PaymentInfo1, session(Refund)), {Response, {Result, Session1}} = hg_session:process_callback(Payload, Session0), @@ -490,13 +499,13 @@ get_initial_retry_strategy() -> hg_retry:new_strategy(maps:get(refunded, PolicyConfig, no_retry)). inject_context(Options, Refund) -> - Invoice = maps:get(invoice, Options), - Payment = maps:get(payment, Options), - #domain_Invoice{id = InvoiceID, shop_ref = ShopConfigRef} = Invoice, - #domain_InvoicePayment{id = PaymentID, domain_revision = Revision} = Payment, - Party = maps:get(party, Options), - PartyConfigRef = maps:get(party_config_ref, Options), - {ShopConfigRef, Shop} = hg_party:get_shop(ShopConfigRef, PartyConfigRef, Revision), + Invoice = maps:get(invoice, Options, undefined), + Payment = maps:get(payment, Options, undefined), + {InvoiceID, ShopConfigRef} = get_opts_invoice_data(Invoice), + {PaymentID, Revision} = get_opts_payment_data(Payment), + Party = maps:get(party, Options, undefined), + PartyConfigRef = maps:get(party_config_ref, Options, undefined), + {ShopConfigRef, Shop} = try_get_shop(ShopConfigRef, PartyConfigRef, Revision), Context = genlib_map:compact(#{ party => Party, shop => Shop, @@ -507,10 +516,29 @@ inject_context(Options, Refund) -> invoice_id => InvoiceID, payment_id => PaymentID, repair_scenario => maps:get(repair_scenario, Options, undefined), - payment_info => maps:get(payment_info, Options, undefined) + payment_info => maps:get(payment_info, Options, undefined), + exchange_context => maps:get(exchange_context, Options, undefined) }), Refund#{injected_context => Context}. +get_opts_invoice_data(undefined) -> + {undefined, undefined}; +get_opts_invoice_data(#domain_Invoice{id = InvoiceID, shop_ref = ShopConfigRef}) -> + {InvoiceID, ShopConfigRef}. + +get_opts_payment_data(undefined) -> + {undefined, undefined}; +get_opts_payment_data(#domain_InvoicePayment{id = PaymentID, domain_revision = Revision}) -> + {PaymentID, Revision}. + +try_get_shop(ShopConfigRef, PartyConfigRef, _Revision) when + ShopConfigRef =:= undefined; + PartyConfigRef =:= undefined +-> + {undefined, undefined}; +try_get_shop(ShopConfigRef, PartyConfigRef, Revision) -> + hg_party:get_shop(ShopConfigRef, PartyConfigRef, Revision). + get_injected_party_config_ref(#{injected_context := #{party_config_ref := V}}) -> V. get_injected_invoice(#{injected_context := #{invoice := V}}) -> V. get_injected_payment(#{injected_context := #{payment := V}}) -> V. @@ -520,6 +548,7 @@ get_injected_invoice_id(#{injected_context := #{invoice_id := V}}) -> V. get_injected_payment_id(#{injected_context := #{payment_id := V}}) -> V. get_injected_repair_scenario(#{injected_context := Context}) -> maps:get(repair_scenario, Context, undefined). get_injected_payment_info(#{injected_context := Context}) -> maps:get(payment_info, Context, undefined). +get_injected_exchange_context(#{injected_context := Context}) -> maps:get(exchange_context, Context, undefined). %% Event utils @@ -581,15 +610,25 @@ get_refund_created_at(#domain_InvoicePaymentRefund{created_at = CreatedAt}) -> CreatedAt. construct_payment_info(PaymentInfo, Refund) -> + ExchangeContext = get_injected_exchange_context(Refund), + {ConvertedCash, _OriginalCash} = maybe_convert_cash(ExchangeContext, cash(Refund)), PaymentInfo#proxy_provider_PaymentInfo{ refund = #proxy_provider_InvoicePaymentRefund{ id = id(Refund), created_at = get_refund_created_at(refund(Refund)), trx = hg_session:trx_info(session(Refund)), - cash = construct_proxy_cash(cash(Refund)) + cash = construct_proxy_cash(ConvertedCash) } }. +maybe_convert_cash(undefined, Cash) -> + {Cash, undefined}; +maybe_convert_cash(ExchangeContext, Cash) -> + { + hg_currency_converter:convert_cash(ExchangeContext, Cash), + Cash + }. + construct_proxy_cash(#domain_Cash{ amount = Amount, currency = CurrencyRef diff --git a/apps/hellgate/src/hg_invoice_registered_payment.erl b/apps/hellgate/src/hg_invoice_registered_payment.erl index 8414ccaf..bb6df5da 100644 --- a/apps/hellgate/src/hg_invoice_registered_payment.erl +++ b/apps/hellgate/src/hg_invoice_registered_payment.erl @@ -81,6 +81,7 @@ init_(PaymentID, Params, #{timestamp := CreatedAt0} = Opts) -> timestamp => CreatedAt1, varset => VS, revision => Revision + % add exchange_context }, FinalCashflow = hg_invoice_payment:calculate_cashflow(PaymentInstitution, CashflowContext, Opts), @@ -93,6 +94,7 @@ init_(PaymentID, Params, #{timestamp := CreatedAt0} = Opts) -> RiskScoreEventList ++ [ ?route_changed(Route), + %% TODO currency_changed ?cash_flow_changed(FinalCashflow), hg_session:wrap_event(?processed(), hg_session:create()) ] ++ diff --git a/apps/hellgate/src/hg_invoice_repair.erl b/apps/hellgate/src/hg_invoice_repair.erl index 3a644f08..64dd5c0a 100644 --- a/apps/hellgate/src/hg_invoice_repair.erl +++ b/apps/hellgate/src/hg_invoice_repair.erl @@ -90,6 +90,7 @@ check_activity_compatibility(?SCENARIO_FAIL_PRE_PROCESSING, Activity) when Activity =:= {payment, new} orelse Activity =:= {payment, risk_scoring} orelse Activity =:= {payment, routing} orelse + Activity =:= {payment, exchange_context_building} orelse Activity =:= {payment, cash_flow_building} -> ok; diff --git a/apps/hellgate/test/hg_dummy_exrates.erl b/apps/hellgate/test/hg_dummy_exrates.erl new file mode 100644 index 00000000..3c1b3906 --- /dev/null +++ b/apps/hellgate/test/hg_dummy_exrates.erl @@ -0,0 +1,31 @@ +-module(hg_dummy_exrates). + +-include_lib("exrates_proto/include/exrates_service_thrift.hrl"). +-include_lib("exrates_proto/include/exrates_base_thrift.hrl"). + +-behaviour(hg_woody_service_wrapper). + +-export([handle_function/3]). + +-behaviour(hg_test_proxy). + +-export([get_service_spec/0]). + +-spec get_service_spec() -> hg_proto:service_spec(). +get_service_spec() -> + {"/test/exrates/dummy", {exrates_service_thrift, 'ExchangeRateService'}}. + +-spec handle_function(woody:func(), woody:args(), hg_woody_service_wrapper:handler_opts()) -> term() | no_return(). +handle_function( + 'GetExchangeRateData', + #service_GetCurrencyExchangeRateRequest{ + currency_data = CurrencyData + }, + _ +) -> + Timestamp = calendar:system_time_to_rfc3339(erlang:system_time(second), [{offset, "Z"}]), + #service_GetCurrencyExchangeRateResult{ + currency_data = CurrencyData, + exchange_rate = #base_Rational{p = 1, q = 1}, + timestamp = unicode:characters_to_binary(Timestamp) + }. diff --git a/apps/hg_proto/src/hg_proto.erl b/apps/hg_proto/src/hg_proto.erl index 2da3e413..0c76413c 100644 --- a/apps/hg_proto/src/hg_proto.erl +++ b/apps/hg_proto/src/hg_proto.erl @@ -45,7 +45,9 @@ get_service(party_config) -> get_service(customer_management) -> {dmsl_customer_thrift, 'CustomerManagement'}; get_service(bank_card_storage) -> - {dmsl_customer_thrift, 'BankCardStorage'}. + {dmsl_customer_thrift, 'BankCardStorage'}; +get_service(rate_boss) -> + {exrates_service_thrift, 'ExchangeRateService'}. -spec get_service_spec(Name :: atom()) -> service_spec(). get_service_spec(Name) -> diff --git a/config/sys.config b/config/sys.config index 15013612..0812ea52 100644 --- a/config/sys.config +++ b/config/sys.config @@ -38,7 +38,8 @@ bank_card_storage => "http://cubasty:8022/v1/customer/bank_card", % TODO make more consistent recurrent_paytool => "http://hellgate:8022/v1/processing/recpaytool", - fault_detector => "http://fault-detector:8022/v1/fault-detector" + fault_detector => "http://fault-detector:8022/v1/fault-detector", + rate_boss => "http://rate-boss:8022/v1/ex-rate" }} ]}, diff --git a/rebar.config b/rebar.config index bacf7d6f..ec6e06d1 100644 --- a/rebar.config +++ b/rebar.config @@ -36,7 +36,9 @@ {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.33"}}}, + %{damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.33"}}}, + {damsel, {git, "https://github.com/valitydev/damsel.git", {branch, "XYZ-19/ft/multi-currency"}}}, + {exrates_proto, {git, "https://github.com/valitydev/exrates-proto.git", {branch, "fx/add-rebar-config"}}}, {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"}}}, {dmt_client, {git, "https://github.com/valitydev/dmt-client.git", {tag, "v2.0.3"}}}, diff --git a/rebar.lock b/rebar.lock index 21ff1a68..7b26950d 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,"e7a302a684deba1bb18a00d1056879329219d280"}}, + {ref,"e139ec58580d5433f87f04607fdaf56a3445970c"}}, 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt-client.git", @@ -59,6 +59,10 @@ {ref,"49716470d0e8dab5e37db55d52dea78001735a3d"}}, 0}, {<<"erlydtl">>,{pkg,<<"erlydtl">>,<<"0.14.0">>},2}, + {<<"exrates_proto">>, + {git,"https://github.com/valitydev/exrates-proto.git", + {ref,"e23772c160f10d141851e98a3a0995e297df9cb3"}}, + 0}, {<<"fault_detector_proto">>, {git,"https://github.com/valitydev/fault-detector-proto.git", {ref,"32f0b845052aa352539edade5b1efdd3c4e01371"}}, From 3188e1c08986276894665865c35f3e8275b37325 Mon Sep 17 00:00:00 2001 From: ttt161 Date: Sun, 21 Jun 2026 23:37:29 +0300 Subject: [PATCH 2/8] finalize multicurrency payment implementation --- apps/hellgate/src/hg_accounting.erl | 6 +- apps/hellgate/src/hg_cashflow.erl | 29 +- apps/hellgate/src/hg_cashflow_utils.erl | 19 +- apps/hellgate/src/hg_exrates.erl | 4 +- apps/hellgate/src/hg_invoice_payment.erl | 19 +- apps/hellgate/test/hg_ct_helper.erl | 2 + apps/hellgate/test/hg_dummy_exrates.erl | 45 +- apps/hellgate/test/hg_dummy_provider.erl | 34 +- apps/hellgate/test/hg_invoice_dummy_data.erl | 337 ++++++++++ .../test/hg_invoice_exchange_tests_SUITE.erl | 598 ++++++++++++++++++ apps/hellgate/test/hg_invoice_helper.erl | 87 ++- apps/routing/src/hg_route_collector.erl | 9 +- rebar.lock | 2 +- 13 files changed, 1159 insertions(+), 32 deletions(-) create mode 100644 apps/hellgate/test/hg_invoice_dummy_data.erl create mode 100644 apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl diff --git a/apps/hellgate/src/hg_accounting.erl b/apps/hellgate/src/hg_accounting.erl index 8b1bd6d1..d2e938fa 100644 --- a/apps/hellgate/src/hg_accounting.erl +++ b/apps/hellgate/src/hg_accounting.erl @@ -238,7 +238,8 @@ collect_postings(Cashflow) -> to_id = Destination, amount = Amount, currency_sym_code = CurrencyCode, - description = construct_posting_description(Details) + description = construct_posting_description(Details), + exchange_context = ExchangeContext } || #domain_FinalCashFlowPosting{ source = #domain_FinalCashFlowAccount{account_id = Source}, @@ -247,7 +248,8 @@ collect_postings(Cashflow) -> volume = #domain_Cash{ amount = Amount, currency = #domain_CurrencyRef{symbolic_code = CurrencyCode} - } + }, + exchange_context = ExchangeContext } <- Cashflow ]. diff --git a/apps/hellgate/src/hg_cashflow.erl b/apps/hellgate/src/hg_cashflow.erl index c0a18bc3..cf85c0ee 100644 --- a/apps/hellgate/src/hg_cashflow.erl +++ b/apps/hellgate/src/hg_cashflow.erl @@ -44,11 +44,19 @@ %% --define(posting(Source, Destination, Volume, Details), #domain_CashFlowPosting{ +%-define(posting(Source, Destination, Volume, Details), #domain_CashFlowPosting{ +% source = Source, +% destination = Destination, +% volume = Volume, +% details = Details +%}). + +-define(posting(Source, Destination, Volume, Details, ExchangeContext), #domain_CashFlowPosting{ source = Source, destination = Destination, volume = Volume, - details = Details + details = Details, + exchange_context = ExchangeContext }). -define(final_posting(Source, Destination, Volume, Details), #domain_FinalCashFlowPosting{ @@ -58,6 +66,14 @@ details = Details }). +-define(final_posting(Source, Destination, Volume, Details, ExchangeContext), #domain_FinalCashFlowPosting{ + source = Source, + destination = Destination, + volume = Volume, + details = Details, + exchange_context = ExchangeContext +}). + -spec finalize(cash_flow(), context(), account_map()) -> final_cash_flow() | no_return(). finalize(CF, Context, AccountMap) -> compute_postings(CF, Context, AccountMap). @@ -69,9 +85,10 @@ compute_postings(CF, Context, AccountMap) -> construct_final_account(Source, AccountMap), construct_final_account(Destination, AccountMap), compute_volume(Volume, Context), - Details + Details, + ExchangeContext ) - || ?posting(Source, Destination, Volume, Details) <- CF + || ?posting(Source, Destination, Volume, Details, ExchangeContext) <- CF ]. -spec construct_final_account(account(), account_map()) -> final_cash_flow_account() | no_return(). @@ -127,8 +144,8 @@ resolve_account(AccountType, AccountMap) -> -spec revert(final_cash_flow()) -> final_cash_flow(). revert(CF) -> [ - ?final_posting(Destination, Source, Volume, revert_details(Details)) - || ?final_posting(Source, Destination, Volume, Details) <- CF + ?final_posting(Destination, Source, Volume, revert_details(Details), ExchangeContext) + || ?final_posting(Source, Destination, Volume, Details, ExchangeContext) <- CF ]. revert_details(undefined) -> diff --git a/apps/hellgate/src/hg_cashflow_utils.erl b/apps/hellgate/src/hg_cashflow_utils.erl index 6063a0c3..f167d0bc 100644 --- a/apps/hellgate/src/hg_cashflow_utils.erl +++ b/apps/hellgate/src/hg_cashflow_utils.erl @@ -136,12 +136,29 @@ maybe_convert_cashflow(ExchangeContext, ProviderCashflow) -> convert_cashflow(ExchangeContext, ProviderCashflow) -> lists:foldr( fun(#domain_CashFlowPosting{volume = CashVolume} = P, Acc) -> - [P#domain_CashFlowPosting{volume = convert_volume(ExchangeContext, CashVolume)} | Acc] + [ + P#domain_CashFlowPosting{ + volume = convert_volume(ExchangeContext, CashVolume), + exchange_context = construct_exchange_context(ExchangeContext) + } + | Acc + ] end, [], ProviderCashflow ). +construct_exchange_context(#{ + source := SourceCurrency, + destination := DestinationCurrency, + rate := ExchangeRate +}) -> + #domain_ExchangeContext{ + source_currency = SourceCurrency, + destination_currency = DestinationCurrency, + exchange_rate = ExchangeRate + }. + -spec convert_volume(hg_invoice_payment:exchange_context(), cash_volume()) -> cash_volume(). convert_volume(_ExchangeContext, {share, _} = CashVolume) -> CashVolume; diff --git a/apps/hellgate/src/hg_exrates.erl b/apps/hellgate/src/hg_exrates.erl index 2d682220..8088064e 100644 --- a/apps/hellgate/src/hg_exrates.erl +++ b/apps/hellgate/src/hg_exrates.erl @@ -28,7 +28,9 @@ get_exchange_rate(SourceCurrency, DestinationCurrency) -> }} -> {ok, #{p => P, q => Q}}; {exception, #'service_ExRateNotFound'{}} -> - {error, not_found} + {error, not_found}; + {error, _} -> + {error, unexpected_error} end. issue_call(Func, Args) -> diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index b91a00a8..9c729071 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -1224,6 +1224,7 @@ make_refund_cashflow(Refund, Payment, Revision, St, Opts, MerchantTerms, VS, Tim Route = get_route(St), ProviderPaymentsTerms = get_provider_terminal_terms(Route, VS, Revision), Allocation = Refund#domain_InvoicePaymentRefund.allocation, + ExchangeContext = get_exchange_context(St), CollectCashflowContext = genlib_map:compact(#{ operation => refund, provision_terms => get_provider_refunds_terms(ProviderPaymentsTerms, Refund, Payment), @@ -1237,7 +1238,8 @@ make_refund_cashflow(Refund, Payment, Revision, St, Opts, MerchantTerms, VS, Tim varset => VS, revision => Revision, refund => Refund, - allocation => Allocation + allocation => Allocation, + exchange_context => ExchangeContext }), hg_cashflow_utils:collect_cashflow(CollectCashflowContext). @@ -1864,8 +1866,9 @@ process_refund_result(Changes, Refund0, St) -> repair_process_timeout(Activity, Action, #st{repair_scenario = Scenario} = St) -> case hg_invoice_repair:check_for_action(fail_pre_processing, Scenario) of {result, Result} when - Activity =:= {payment, routing} orelse - Activity =:= {payment, cash_flow_building} + Activity =:= {payment, routing}; + Activity =:= {payment, cash_flow_building}; + Activity =:= {payment, exchange_context_building} -> rollback_broken_payment_limits(St), Result; @@ -3153,17 +3156,14 @@ construct_proxy_invoice( details = Details, cost = Cost }, - St + _St ) -> - ExchangeContext = get_exchange_context(St), - {ConvertedCost, OriginalCost} = maybe_convert_cash(ExchangeContext, Cost), #proxy_provider_Invoice{ id = InvoiceID, created_at = CreatedAt, due = Due, details = Details, - cost = construct_proxy_cash(ConvertedCost), - original_cost = construct_proxy_cash(OriginalCost) + cost = construct_proxy_cash(Cost) }. construct_proxy_shop( @@ -3408,7 +3408,7 @@ merge_change( #st{} = St, Opts ) -> - _ = validate_transition([{payment, routing}], Change, St, Opts), + _ = validate_transition([{payment, exchange_context_building}], Change, St, Opts), St#st{ activity = {payment, cash_flow_building}, exchange_context = #{ @@ -3513,6 +3513,7 @@ merge_change(Change = ?payment_status_changed({failed, _} = Status), #st{payment || S <- [ risk_scoring, routing, + exchange_context_building, cash_flow_building, shop_limit_failure, routing_failure, diff --git a/apps/hellgate/test/hg_ct_helper.erl b/apps/hellgate/test/hg_ct_helper.erl index 37e6b445..e8c99cca 100644 --- a/apps/hellgate/test/hg_ct_helper.erl +++ b/apps/hellgate/test/hg_ct_helper.erl @@ -165,6 +165,7 @@ start_app(hg_proto = AppName) -> url => <<"http://limiter:8022/v1/limiter">>, transport_opts => #{} }, + rate_boss => <<"http://127.0.0.1:32022/test/exrates/dummy">>, customer_management => <<"http://cubasty:8022/v1/customer/management">>, bank_card_storage => <<"http://cubasty:8022/v1/customer/bank_card">> }} @@ -208,6 +209,7 @@ start_app(hellgate = AppName) -> pre_aggregation_size => 2 } }}, + {currency_exchange_enabled, true}, {backend, progressor} ]), #{ diff --git a/apps/hellgate/test/hg_dummy_exrates.erl b/apps/hellgate/test/hg_dummy_exrates.erl index 3c1b3906..3200b2a9 100644 --- a/apps/hellgate/test/hg_dummy_exrates.erl +++ b/apps/hellgate/test/hg_dummy_exrates.erl @@ -18,14 +18,49 @@ get_service_spec() -> -spec handle_function(woody:func(), woody:args(), hg_woody_service_wrapper:handler_opts()) -> term() | no_return(). handle_function( 'GetExchangeRateData', - #service_GetCurrencyExchangeRateRequest{ - currency_data = CurrencyData - }, + {#service_GetCurrencyExchangeRateRequest{ + currency_data = + #service_CurrencyData{ + source_currency = <<"RUB">>, + destination_currency = <<"USD">> + } = CurrencyData + }}, _ ) -> Timestamp = calendar:system_time_to_rfc3339(erlang:system_time(second), [{offset, "Z"}]), #service_GetCurrencyExchangeRateResult{ currency_data = CurrencyData, - exchange_rate = #base_Rational{p = 1, q = 1}, + exchange_rate = #base_Rational{p = 80, q = 1}, timestamp = unicode:characters_to_binary(Timestamp) - }. + }; +handle_function( + 'GetExchangeRateData', + {#service_GetCurrencyExchangeRateRequest{ + currency_data = + #service_CurrencyData{ + source_currency = <<"RUB">>, + destination_currency = <<"EUR">> + } + }}, + _ +) -> + throw({exception, #service_ExRateNotFound{}}); +handle_function( + 'GetExchangeRateData', + {#service_GetCurrencyExchangeRateRequest{ + currency_data = + #service_CurrencyData{ + source_currency = <<"RUB">>, + destination_currency = <<"JPY">> + } + }}, + _ +) -> + timer:sleep(5100), + {exception, #service_ExRateNotFound{}}; +handle_function( + 'GetExchangeRateData', + _, + _ +) -> + erlang:error(internal_exchange_error). diff --git a/apps/hellgate/test/hg_dummy_provider.erl b/apps/hellgate/test/hg_dummy_provider.erl index 5725d5d9..badda0ce 100644 --- a/apps/hellgate/test/hg_dummy_provider.erl +++ b/apps/hellgate/test/hg_dummy_provider.erl @@ -189,6 +189,9 @@ process_payment(?processed(), undefined, PaymentInfo, CtxOpts, _) -> change_cash_decrease -> %% simple workflow without 3DS result(?sleep(0), <<"sleeping">>); + change_currency_and_increase -> + %% simple workflow without 3DS + result(?sleep(0), <<"sleeping">>); no_preauth -> %% simple workflow without 3DS maybe_fail(PaymentInfo, CtxOpts, result(?sleep(0), <<"sleeping">>)); @@ -257,6 +260,14 @@ process_payment(?processed(), <<"sleeping">>, PaymentInfo, CtxOpts, _) -> finish(success(PaymentInfo, get_payment_increased_cost(PaymentInfo)), mk_trx(TrxID, PaymentInfo)); change_cash_decrease -> finish(success(PaymentInfo, get_payment_decreased_cost(PaymentInfo)), mk_trx(TrxID, PaymentInfo)); + change_currency_and_increase -> + finish( + success( + PaymentInfo, + get_payment_increased_changed_currency(PaymentInfo) + ), + mk_trx(TrxID, PaymentInfo) + ); unexpected_failure -> error(unexpected_failure); {temporary_unavailability, Scenario} -> @@ -549,6 +560,23 @@ get_payment_decreased_cost(PaymentInfo) -> Cost = #proxy_provider_Cash{amount = Amount} = get_payment_cost(PaymentInfo), Cost#proxy_provider_Cash{amount = Amount div 2}. +get_payment_increased_changed_currency(PaymentInfo) -> + %% the cost was converted from RUB at the exchange rate {80, 1} + #proxy_provider_PaymentInfo{ + payment = #proxy_provider_InvoicePayment{ + cost = + #proxy_provider_Cash{ + amount = 525 = Amount, + currency = #domain_Currency{symbolic_code = <<"USD">>} + } = Cost, + original_cost = #proxy_provider_Cash{ + amount = 42000, + currency = #domain_Currency{symbolic_code = <<"RUB">>} + } + } + } = PaymentInfo, + Cost#proxy_provider_Cash{amount = Amount + 10}. + get_payment_info_scenario( #proxy_provider_PaymentInfo{payment = #proxy_provider_InvoicePayment{payment_resource = Resource}} ) -> @@ -564,6 +592,8 @@ get_payment_tool_scenario({'bank_card', #domain_BankCard{token = <<"change_cash_ change_cash_increase; get_payment_tool_scenario({'bank_card', #domain_BankCard{token = <<"change_cash_decrease">>}}) -> change_cash_decrease; +get_payment_tool_scenario({'bank_card', #domain_BankCard{token = <<"change_currency_and_increase">>}}) -> + change_currency_and_increase; get_payment_tool_scenario({'bank_card', #domain_BankCard{token = <<"no_preauth">>}}) -> no_preauth; get_payment_tool_scenario({'bank_card', #domain_BankCard{token = <<"no_preauth_timeout">>}}) -> @@ -639,6 +669,7 @@ get_payment_tool_scenario({'bank_card', #domain_BankCard{token = Token} = BCard} | preauth_3ds_offsite | change_cash_increase | change_cash_decrease + | change_currency_and_increase | forbidden | unexpected_failure | unexpected_failure_no_trx @@ -668,7 +699,8 @@ make_payment_tool(Code, PSys) when Code =:= forbidden orelse Code =:= unexpected_failure orelse Code =:= unexpected_failure_when_suspended orelse - Code =:= unexpected_failure_no_trx + Code =:= unexpected_failure_no_trx orelse + Code =:= change_currency_and_increase -> ?SESSION42(make_bank_card_payment_tool(atom_to_binary(Code, utf8), PSys)); make_payment_tool({assert_contact_info, ContactInfo}, PSys) -> diff --git a/apps/hellgate/test/hg_invoice_dummy_data.erl b/apps/hellgate/test/hg_invoice_dummy_data.erl new file mode 100644 index 00000000..9e36fb6b --- /dev/null +++ b/apps/hellgate/test/hg_invoice_dummy_data.erl @@ -0,0 +1,337 @@ +-module(hg_invoice_dummy_data). + +%-include_lib("hellgate/include/hg_invoice.hrl"). +-include("hg_ct_domain.hrl"). + +-export([construct_domain_fixture/0]). + +-spec construct_domain_fixture() -> _. +construct_domain_fixture() -> + [ + hg_ct_fixture:construct_currency(?cur(<<"RUB">>)), + hg_ct_fixture:construct_currency(?cur(<<"USD">>)), + hg_ct_fixture:construct_currency(?cur(<<"EUR">>)), + hg_ct_fixture:construct_currency(?cur(<<"JPY">>)), + hg_ct_fixture:construct_currency(?cur(<<"CNY">>)), + + hg_ct_fixture:construct_category(?cat(1), <<"Test category">>, test), + hg_ct_fixture:construct_category(?cat(2), <<"Generic Store">>, live), + hg_ct_fixture:construct_category(?cat(3), <<"Guns & Booze">>, live), + hg_ct_fixture:construct_category(?cat(4), <<"Flowers & Meat">>, live), + hg_ct_fixture:construct_category(?cat(5), <<"Horns & Hooves">>, live), + + hg_ct_fixture:construct_payment_method(?pmt(bank_card, ?bank_card(<<"visa-ref">>))), + + hg_ct_fixture:construct_proxy(?prx(1), <<"Dummy proxy">>), + hg_ct_fixture:construct_proxy(?prx(2), <<"Inspector proxy">>), + + hg_ct_fixture:construct_inspector(?insp(1), <<"Rejector">>, ?prx(2), #{<<"risk_score">> => <<"trusted">>}), + + hg_ct_fixture:construct_system_account_set(?sas(1)), + hg_ct_fixture:construct_external_account_set(?eas(1)), + + hg_ct_fixture:construct_payment_routing_ruleset( + ?ruleset(1), + <<"SubMain">>, + {candidates, [ + ?candidate({condition, {category_is, ?cat(1)}}, ?trm(1)), + ?candidate({condition, {category_is, ?cat(2)}}, ?trm(2)), + ?candidate({condition, {category_is, ?cat(3)}}, ?trm(3)), + ?candidate({condition, {category_is, ?cat(4)}}, ?trm(4)), + ?candidate({condition, {category_is, ?cat(5)}}, ?trm(5)) + ]} + ), + hg_ct_fixture:construct_payment_routing_ruleset( + ?ruleset(2), + <<"Prohibitions">>, + {candidates, []} + ), + + {payment_institution, #domain_PaymentInstitutionObject{ + ref = ?pinst(1), + data = #domain_PaymentInstitution{ + name = <<"Test Inc.">>, + system_account_set = {value, ?sas(1)}, + payment_routing_rules = #domain_RoutingRules{ + policies = ?ruleset(1), + prohibitions = ?ruleset(2) + }, + inspector = {value, ?insp(1)}, + residences = [], + realm = test + } + }}, + + {globals, #domain_GlobalsObject{ + ref = #domain_GlobalsRef{}, + data = #domain_Globals{ + external_account_set = {value, ?eas(1)}, + payment_institutions = ?ordset([?pinst(1)]) + } + }}, + + {term_set_hierarchy, #domain_TermSetHierarchyObject{ + ref = ?trms(1), + data = #domain_TermSetHierarchy{ + term_set = #domain_TermSet{payments = payment_service_terms()} + } + }}, + + {provider, #domain_ProviderObject{ + ref = ?prv(1), + data = #domain_Provider{ + name = <<"Brovider">>, + description = <<"A provider but bro">>, + realm = test, + proxy = #domain_Proxy{ + ref = ?prx(1), + additional = #{} + }, + accounts = hg_ct_fixture:construct_provider_account_set([ + ?cur(<<"RUB">>), + ?cur(<<"USD">>), + ?cur(<<"EUR">>), + ?cur(<<"JPY">>), + ?cur(<<"CNY">>) + ]), + terms = #domain_ProvisionTermSet{ + payments = #domain_PaymentsProvisionTerms{ + cash_limit = { + decisions, + [ + #domain_CashLimitDecision{ + if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, + then_ = { + value, + ?cashrng( + {inclusive, ?cash(10, <<"RUB">>)}, + {exclusive, ?cash(420000000, <<"RUB">>)} + ) + } + }, + #domain_CashLimitDecision{ + if_ = {condition, {currency_is, ?cur(<<"USD">>)}}, + then_ = { + value, + ?cashrng( + {inclusive, ?cash(10, <<"USD">>)}, + {exclusive, ?cash(420000000, <<"USD">>)} + ) + } + }, + #domain_CashLimitDecision{ + if_ = {condition, {currency_is, ?cur(<<"EUR">>)}}, + then_ = { + value, + ?cashrng( + {inclusive, ?cash(10, <<"EUR">>)}, + {exclusive, ?cash(420000000, <<"EUR">>)} + ) + } + }, + #domain_CashLimitDecision{ + if_ = {condition, {currency_is, ?cur(<<"JPY">>)}}, + then_ = { + value, + ?cashrng( + {inclusive, ?cash(10, <<"JPY">>)}, + {exclusive, ?cash(420000000, <<"JPY">>)} + ) + } + }, + #domain_CashLimitDecision{ + if_ = {condition, {currency_is, ?cur(<<"CNY">>)}}, + then_ = { + value, + ?cashrng( + {inclusive, ?cash(10, <<"CNY">>)}, + {exclusive, ?cash(420000000, <<"CNY">>)} + ) + } + } + ] + } + } + } + } + }}, + + {terminal, #domain_TerminalObject{ + ref = ?trm(1), + data = #domain_Terminal{ + name = <<"Brominal 1">>, + description = <<"Brominal 1">>, + provider_ref = ?prv(1), + terms = #domain_ProvisionTermSet{ + payments = payment_provision_terms(<<"RUB">>, 1) + } + } + }}, + {terminal, #domain_TerminalObject{ + ref = ?trm(2), + data = #domain_Terminal{ + name = <<"Brominal 2">>, + description = <<"Brominal 2">>, + provider_ref = ?prv(1), + terms = #domain_ProvisionTermSet{ + payments = payment_provision_terms(<<"USD">>, 2) + } + } + }}, + {terminal, #domain_TerminalObject{ + ref = ?trm(3), + data = #domain_Terminal{ + name = <<"Brominal 3">>, + description = <<"Brominal 3">>, + provider_ref = ?prv(1), + terms = #domain_ProvisionTermSet{ + payments = payment_provision_terms(<<"EUR">>, 3) + } + } + }}, + {terminal, #domain_TerminalObject{ + ref = ?trm(4), + data = #domain_Terminal{ + name = <<"Brominal 4">>, + description = <<"Brominal 4">>, + provider_ref = ?prv(1), + terms = #domain_ProvisionTermSet{ + payments = payment_provision_terms(<<"JPY">>, 4) + } + } + }}, + {terminal, #domain_TerminalObject{ + ref = ?trm(5), + data = #domain_Terminal{ + name = <<"Brominal 5">>, + description = <<"Brominal 5">>, + provider_ref = ?prv(1), + terms = #domain_ProvisionTermSet{ + payments = payment_provision_terms(<<"CNY">>, 5) + } + } + }}, + + hg_ct_fixture:construct_payment_system(?pmt_sys(<<"visa-ref">>), <<"visa payment system">>) + ]. + +%% Internals + +payment_service_terms() -> + #domain_PaymentsServiceTerms{ + currencies = + {value, ?ordset([?cur(<<"RUB">>), ?cur(<<"USD">>), ?cur(<<"EUR">>), ?cur(<<"JPY">>), ?cur(<<"CNY">>)])}, + categories = {value, ?ordset([?cat(1), ?cat(2), ?cat(3), ?cat(4), ?cat(5)])}, + payment_methods = {value, ?ordset([?pmt(bank_card, ?bank_card(<<"visa-ref">>))])}, + fees = { + value, + [ + ?cfpost( + {merchant, settlement}, + {system, settlement}, + ?share(45, 1000, operation_amount) + ) + ] + }, + refunds = #domain_PaymentRefundsServiceTerms{ + payment_methods = { + value, + ?ordset([ + ?pmt(bank_card, ?bank_card(<<"visa-ref">>)) + ]) + }, + eligibility_time = {value, #base_TimeSpan{minutes = 1}}, + fees = { + value, + [ + ?cfpost( + {merchant, settlement}, + {system, settlement}, + ?fixed(100, <<"RUB">>) + ) + ] + } + }, + chargebacks = #domain_PaymentChargebackServiceTerms{ + allow = {constant, true}, + fees = { + value, + [ + ?cfpost( + {merchant, settlement}, + {system, settlement}, + ?share(1, 1, surplus) + ) + ] + } + }, + cash_limit = { + value, + ?cashrng( + {inclusive, ?cash(10, <<"RUB">>)}, + {exclusive, ?cash(420000000, <<"RUB">>)} + ) + }, + attempt_limit = {value, #domain_AttemptLimit{attempts = 2}} + }. + +payment_provision_terms(Currency, Category) -> + #domain_PaymentsProvisionTerms{ + currencies = {value, ?ordset([?cur(Currency)])}, + categories = {value, ?ordset([?cat(Category)])}, + payment_methods = { + value, + ?ordset([ + ?pmt(bank_card, ?bank_card(<<"visa-ref">>)) + ]) + }, + cash_flow = { + value, + [ + ?cfpost( + {provider, settlement}, + {merchant, settlement}, + ?share(1, 1, operation_amount) + ), + ?cfpost( + {system, settlement}, + {provider, settlement}, + ?fixed(10, Currency) + ) + ] + }, + refunds = #domain_PaymentRefundsProvisionTerms{ + cash_flow = { + value, + [ + ?cfpost( + {merchant, settlement}, + {provider, settlement}, + ?share(1, 1, operation_amount) + ), + ?cfpost( + {system, settlement}, + {provider, settlement}, + ?fixed(10, Currency) + ) + ] + } + }, + chargebacks = #domain_PaymentChargebackProvisionTerms{ + cash_flow = { + value, + [ + ?cfpost( + {merchant, settlement}, + {provider, settlement}, + ?share(1, 1, operation_amount) + ), + ?cfpost( + {system, settlement}, + {provider, settlement}, + ?fixed(10, Currency) + ) + ] + } + } + }. diff --git a/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl new file mode 100644 index 00000000..c911f42c --- /dev/null +++ b/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl @@ -0,0 +1,598 @@ +-module(hg_invoice_exchange_tests_SUITE). + +-include_lib("hellgate/include/hg_invoice.hrl"). +-include_lib("hellgate/include/payment_events.hrl"). +-include_lib("hellgate/include/invoice_events.hrl"). +-include_lib("stdlib/include/assert.hrl"). +-include("hg_ct_domain.hrl"). +-include("hg_ct_invoice.hrl"). + +-export([all/0]). +-export([groups/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). +-export([init_per_group/2]). +-export([end_per_group/2]). +-export([init_per_testcase/2]). +-export([end_per_testcase/2]). + +%% Tests +-export([payment_success/1]). +-export([payment_and_refund_with_increased_cost/1]). +-export([accept_payment_chargeback_new_body/1]). +-export([payment_adjustment_success/1]). +-export([payment_failed_exchange_rate_unknown/1]). +-export([payment_failed_exchange_rate_timeout/1]). +-export([payment_failed_exchange_rate_unexpected/1]). + +-type config() :: hg_ct_helper:config(). +-type test_case_name() :: hg_ct_helper:test_case_name(). +-type group_name() :: hg_ct_helper:group_name(). +-type test_return() :: _ | no_return(). + +%% Supervisor +-behaviour(supervisor). + +-export([init/1]). + +-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init([]) -> + {ok, {#{strategy => one_for_all, intensity => 1, period => 1}, []}}. + +-spec all() -> [test_case_name() | {group, group_name()}]. +all() -> + [ + payment_success, + payment_and_refund_with_increased_cost, + accept_payment_chargeback_new_body, + payment_adjustment_success, + payment_failed_exchange_rate_unknown, + payment_failed_exchange_rate_timeout, + payment_failed_exchange_rate_unexpected + ]. + +-spec groups() -> [{group_name(), list(), [test_case_name()]}]. +groups() -> + []. + +-spec init_per_suite(config()) -> config(). +init_per_suite(C) -> + CowboySpec = hg_dummy_provider:get_http_cowboy_spec(), + {Apps, Ret} = hg_ct_helper:start_apps([ + woody, + scoper, + dmt_client, + bender_client, + party_client, + hg_proto, + epg_connector, + progressor, + hellgate, + {cowboy, CowboySpec}, + snowflake + ]), + RootUrl = maps:get(hellgate_root_url, Ret), + _ = hg_limiter_helper:init_per_suite(C), + _ = hg_domain:upsert(hg_invoice_dummy_data:construct_domain_fixture()), + PartyConfigRef = #domain_PartyConfigRef{id = hg_utils:unique_id()}, + PartyClient = {party_client:create_client(), party_client:create_context()}, + ok = hg_context:save(hg_context:create()), + ShopConfigRef = hg_ct_helper:create_party_and_shop( + PartyConfigRef, ?cat(2), <<"RUB">>, ?trms(1), ?pinst(1), PartyClient + ), + ShopConfigRefEur = hg_ct_helper:create_party_and_shop( + PartyConfigRef, ?cat(3), <<"RUB">>, ?trms(1), ?pinst(1), PartyClient + ), + ShopConfigRefJpy = hg_ct_helper:create_party_and_shop( + PartyConfigRef, ?cat(4), <<"RUB">>, ?trms(1), ?pinst(1), PartyClient + ), + ShopConfigRefCny = hg_ct_helper:create_party_and_shop( + PartyConfigRef, ?cat(5), <<"RUB">>, ?trms(1), ?pinst(1), PartyClient + ), + ok = hg_context:cleanup(), + {ok, SupPid} = supervisor:start_link(?MODULE, []), + _ = unlink(SupPid), + ok = hg_invoice_helper:start_kv_store(SupPid), + _ = start_exchange_service_handler([{test_sup, SupPid} | C]), + NewC = [ + {party_config_ref, PartyConfigRef}, + {shop_config_ref, ShopConfigRef}, + {shop_config_ref_eur, ShopConfigRefEur}, + {shop_config_ref_jpy, ShopConfigRefJpy}, + {shop_config_ref_cny, ShopConfigRefCny}, + {root_url, RootUrl}, + {test_sup, SupPid}, + {apps, Apps} + | C + ], + ok = hg_invoice_helper:start_proxies([{hg_dummy_provider, 1, NewC}, {hg_dummy_inspector, 2, NewC}]), + NewC. + +-spec end_per_suite(config()) -> _. +end_per_suite(C) -> + _ = hg_domain:cleanup(), + _ = application:stop(progressor), + _ = hg_progressor:cleanup(), + _ = [application:stop(App) || App <- cfg(apps, C)], + hg_invoice_helper:stop_kv_store(cfg(test_sup, C)), + exit(cfg(test_sup, C), shutdown). + +-spec init_per_group(group_name(), config()) -> config(). +init_per_group(_, C) -> + C. + +-spec end_per_group(group_name(), config()) -> _. +end_per_group(_Group, _C) -> + ok. + +-spec init_per_testcase(test_case_name(), config()) -> config(). +init_per_testcase(_, C) -> + ApiClient = hg_ct_helper:create_client(hg_ct_helper:cfg(root_url, C)), + Client = hg_client_invoicing:start_link(ApiClient), + ok = hg_context:save(hg_context:create()), + [ + {client, Client} + | C + ]. + +-spec end_per_testcase(test_case_name(), config()) -> config(). +end_per_testcase(_, C) -> + C. + +%% TESTS + +-spec payment_success(config()) -> test_return(). +payment_success(C) -> + Client = cfg(client, C), + + InvoiceID = hg_invoice_helper:start_invoice(<<"rubberduck">>, hg_invoice_helper:make_due_date(10), 42000, C), + PaymentParams = hg_invoice_helper:make_payment_params(?pmt_sys(<<"visa-ref">>)), + PaymentID = hg_invoice_helper:process_payment(InvoiceID, PaymentParams, Client), + PaymentID = hg_invoice_helper:await_payment_capture(InvoiceID, PaymentID, Client), + #payproc_Invoice{payments = [Payment]} = hg_client_invoicing:get(InvoiceID, Client), + #payproc_InvoicePayment{cash_flow = CashFlow} = Payment, + [ + #domain_FinalCashFlowPosting{ + volume = ConvertedCash, + exchange_context = ExchangeContext + } + ] = lookup_posting(CashFlow, {system, settlement}, {provider, settlement}), + %% Expected Cash: converted 800 RUB from fixed 10 USD + ExpectCash = #domain_Cash{amount = 800, currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>}}, + ?assertEqual(ExpectCash, ConvertedCash), + ExpectContext = #domain_ExchangeContext{ + source_currency = <<"RUB">>, + destination_currency = <<"USD">>, + exchange_rate = #base_Rational{p = 80, q = 1} + }, + ?assertEqual(ExpectContext, ExchangeContext), + ok. + +-spec payment_and_refund_with_increased_cost(config()) -> test_return(). +payment_and_refund_with_increased_cost(C) -> + Client = cfg(client, C), + + % top up merchant account + InvoiceID2 = hg_invoice_helper:start_invoice( + <<"rubberduck">>, + hg_invoice_helper:make_due_date(10), + 42800, + C + ), + _PaymentID2 = hg_invoice_helper:execute_payment( + InvoiceID2, + hg_invoice_helper:make_payment_params(?pmt_sys(<<"visa-ref">>)), + Client + ), + + InvoiceID = hg_invoice_helper:start_invoice(<<"rubberduck">>, hg_invoice_helper:make_due_date(10), 42000, C), + {PaymentTool, Session} = hg_dummy_provider:make_payment_tool( + change_currency_and_increase, + ?pmt_sys(<<"visa-ref">>) + ), + PaymentParams = hg_invoice_helper:make_payment_params(PaymentTool, Session, instant), + PaymentID = hg_invoice_helper:start_payment(InvoiceID, PaymentParams, Client), + PaymentID = hg_invoice_helper:await_payment_session_started(InvoiceID, PaymentID, Client, ?processed()), + [ + ?payment_ev(PaymentID, ?session_ev(?processed(), ?trx_bound(?trx_info(_)))), + ?payment_ev(PaymentID, ?cash_changed(_, _)), + ?payment_ev(PaymentID, ?session_ev(?processed(), ?session_finished(?session_succeeded()))), + ?payment_ev(PaymentID, ?cash_flow_changed(_)), + ?payment_ev(PaymentID, ?payment_status_changed(?processed())) + ] = hg_invoice_helper:next_changes(InvoiceID, 5, Client), + PaymentID = hg_invoice_helper:await_payment_capture(InvoiceID, PaymentID, Client), + #payproc_Invoice{ + payments = [ + #payproc_InvoicePayment{ + payment = #domain_InvoicePayment{ + changed_cost = + #domain_Cash{ + amount = ChangedAmount, + currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>} + } = ChangedCost + }, + route = Route, + cash_flow = CashFlow + } + ] + } = hg_client_invoicing:get(InvoiceID, Client), + %% changed cost: 42800 = 42000 RUB + (10 USD * 80) + ?assertEqual(42800, ChangedAmount), + [ + #domain_FinalCashFlowPosting{ + volume = #domain_Cash{ + amount = ChangedVolumeAmount, + currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>} + }, + exchange_context = _ExchangeContext + } + ] = lookup_posting(CashFlow, {provider, settlement}, {merchant, settlement}), + ?assertEqual(42800, ChangedVolumeAmount), + + CFContext = hg_invoice_helper:construct_ta_context(cfg(party_config_ref, C), cfg(shop_config_ref, C), Route), + PrvAccount1 = get_deprecated_cashflow_account({provider, settlement}, CashFlow, CFContext), + SysAccount1 = get_deprecated_cashflow_account({system, settlement}, CashFlow, CFContext), + MrcAccount1 = get_deprecated_cashflow_account({merchant, settlement}, CashFlow, CFContext), + + %% payment with changed cost are captured + %% now refund it + + RefundParams = make_refund_params(), + % create a refund finally + RefundID = hg_invoice_helper:execute_payment_refund(InvoiceID, PaymentID, RefundParams, Client), + #domain_InvoicePaymentRefund{status = ?refund_succeeded()} = + hg_client_invoicing:get_payment_refund(InvoiceID, PaymentID, RefundID, Client), + + % no more refunds for you + {exception, #payproc_InvalidPaymentStatus{status = ?refunded()}} = + hg_client_invoicing:refund_payment(InvoiceID, PaymentID, RefundParams, Client), + + Context = #{operation_amount => ChangedCost}, + PrvAccount2 = get_deprecated_cashflow_account({provider, settlement}, CashFlow, CFContext), + SysAccount2 = get_deprecated_cashflow_account({system, settlement}, CashFlow, CFContext), + MrcAccount2 = get_deprecated_cashflow_account({merchant, settlement}, CashFlow, CFContext), + #domain_Cash{amount = MrcAmountFixed} = hg_cashflow:compute_volume(?fixed(100, <<"RUB">>), Context), + #domain_Cash{amount = PrvAmountFixed} = hg_cashflow:compute_volume(?fixed(800, <<"RUB">>), Context), + ?assertEqual( + maps:get(own_amount, MrcAccount2), + maps:get(own_amount, MrcAccount1) - ChangedAmount - MrcAmountFixed + ), + ?assertEqual( + maps:get(own_amount, PrvAccount2), + maps:get(own_amount, PrvAccount1) + ChangedAmount + PrvAmountFixed + ), + ?assertEqual( + MrcAmountFixed - PrvAmountFixed, + maps:get(own_amount, SysAccount2) - maps:get(own_amount, SysAccount1) + ), + ok. + +-spec accept_payment_chargeback_new_body(config()) -> _ | no_return(). +accept_payment_chargeback_new_body(C) -> + %% new shop with new balances + PartyConfigRef = cfg(party_config_ref, C), + PartyPair = cfg(party_client, C), + UpdShopConfigRef = + hg_ct_helper:create_battle_ready_shop(PartyConfigRef, ?cat(2), <<"RUB">>, ?trms(1), ?pinst(1), PartyPair), + NewC = lists:keyreplace(shop_config_ref, 1, C, {shop_config_ref, UpdShopConfigRef}), + + Client = cfg(client, NewC), + Cost = 42000, + Fee = 1890, + Paid = Cost - Fee, + LevyAmount = 5000, + Levy = ?cash(LevyAmount, <<"RUB">>), + CBParams = make_chargeback_params(Levy), + PaymentParams = hg_invoice_helper:make_payment_params(?pmt_sys(<<"visa-ref">>)), + ExchangeContext = #domain_ExchangeContext{ + source_currency = <<"RUB">>, + destination_currency = <<"USD">>, + exchange_rate = #base_Rational{p = 80, q = 1} + }, + {IID, PID, SID, CB} = + hg_invoice_helper:start_chargeback(NewC, Cost, CBParams, PaymentParams), + CBID = CB#domain_InvoicePaymentChargeback.id, + [ + ?payment_ev(PID, ?chargeback_ev(CBID, ?chargeback_created(CB))), + ?payment_ev(PID, ?chargeback_ev(CBID, ?chargeback_cash_flow_changed(CF1))) + ] = hg_invoice_helper:next_changes(IID, 2, Client), + %% shared cash volume + [ + #domain_FinalCashFlowPosting{ + volume = #domain_Cash{ + amount = Cost, + currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>} + }, + exchange_context = ExchangeContext + } + ] = lookup_posting(CF1, {merchant, settlement}, {provider, settlement}), + %% fixed cash volume + [ + #domain_FinalCashFlowPosting{ + volume = #domain_Cash{ + amount = 800, + currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>} + }, + exchange_context = ExchangeContext + } + ] = lookup_posting(CF1, {system, settlement}, {provider, settlement}), + Settlement0 = hg_accounting:get_balance(SID), + Body = 40000, + AcceptParams = make_chargeback_accept_params(undefined, ?cash(Body, <<"RUB">>)), + ok = hg_client_invoicing:accept_chargeback(IID, PID, CBID, AcceptParams, Client), + [ + ?payment_ev(PID, ?chargeback_ev(CBID, ?chargeback_body_changed(_))), + ?payment_ev(PID, ?chargeback_ev(CBID, ?chargeback_target_status_changed(?chargeback_status_accepted()))), + ?payment_ev(PID, ?chargeback_ev(CBID, ?chargeback_cash_flow_changed(CF2))), + ?payment_ev(PID, ?chargeback_ev(CBID, ?chargeback_status_changed(?chargeback_status_accepted()))) + ] = hg_invoice_helper:next_changes(IID, 4, Client), + %% shared cash volume + [ + #domain_FinalCashFlowPosting{ + volume = #domain_Cash{ + amount = Body, + currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>} + }, + exchange_context = ExchangeContext + } + ] = lookup_posting(CF2, {merchant, settlement}, {provider, settlement}), + %% fixed cash volume + [ + #domain_FinalCashFlowPosting{ + volume = #domain_Cash{ + amount = 800, + currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>} + }, + exchange_context = ExchangeContext + } + ] = lookup_posting(CF2, {system, settlement}, {provider, settlement}), + Settlement1 = hg_accounting:get_balance(SID), + + ?assertEqual(Paid - Cost - LevyAmount, maps:get(min_available_amount, Settlement0)), + ?assertEqual(Paid, maps:get(max_available_amount, Settlement0)), + ?assertEqual(Paid - Body - LevyAmount, maps:get(min_available_amount, Settlement1)), + ?assertEqual(Paid - Body - LevyAmount, maps:get(max_available_amount, Settlement1)), + ok. + +-spec payment_adjustment_success(config()) -> test_return(). +payment_adjustment_success(C) -> + %% old cf : + %% merch - 1890 -> syst + %% prov - 42000 -> merch (with exchange) + %% syst - 800 -> prov (fixed, with exchange from 10 USD) + %% + %% new cf : + %% merch - 1890 -> syst + %% prov - 42000 -> merch (with exchange) + %% syst - 400 -> prov (fixed, with exchange from 5 USD) + Client = cfg(client, C), + + InvoiceID = hg_invoice_helper:start_invoice(<<"rubberduck">>, hg_invoice_helper:make_due_date(10), 42000, C), + PaymentParams = hg_invoice_helper:make_payment_params(?pmt_sys(<<"visa-ref">>)), + PaymentID = hg_invoice_helper:process_payment(InvoiceID, PaymentParams, Client), + PaymentID = hg_invoice_helper:await_payment_capture(InvoiceID, PaymentID, Client), + ExchangeContext = #domain_ExchangeContext{ + source_currency = <<"RUB">>, + destination_currency = <<"USD">>, + exchange_rate = #base_Rational{p = 80, q = 1} + }, + #payproc_Invoice{ + payments = [ + #payproc_InvoicePayment{ + route = Route, + cash_flow = CF1 + } + ] + } = hg_client_invoicing:get(InvoiceID, Client), + %% System -> Provider fixed cash volume 800 RUB (10 USD * 80) + [ + #domain_FinalCashFlowPosting{ + volume = #domain_Cash{ + amount = 800, + currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>} + }, + exchange_context = ExchangeContext + } + ] = lookup_posting(CF1, {system, settlement}, {provider, settlement}), + + CFContext = hg_invoice_helper:construct_ta_context(cfg(party_config_ref, C), cfg(shop_config_ref, C), Route), + PrvAccount1 = get_deprecated_cashflow_account({provider, settlement}, CF1, CFContext), + SysAccount1 = get_deprecated_cashflow_account({system, settlement}, CF1, CFContext), + MrcAccount1 = get_deprecated_cashflow_account({merchant, settlement}, CF1, CFContext), + + %% update terminal cashflow + ok = update_payment_terms_cashflow(?trm(2)), + + %% make an adjustment + Params = make_adjustment_params(Reason = <<"imdrunk">>), + ?adjustment(AdjustmentID, ?adjustment_pending()) = + Adjustment = + hg_client_invoicing:create_payment_adjustment(InvoiceID, PaymentID, Params, Client), + Adjustment = + #domain_InvoicePaymentAdjustment{id = AdjustmentID, reason = Reason} = + hg_client_invoicing:get_payment_adjustment(InvoiceID, PaymentID, AdjustmentID, Client), + ?payment_ev(PaymentID, ?adjustment_ev(AdjustmentID, ?adjustment_created(Adjustment))) = + hg_invoice_helper:next_change(InvoiceID, Client), + [ + ?payment_ev(PaymentID, ?adjustment_ev(AdjustmentID, ?adjustment_status_changed(?adjustment_processed()))), + ?payment_ev(PaymentID, ?adjustment_ev(AdjustmentID, ?adjustment_status_changed(?adjustment_captured(_)))) + ] = hg_invoice_helper:next_changes(InvoiceID, 2, Client), + %% verify that cash deposited correctly everywhere + #domain_InvoicePaymentAdjustment{new_cash_flow = DCF2} = Adjustment, + %% System -> Provider change fixed cash volume 400 RUB (5 USD * 80) + [ + #domain_FinalCashFlowPosting{ + volume = #domain_Cash{ + amount = 400, + currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>} + }, + exchange_context = ExchangeContext + } + ] = lookup_posting(DCF2, {system, settlement}, {provider, settlement}), + + PrvAccount2 = get_deprecated_cashflow_account({provider, settlement}, DCF2, CFContext), + SysAccount2 = get_deprecated_cashflow_account({system, settlement}, DCF2, CFContext), + MrcAccount2 = get_deprecated_cashflow_account({merchant, settlement}, DCF2, CFContext), + + 0 = MrcDiff = maps:get(own_amount, MrcAccount2) - maps:get(own_amount, MrcAccount1), + -400 = PrvDiff = maps:get(own_amount, PrvAccount2) - maps:get(own_amount, PrvAccount1), + SysDiff = MrcDiff - PrvDiff, + SysDiff = maps:get(own_amount, SysAccount2) - maps:get(own_amount, SysAccount1), + ok. + +-spec payment_failed_exchange_rate_unknown(config()) -> test_return(). +payment_failed_exchange_rate_unknown(C) -> + Client = cfg(client, C), + + InvoiceID = hg_invoice_helper:start_invoice( + cfg(shop_config_ref_eur, C), + <<"rubberduck">>, + hg_invoice_helper:make_due_date(10), + 42000, + C + ), + PaymentParams = hg_invoice_helper:make_payment_params(?pmt_sys(<<"visa-ref">>)), + ?payment_state(?payment(PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), + _ = hg_invoice_helper:start_payment_ev(InvoiceID, Client), + [ + ?payment_ev(PaymentID, ?payment_rollback_started(_)), + ?payment_ev(PaymentID, ?payment_status_changed(?failed(_))) + ] = hg_invoice_helper:next_changes(InvoiceID, 2, Client), + ok. + +-spec payment_failed_exchange_rate_timeout(config()) -> test_return(). +payment_failed_exchange_rate_timeout(C) -> + Client = cfg(client, C), + + InvoiceID = hg_invoice_helper:start_invoice( + cfg(shop_config_ref_jpy, C), + <<"rubberduck">>, + hg_invoice_helper:make_due_date(10), + 42000, + C + ), + PaymentParams = hg_invoice_helper:make_payment_params(?pmt_sys(<<"visa-ref">>)), + ?payment_state(?payment(PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), + _ = hg_invoice_helper:start_payment_ev(InvoiceID, Client), + [ + ?payment_ev(PaymentID, ?payment_rollback_started(_)), + ?payment_ev(PaymentID, ?payment_status_changed(?failed(_))) + ] = hg_invoice_helper:next_changes(InvoiceID, 2, Client), + ok. + +-spec payment_failed_exchange_rate_unexpected(config()) -> test_return(). +payment_failed_exchange_rate_unexpected(C) -> + Client = cfg(client, C), + + InvoiceID = hg_invoice_helper:start_invoice( + cfg(shop_config_ref_cny, C), + <<"rubberduck">>, + hg_invoice_helper:make_due_date(10), + 42000, + C + ), + PaymentParams = hg_invoice_helper:make_payment_params(?pmt_sys(<<"visa-ref">>)), + ?payment_state(?payment(PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), + _ = hg_invoice_helper:start_payment_ev(InvoiceID, Client), + [ + ?payment_ev(PaymentID, ?payment_rollback_started(_)), + ?payment_ev(PaymentID, ?payment_status_changed(?failed(_))) + ] = hg_invoice_helper:next_changes(InvoiceID, 2, Client), + ok. + +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% +% Internals +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% + +start_exchange_service_handler(C) -> + Module = hg_dummy_exrates, + IP = "127.0.0.1", + Port = 32022, + ChildSpec = hg_test_proxy:get_child_spec(Module, Module, IP, Port, #{}), + {ok, _} = supervisor:start_child(hg_ct_helper:cfg(test_sup, C), ChildSpec), + hg_test_proxy:get_url(Module, IP, Port). + +cfg(Key, Config) -> + hg_ct_helper:cfg(Key, Config). + +make_chargeback_params(Levy) -> + #payproc_InvoicePaymentChargebackParams{ + id = hg_utils:unique_id(), + reason = #domain_InvoicePaymentChargebackReason{ + code = <<"CB.C0DE">>, + category = {fraud, #domain_InvoicePaymentChargebackCategoryFraud{}} + }, + levy = Levy, + occurred_at = hg_datetime:format_now() + }. + +make_chargeback_accept_params(Levy, Body) -> + #payproc_InvoicePaymentChargebackAcceptParams{ + body = Body, + levy = Levy + }. + +make_refund_params() -> + #payproc_InvoicePaymentRefundParams{ + reason = <<"ZANOZED">> + }. + +make_adjustment_params(Reason) -> + make_adjustment_params(Reason, undefined, undefined). + +make_adjustment_params(Reason, Revision, Amount) -> + #payproc_InvoicePaymentAdjustmentParams{ + reason = Reason, + scenario = + {cash_flow, #domain_InvoicePaymentAdjustmentCashFlow{ + domain_revision = Revision, + new_amount = Amount + }} + }. + +get_deprecated_cashflow_account(Type, CF, CFContext) -> + hg_invoice_helper:get_deprecated_cashflow_account(Type, CF, CFContext). + +update_payment_terms_cashflow(TerminalRef) -> + NewCashFlow = [ + ?cfpost( + {provider, settlement}, + {merchant, settlement}, + ?share(1, 1, operation_amount) + ), + ?cfpost( + {system, settlement}, + {provider, settlement}, + ?fixed(5, <<"USD">>) + ) + ], + Terminal = hg_domain:get({terminal, TerminalRef}), + TerminalTerms = Terminal#domain_Terminal.terms, + PaymentTerms = TerminalTerms#domain_ProvisionTermSet.payments, + NewTerminal = Terminal#domain_Terminal{ + terms = TerminalTerms#domain_ProvisionTermSet{ + payments = PaymentTerms#domain_PaymentsProvisionTerms{ + cash_flow = {value, NewCashFlow} + } + } + }, + _ = hg_domain:upsert( + {terminal, #domain_TerminalObject{ + ref = TerminalRef, + data = NewTerminal + }} + ), + ok. + +lookup_posting(CashFlow, Source, Destination) -> + lists:filter( + fun( + #domain_FinalCashFlowPosting{ + source = #domain_FinalCashFlowAccount{account_type = SourceAcc}, + destination = #domain_FinalCashFlowAccount{account_type = DestinationAcc} + } + ) -> + Source =:= SourceAcc andalso Destination =:= DestinationAcc + end, + CashFlow + ). diff --git a/apps/hellgate/test/hg_invoice_helper.erl b/apps/hellgate/test/hg_invoice_helper.erl index 9bf3895b..c9b666f1 100644 --- a/apps/hellgate/test/hg_invoice_helper.erl +++ b/apps/hellgate/test/hg_invoice_helper.erl @@ -9,7 +9,8 @@ -export([ start_kv_store/1, stop_kv_store/1, - start_proxies/1 + start_proxies/1, + start_service_handler/3 ]). -export([ @@ -44,7 +45,10 @@ process_payment/3, get_payment_cost/3, make_cash/1, - make_cash/2 + make_cash/2, + execute_payment_refund/4, + get_deprecated_cashflow_account/3, + start_chargeback/4 ]). cfg(Key, C) -> @@ -83,6 +87,7 @@ start_proxies(Proxies) -> ) ). +-spec start_service_handler(_, _, _) -> binary(). start_service_handler(Module, C, HandlerOpts) -> start_service_handler(Module, Module, C, HandlerOpts). @@ -252,9 +257,13 @@ register_payment(InvoiceID, RegisterPaymentParams, WithRiskScoring, Client) -> start_payment(InvoiceID, PaymentParams, Client) -> ?payment_state(?payment(PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), _ = start_payment_ev(InvoiceID, Client), - ?payment_ev(PaymentID, ?cash_flow_changed(_)) = - next_change(InvoiceID, Client), - PaymentID. + case next_change(InvoiceID, Client) of + ?payment_ev(PaymentID, ?cash_flow_changed(_)) -> + PaymentID; + ?payment_ev(PaymentID, ?payment_currency_changed(_, _, _)) -> + ?payment_ev(PaymentID, ?cash_flow_changed(_)) = next_change(InvoiceID, Client), + PaymentID + end. -spec start_payment_ev(_, _) -> _. start_payment_ev(InvoiceID, Client) -> @@ -429,3 +438,71 @@ await_payment_process_finish(InvoiceID, PaymentID, Client) -> ?payment_ev(PaymentID, ?payment_status_changed(?processed())) ] = next_changes(InvoiceID, 3, Client), PaymentID. + +-spec execute_payment_refund(_, _, _, _) -> _. +execute_payment_refund(InvoiceID, PaymentID, #payproc_InvoicePaymentRefundParams{cash = undefined} = Params, Client) -> + execute_payment_refund_complete(InvoiceID, PaymentID, Params, Client). + +execute_payment_refund_complete(InvoiceID, PaymentID, Params, Client) -> + ?refund_id(RefundID) = hg_client_invoicing:refund_payment(InvoiceID, PaymentID, Params, Client), + PaymentID = await_refund_created(InvoiceID, PaymentID, RefundID, Client), + PaymentID = await_refund_session_started(InvoiceID, PaymentID, RefundID, Client), + PaymentID = await_refund_payment_complete(InvoiceID, PaymentID, Client), + RefundID. + +await_refund_created(InvoiceID, PaymentID, RefundID, Client) -> + ?payment_ev(PaymentID, ?refund_ev(RefundID, ?refund_created(_Refund, _))) = + next_change(InvoiceID, Client), + PaymentID. + +await_refund_session_started(InvoiceID, PaymentID, RefundID, Client) -> + ?payment_ev(PaymentID, ?refund_ev(RefundID, ?session_ev(?refunded(), ?session_started()))) = + next_change(InvoiceID, Client), + PaymentID. + +await_refund_payment_complete(InvoiceID, PaymentID, Client) -> + PaymentID = await_sessions_restarts(PaymentID, ?refunded(), InvoiceID, Client, 0), + [ + ?payment_ev(PaymentID, ?refund_ev(_, ?session_ev(?refunded(), ?trx_bound(_)))), + ?payment_ev(PaymentID, ?refund_ev(_, ?session_ev(?refunded(), ?session_finished(?session_succeeded())))), + ?payment_ev(PaymentID, ?refund_ev(_, ?refund_status_changed(?refund_succeeded()))), + ?payment_ev(PaymentID, ?payment_status_changed(?refunded())) + ] = next_changes(InvoiceID, 4, Client), + PaymentID. + +await_sessions_restarts(PaymentID, _Target, _InvoiceID, _Client, 0) -> + PaymentID. + +-spec get_deprecated_cashflow_account(_, _, _) -> _. +get_deprecated_cashflow_account(Type, CF, CFContext) -> + ID = get_deprecated_cashflow_account_id(Type, CF, CFContext), + hg_accounting:get_balance(ID). + +get_deprecated_cashflow_account_id(Type, CF, CFContext) -> + Account = convert_transaction_account(Type, CFContext), + [ID] = [ + V + || #domain_FinalCashFlowPosting{ + destination = #domain_FinalCashFlowAccount{ + account_id = V, + account_type = T, + transaction_account = A + } + } <- CF, + T == Type, + A == Account + ], + ID. + +-spec start_chargeback(_, _, _, _) -> _. +start_chargeback(C, Cost, CBParams, PaymentParams) -> + Client = cfg(client, C), + PartyConfigRef = cfg(party_config_ref, C), + ShopConfigRef = cfg(shop_config_ref, C), + {PartyConfigRef, _Party} = hg_party:get_party(PartyConfigRef), + {ShopConfigRef, Shop} = hg_party:get_shop(ShopConfigRef, PartyConfigRef), + {SettlementID, _} = hg_invoice_utils:get_shop_account(Shop), + InvoiceID = start_invoice(ShopConfigRef, <<"rubberduck">>, make_due_date(10), Cost, C), + PaymentID = execute_payment(InvoiceID, PaymentParams, Client), + Chargeback = hg_client_invoicing:create_chargeback(InvoiceID, PaymentID, CBParams, Client), + {InvoiceID, PaymentID, SettlementID, Chargeback}. diff --git a/apps/routing/src/hg_route_collector.erl b/apps/routing/src/hg_route_collector.erl index 0ab435a7..2a8c0da0 100644 --- a/apps/routing/src/hg_route_collector.erl +++ b/apps/routing/src/hg_route_collector.erl @@ -398,7 +398,14 @@ try_accept_term(ParentName, Name, Value, Selector) -> test_term(Name, Value, Values) orelse throw(?rejected({ParentName, Name})). test_term(currency, V, Vs) -> - ordsets:is_element(V, Vs); + case application:get_env(hellgate, currency_exchange_enabled, false) of + true -> + %% currency conversion is allowed + %% ignore the currency difference + true; + false -> + ordsets:is_element(V, Vs) + end; test_term(category, V, Vs) -> ordsets:is_element(V, Vs); test_term(payment_tool, PT, PMs) -> diff --git a/rebar.lock b/rebar.lock index 7b26950d..85e4b239 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,"e139ec58580d5433f87f04607fdaf56a3445970c"}}, + {ref,"9b725828e809190b01d5e53f4b49de6b9262057f"}}, 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt-client.git", From c886a22bef6c3f13bdd58789a08ac6b23f7325b0 Mon Sep 17 00:00:00 2001 From: ttt161 Date: Mon, 22 Jun 2026 00:04:15 +0300 Subject: [PATCH 3/8] fix test config --- apps/hellgate/test/hg_ct_helper.erl | 1 - apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/hellgate/test/hg_ct_helper.erl b/apps/hellgate/test/hg_ct_helper.erl index e8c99cca..0bceec03 100644 --- a/apps/hellgate/test/hg_ct_helper.erl +++ b/apps/hellgate/test/hg_ct_helper.erl @@ -209,7 +209,6 @@ start_app(hellgate = AppName) -> pre_aggregation_size => 2 } }}, - {currency_exchange_enabled, true}, {backend, progressor} ]), #{ diff --git a/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl index c911f42c..72975ee5 100644 --- a/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl @@ -71,6 +71,7 @@ init_per_suite(C) -> {cowboy, CowboySpec}, snowflake ]), + application:set_env(hellgate, currency_exchange_enabled, true), RootUrl = maps:get(hellgate_root_url, Ret), _ = hg_limiter_helper:init_per_suite(C), _ = hg_domain:upsert(hg_invoice_dummy_data:construct_domain_fixture()), From 98f07e56bc4e940e24f299048efd2cb5b69a98d5 Mon Sep 17 00:00:00 2001 From: ttt161 Date: Mon, 22 Jun 2026 07:31:50 +0300 Subject: [PATCH 4/8] add exchange context to registered payment --- apps/hellgate/src/hg_cashflow.erl | 7 ---- apps/hellgate/src/hg_invoice_payment.erl | 12 ++++--- .../src/hg_invoice_registered_payment.erl | 35 ++++++++++++++++--- .../test/hg_invoice_exchange_tests_SUITE.erl | 1 + 4 files changed, 40 insertions(+), 15 deletions(-) diff --git a/apps/hellgate/src/hg_cashflow.erl b/apps/hellgate/src/hg_cashflow.erl index cf85c0ee..eec04034 100644 --- a/apps/hellgate/src/hg_cashflow.erl +++ b/apps/hellgate/src/hg_cashflow.erl @@ -44,13 +44,6 @@ %% -%-define(posting(Source, Destination, Volume, Details), #domain_CashFlowPosting{ -% source = Source, -% destination = Destination, -% volume = Volume, -% details = Details -%}). - -define(posting(Source, Destination, Volume, Details, ExchangeContext), #domain_CashFlowPosting{ source = Source, destination = Destination, diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index 9c729071..34336e46 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -86,6 +86,8 @@ -export([accrue_status_timing/3]). -export([get_limit_values/2]). +-export([get_exchange_context/2]). + %% Machine like -export([init/3]). @@ -239,9 +241,11 @@ exchange_context => exchange_context() | undefined }. +-type currency_code() :: dmsl_domain_thrift:'CurrencySymbolicCode'(). + -type exchange_context() :: #{ - source := dmsl_domain_thrift:'CurrencySymbolicCode'(), - destination := dmsl_domain_thrift:'CurrencySymbolicCode'(), + source := currency_code(), + destination := currency_code(), rate := dmsl_base_thrift:'Rational'() }. @@ -2207,9 +2211,7 @@ process_currency_exchange(Action, St) -> {next, {[?payment_rollback_started({failure, Failure})], hg_machine_action:set_timeout(0, Action)}} end. -%build_exchange_context(#{provider_ref := ProviderRef, terminal_ref := TerminalRef}, VS, Revision, St) -> build_exchange_context(Route, VS, Revision, St) -> - %Route = ?route(ProviderRef, TerminalRef), #domain_Cash{ currency = #domain_CurrencyRef{symbolic_code = PaymentCurrency} } = get_payment_cost(get_payment(St)), @@ -2223,6 +2225,8 @@ build_exchange_context(Route, VS, Revision, St) -> get_exchange_context(PaymentCurrency, TerminalCurrency) end. +-spec get_exchange_context(currency_code(), currency_code()) -> + {ok, exchange_context()} | {error, _Reason}. get_exchange_context(SrcCurrency, DstCurrency) -> case hg_exrates:get_exchange_rate(SrcCurrency, DstCurrency) of {ok, #{p := P, q := Q}} -> diff --git a/apps/hellgate/src/hg_invoice_registered_payment.erl b/apps/hellgate/src/hg_invoice_registered_payment.erl index bb6df5da..bb632fad 100644 --- a/apps/hellgate/src/hg_invoice_registered_payment.erl +++ b/apps/hellgate/src/hg_invoice_registered_payment.erl @@ -73,6 +73,8 @@ init_(PaymentID, Params, #{timestamp := CreatedAt0} = Opts) -> MerchantTerms = get_merchant_payment_terms(Revision, Shop, VS), ProviderTerms = hg_invoice_payment:get_provider_terminal_terms(Route, VS, Revision), + {MaybeExchangeContext, MaybeCurrencyChangedEvents} = maybe_get_exchange_context(Cost1, ProviderTerms), + CashflowContext = #{ provision_terms => ProviderTerms, merchant_terms => MerchantTerms, @@ -80,8 +82,8 @@ init_(PaymentID, Params, #{timestamp := CreatedAt0} = Opts) -> payment => Payment, timestamp => CreatedAt1, varset => VS, - revision => Revision - % add exchange_context + revision => Revision, + exchange_context => MaybeExchangeContext }, FinalCashflow = hg_invoice_payment:calculate_cashflow(PaymentInstitution, CashflowContext, Opts), @@ -92,9 +94,9 @@ init_(PaymentID, Params, #{timestamp := CreatedAt0} = Opts) -> ?shop_limit_applied() ] ++ RiskScoreEventList ++ + [?route_changed(Route)] ++ + MaybeCurrencyChangedEvents ++ [ - ?route_changed(Route), - %% TODO currency_changed ?cash_flow_changed(FinalCashflow), hg_session:wrap_event(?processed(), hg_session:create()) ] ++ @@ -269,6 +271,31 @@ collect_validation_varset( flow => instant }. +maybe_get_exchange_context( + #domain_Cash{ + currency = #domain_CurrencyRef{symbolic_code = PaymentCurrency} + }, + #domain_PaymentsProvisionTerms{ + currencies = {value, [#domain_CurrencyRef{symbolic_code = TerminalCurrency}]} + } +) when PaymentCurrency =:= TerminalCurrency -> + {undefined, []}; +maybe_get_exchange_context( + #domain_Cash{ + currency = #domain_CurrencyRef{symbolic_code = PaymentCurrency} + }, + #domain_PaymentsProvisionTerms{ + currencies = {value, [#domain_CurrencyRef{symbolic_code = TerminalCurrency}]} + } +) -> + {ok, + #{ + source := Src, + destination := Dst, + rate := Rate + } = ExchangeContext} = hg_invoice_payment:get_exchange_context(PaymentCurrency, TerminalCurrency), + {ExchangeContext, [?payment_currency_changed(Src, Dst, Rate)]}. + %% get_party_config_ref(#{party_config_ref := PartyConfigRef}) -> diff --git a/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl index 72975ee5..e6cd2265 100644 --- a/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl @@ -78,6 +78,7 @@ init_per_suite(C) -> PartyConfigRef = #domain_PartyConfigRef{id = hg_utils:unique_id()}, PartyClient = {party_client:create_client(), party_client:create_context()}, ok = hg_context:save(hg_context:create()), + %% все магазины рублёвые, но каждая категория роутится на терминалы с раными валютами ShopConfigRef = hg_ct_helper:create_party_and_shop( PartyConfigRef, ?cat(2), <<"RUB">>, ?trms(1), ?pinst(1), PartyClient ), From 699539c2c6f9538b79eed6b06871788dc8b6811c Mon Sep 17 00:00:00 2001 From: ttt161 Date: Thu, 25 Jun 2026 18:37:15 +0300 Subject: [PATCH 5/8] fix issues --- apps/hellgate/include/payment_events.hrl | 8 +- apps/hellgate/src/hg_accounting.erl | 7 - apps/hellgate/src/hg_cashflow.erl | 29 +++- apps/hellgate/src/hg_cashflow_utils.erl | 82 +--------- apps/hellgate/src/hg_currency_converter.erl | 38 ++++- apps/hellgate/src/hg_invoice_payment.erl | 145 +++++------------- .../src/hg_invoice_payment_chargeback.erl | 25 +-- .../src/hg_invoice_payment_refund.erl | 47 ++---- .../src/hg_invoice_registered_payment.erl | 32 +--- apps/hellgate/src/hg_invoice_repair.erl | 1 - apps/hellgate/test/hg_invoice_dummy_data.erl | 66 ++------ .../test/hg_invoice_exchange_tests_SUITE.erl | 24 +-- apps/hellgate/test/hg_invoice_helper.erl | 14 +- apps/routing/src/hg_route.erl | 3 +- apps/routing/src/hg_route_collector.erl | 58 +++++-- compose.yaml | 4 +- rebar.lock | 2 +- 17 files changed, 198 insertions(+), 387 deletions(-) diff --git a/apps/hellgate/include/payment_events.hrl b/apps/hellgate/include/payment_events.hrl index 4dba0b36..180548e9 100644 --- a/apps/hellgate/include/payment_events.hrl +++ b/apps/hellgate/include/payment_events.hrl @@ -76,11 +76,9 @@ } ). --define(payment_currency_changed(SourceCurrency, DestinationCurrency, ExchangeRate), - {invoice_payment_currency_changed, #payproc_InvoicePaymentCurrencyChanged{ - source_currency = SourceCurrency, - destination_currency = DestinationCurrency, - exchange_rate = ExchangeRate +-define(invoice_payment_exchange_context_changed(ExchangeContext), + {invoice_payment_exchange_context_changed, #payproc_InvoicePaymentExchangeContextChanged{ + exchange_context = ExchangeContext }} ). diff --git a/apps/hellgate/src/hg_accounting.erl b/apps/hellgate/src/hg_accounting.erl index d2e938fa..94a0f00f 100644 --- a/apps/hellgate/src/hg_accounting.erl +++ b/apps/hellgate/src/hg_accounting.erl @@ -16,7 +16,6 @@ -export([collect_provider_account_map/4]). -export([collect_system_account_map/4]). -export([collect_external_account_map/4]). --export([empty_log/0]). -export([hold/2]). -export([plan/2]). @@ -75,12 +74,6 @@ max_available_amount => amount() }. --spec empty_log() -> posting_plan_log(). -empty_log() -> - #'accounter_PostingPlanLog'{ - affected_accounts = #{} - }. - -spec get_account(account_id()) -> account(). get_account(AccountID) -> Account = do_get_account(AccountID), diff --git a/apps/hellgate/src/hg_cashflow.erl b/apps/hellgate/src/hg_cashflow.erl index eec04034..258b5f37 100644 --- a/apps/hellgate/src/hg_cashflow.erl +++ b/apps/hellgate/src/hg_cashflow.erl @@ -32,10 +32,14 @@ -type shop_config_ref() :: dmsl_domain_thrift:'ShopConfigRef'(). -type party_config_ref() :: dmsl_domain_thrift:'PartyConfigRef'(). -type route() :: hg_route:payment_route(). +-type options() :: #{ + exchange_context => hg_invoice_payment:exchange_context() +}. %% -export([finalize/3]). +-export([finalize/4]). -export([revert/1]). -export([compute_volume/2]). @@ -44,12 +48,11 @@ %% --define(posting(Source, Destination, Volume, Details, ExchangeContext), #domain_CashFlowPosting{ +-define(posting(Source, Destination, Volume, Details), #domain_CashFlowPosting{ source = Source, destination = Destination, volume = Volume, - details = Details, - exchange_context = ExchangeContext + details = Details }). -define(final_posting(Source, Destination, Volume, Details), #domain_FinalCashFlowPosting{ @@ -69,21 +72,31 @@ -spec finalize(cash_flow(), context(), account_map()) -> final_cash_flow() | no_return(). finalize(CF, Context, AccountMap) -> - compute_postings(CF, Context, AccountMap). + finalize(CF, Context, AccountMap, #{}). --spec compute_postings(cash_flow(), context(), account_map()) -> final_cash_flow() | no_return(). -compute_postings(CF, Context, AccountMap) -> +-spec finalize(cash_flow(), context(), account_map(), options()) -> final_cash_flow() | no_return(). +finalize(CF, Context, AccountMap, Opts) -> + compute_postings(CF, Context, AccountMap, Opts). + +-spec compute_postings(cash_flow(), context(), account_map(), options()) -> final_cash_flow() | no_return(). +compute_postings(CF, Context, AccountMap, Opts) -> + ExchangeContext = maps:get(exchange_context, Opts, undefined), [ ?final_posting( construct_final_account(Source, AccountMap), construct_final_account(Destination, AccountMap), - compute_volume(Volume, Context), + maybe_convert_cash(ExchangeContext, compute_volume(Volume, Context)), Details, ExchangeContext ) - || ?posting(Source, Destination, Volume, Details, ExchangeContext) <- CF + || ?posting(Source, Destination, Volume, Details) <- CF ]. +maybe_convert_cash(undefined, Cash) -> + Cash; +maybe_convert_cash(ExchangeContext, Cash) -> + hg_currency_converter:reverse_convert_cash(ExchangeContext, Cash). + -spec construct_final_account(account(), account_map()) -> final_cash_flow_account() | no_return(). construct_final_account(AccountType, AccountMap) -> #domain_FinalCashFlowAccount{ diff --git a/apps/hellgate/src/hg_cashflow_utils.erl b/apps/hellgate/src/hg_cashflow_utils.erl index f167d0bc..8e5414bd 100644 --- a/apps/hellgate/src/hg_cashflow_utils.erl +++ b/apps/hellgate/src/hg_cashflow_utils.erl @@ -28,8 +28,6 @@ -export([collect_cashflow/1]). -export([collect_cashflow/2]). --export([convert_cashflow/2]). --export([convert_volume/2]). -type party() :: dmsl_domain_thrift:'PartyConfig'(). -type party_config_ref() :: dmsl_domain_thrift:'PartyConfigRef'(). @@ -42,8 +40,6 @@ -type revision() :: hg_domain:revision(). -type payment_institution() :: hg_payment_institution:t(). -type final_cash_flow() :: hg_cashflow:final_cash_flow(). --type cash_flow() :: hg_cashflow:cash_flow(). --type cash_volume() :: hg_cashflow:cash_volume(). -spec collect_cashflow(cash_flow_context()) -> final_cash_flow(). collect_cashflow(#{shop := {_, Shop}, varset := VS, revision := Revision} = Context) -> @@ -121,80 +117,16 @@ construct_transaction_cashflow( construct_provider_cashflow(PaymentInstitution, #{provision_terms := ProvisionTerms} = Context) -> ProviderCashflowSelector = get_provider_cashflow_selector(ProvisionTerms), - ProviderCashflow0 = get_selector_value(provider_payment_cash_flow, ProviderCashflowSelector), - ExchangeContext = maps:get(exchange_context, Context, undefined), - ProviderCashflow = maybe_convert_cashflow(ExchangeContext, ProviderCashflow0), + ProviderCashflow = get_selector_value(provider_payment_cash_flow, ProviderCashflowSelector), + Opts = maps:with([exchange_context], Context), AccountMap = hg_accounting:collect_account_map(make_collect_account_context(PaymentInstitution, Context)), - construct_final_cashflow(ProviderCashflow, #{operation_amount => get_amount(Context)}, AccountMap). - -maybe_convert_cashflow(undefined, ProviderCashflow) -> - ProviderCashflow; -maybe_convert_cashflow(ExchangeContext, ProviderCashflow) -> - convert_cashflow(ExchangeContext, ProviderCashflow). - --spec convert_cashflow(hg_invoice_payment:exchange_context(), cash_flow()) -> cash_flow(). -convert_cashflow(ExchangeContext, ProviderCashflow) -> - lists:foldr( - fun(#domain_CashFlowPosting{volume = CashVolume} = P, Acc) -> - [ - P#domain_CashFlowPosting{ - volume = convert_volume(ExchangeContext, CashVolume), - exchange_context = construct_exchange_context(ExchangeContext) - } - | Acc - ] - end, - [], - ProviderCashflow - ). - -construct_exchange_context(#{ - source := SourceCurrency, - destination := DestinationCurrency, - rate := ExchangeRate -}) -> - #domain_ExchangeContext{ - source_currency = SourceCurrency, - destination_currency = DestinationCurrency, - exchange_rate = ExchangeRate - }. - --spec convert_volume(hg_invoice_payment:exchange_context(), cash_volume()) -> cash_volume(). -convert_volume(_ExchangeContext, {share, _} = CashVolume) -> - CashVolume; -convert_volume(ExchangeContext, {product, {Kind, CashVolumeList}}) -> - {product, {Kind, convert_volumes(ExchangeContext, CashVolumeList)}}; -convert_volume( - #{source := PaymentCurrency, destination := TerminalCurrency} = ExchangeContext, - {fixed, #domain_CashVolumeFixed{ - cash = - #domain_Cash{ - currency = #domain_CurrencyRef{symbolic_code = FeeCurrency} - } = Cash - }} = CashVolume -) -> - case FeeCurrency of - PaymentCurrency -> - CashVolume; - TerminalCurrency -> - %% reverse conversion needed - ReConvertedCash = hg_currency_converter:reverse_convert_cash(ExchangeContext, Cash), - {fixed, #domain_CashVolumeFixed{ - cash = ReConvertedCash - }} - end. - -convert_volumes(ExchangeContext, CashVolumeList) -> - lists:foldr( - fun(CashVolume, Acc) -> - [convert_volume(ExchangeContext, CashVolume) | Acc] - end, - [], - CashVolumeList - ). + construct_final_cashflow(ProviderCashflow, #{operation_amount => get_amount(Context)}, AccountMap, Opts). construct_final_cashflow(Cashflow, Context, AccountMap) -> - hg_cashflow:finalize(Cashflow, Context, AccountMap). + construct_final_cashflow(Cashflow, Context, AccountMap, #{}). + +construct_final_cashflow(Cashflow, Context, AccountMap, Opts) -> + hg_cashflow:finalize(Cashflow, Context, AccountMap, Opts). get_cashflow_payment_institution( #domain_ShopConfig{payment_institution = PaymentInstitutionRef}, diff --git a/apps/hellgate/src/hg_currency_converter.erl b/apps/hellgate/src/hg_currency_converter.erl index 172f2914..d5406c12 100644 --- a/apps/hellgate/src/hg_currency_converter.erl +++ b/apps/hellgate/src/hg_currency_converter.erl @@ -9,31 +9,57 @@ -spec convert_cash(hg_invoice_payment:exchange_context(), cash()) -> cash(). convert_cash( - #{source := SourceCurrency, destination := DestinationCurrency, rate := Rate}, + #domain_ExchangeContext{ + source_currency = SourceCurrency, + destination_currency = DestinationCurrency, + exchange_rate = ExchangeRate + }, #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = SourceCurrency}} ) -> %% Example: %% Amount: 1000, Src: RUB, Dst: USD, Rate: {P=100, Q=1} (1 USD = 100 RUB) %% ConvertedAmountRational = {1000,100} %% ConvertedAmount = 10 - #base_Rational{p = P, q = Q} = Rate, + #base_Rational{p = P, q = Q} = ExchangeRate, RateRational = genlib_rational:new(P, Q), AmountRational = genlib_rational:new(Amount), ConvertedAmountRational = genlib_rational:dvd(AmountRational, RateRational), Rounding = application:get_env(hellgate, exchange_rounding_method, round_half_away_from_zero), ConvertedAmount = genlib_rational:round(ConvertedAmountRational, Rounding), - #domain_Cash{amount = ConvertedAmount, currency = #domain_CurrencyRef{symbolic_code = DestinationCurrency}}. + #domain_Cash{amount = ConvertedAmount, currency = #domain_CurrencyRef{symbolic_code = DestinationCurrency}}; +convert_cash( + #domain_ExchangeContext{ + destination_currency = DestinationCurrency + }, + #domain_Cash{currency = #domain_CurrencyRef{symbolic_code = DestinationCurrency}} = Cash +) -> + %% already needed currency + %% skip conversion + Cash. -spec reverse_convert_cash(hg_invoice_payment:exchange_context(), cash()) -> cash(). reverse_convert_cash( - #{source := SourceCurrency, destination := DestinationCurrency, rate := Rate}, + #domain_ExchangeContext{ + source_currency = SourceCurrency, + destination_currency = DestinationCurrency, + exchange_rate = ExchangeRate + }, #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = DestinationCurrency}} ) -> %% We do not use two-way exchange rates for currency pairs - #base_Rational{p = P, q = Q} = Rate, + #base_Rational{p = P, q = Q} = ExchangeRate, RateRational = genlib_rational:new(P, Q), AmountRational = genlib_rational:new(Amount), ReConvertedAmountRational = genlib_rational:mul(AmountRational, RateRational), Rounding = application:get_env(hellgate, exchange_rounding_method, round_half_away_from_zero), ReConvertedAmount = genlib_rational:round(ReConvertedAmountRational, Rounding), - #domain_Cash{amount = ReConvertedAmount, currency = #domain_CurrencyRef{symbolic_code = SourceCurrency}}. + #domain_Cash{amount = ReConvertedAmount, currency = #domain_CurrencyRef{symbolic_code = SourceCurrency}}; +reverse_convert_cash( + #domain_ExchangeContext{ + source_currency = SourceCurrency + }, + #domain_Cash{currency = #domain_CurrencyRef{symbolic_code = SourceCurrency}} = Cash +) -> + %% already needed currency + %% skip conversion + Cash. diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index 34336e46..683b3ef9 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -86,8 +86,6 @@ -export([accrue_status_timing/3]). -export([get_limit_values/2]). --export([get_exchange_context/2]). - %% Machine like -export([init/3]). @@ -159,7 +157,6 @@ | risk_scoring | routing | routing_failure - | exchange_context_building | cash_flow_building | processing_session | processing_accounter @@ -238,16 +235,10 @@ revision := hg_domain:revision(), merchant_terms => dmsl_domain_thrift:'PaymentsServiceTerms'(), allocation => hg_allocation:allocation() | undefined, - exchange_context => exchange_context() | undefined + exchange_context => exchange_context() }. --type currency_code() :: dmsl_domain_thrift:'CurrencySymbolicCode'(). - --type exchange_context() :: #{ - source := currency_code(), - destination := currency_code(), - rate := dmsl_base_thrift:'Rational'() -}. +-type exchange_context() :: dmsl_domain_thrift:'ExchangeContext'(). %% @@ -1429,7 +1420,7 @@ create_cash_flow_adjustment(Timestamp, Params, DomainRevision, St, Opts) -> Payment, Params#payproc_InvoicePaymentAdjustmentParams.scenario ), ExchangeContext = get_exchange_context(St), - Context = #{ + Context = genlib_map:compact(#{ provision_terms => get_provider_terminal_terms(Route, VS, NewRevision), route => Route, payment => Payment1, @@ -1438,7 +1429,7 @@ create_cash_flow_adjustment(Timestamp, Params, DomainRevision, St, Opts) -> revision => NewRevision, allocation => Allocation, exchange_context => ExchangeContext - }, + }), NewCashFlow = case Payment of #domain_InvoicePayment{status = {failed, _}} -> @@ -1583,7 +1574,7 @@ get_cash_flow_for_target_status({captured, Captured}, St0, Opts) -> Revision = Payment2#domain_InvoicePayment.domain_revision, VS = collect_validation_varset(St, Opts), ExchangeContext = get_exchange_context(St), - Context = #{ + Context = genlib_map:compact(#{ provision_terms => get_provider_terminal_terms(Route, VS, Revision), route => Route, payment => Payment2, @@ -1592,7 +1583,7 @@ get_cash_flow_for_target_status({captured, Captured}, St0, Opts) -> revision => Revision, allocation => Allocation, exchange_context => ExchangeContext - }, + }), calculate_cashflow(Context, Opts); get_cash_flow_for_target_status({cancelled, _}, _St, _Opts) -> []; @@ -1795,8 +1786,6 @@ process_timeout({payment, risk_scoring}, Action, St) -> process_risk_score(Action, St); process_timeout({payment, routing}, Action, St) -> process_routing(Action, St); -process_timeout({payment, exchange_context_building}, Action, St) -> - process_currency_exchange(Action, St); process_timeout({payment, cash_flow_building}, Action, St) -> process_cash_flow_building(Action, St); process_timeout({payment, Step}, _Action, St) when @@ -1831,12 +1820,12 @@ process_refund(ID, #st{opts = Options0, payment = Payment, repair_scenario = Sce RepairAction -> RepairAction end, PaymentInfo = construct_payment_info(St, get_opts(St)), - Options1 = Options0#{ + Options1 = genlib_map:compact(Options0#{ payment => Payment, payment_info => PaymentInfo, repair_scenario => RepairScenario, exchange_context => get_exchange_context(St) - }, + }), Refund = try_get_refund_state(ID, St), {Step, {Events0, Action}} = hg_invoice_payment_refund:process(Options1, Refund), Events1 = hg_invoice_payment_refund:wrap_events(Events0, Refund), @@ -1871,8 +1860,7 @@ repair_process_timeout(Activity, Action, #st{repair_scenario = Scenario} = St) - case hg_invoice_repair:check_for_action(fail_pre_processing, Scenario) of {result, Result} when Activity =:= {payment, routing}; - Activity =:= {payment, cash_flow_building}; - Activity =:= {payment, exchange_context_building} + Activity =:= {payment, cash_flow_building} -> rollback_broken_payment_limits(St), Result; @@ -2040,13 +2028,20 @@ produce_routing_events(#{error := Error} = Ctx, Revision, St) when Error =/= und [?route_changed(Route, Candidates, RouteScores, RouteLimits, Decision), ?payment_rollback_started(Failure)]; produce_routing_events(Ctx, Revision, _St) -> ok = log_route_choice_meta(Ctx, Revision), - Route = hg_route:to_payment_route(hg_routing_ctx:choosen_route(Ctx)), + ChoosenRoute = hg_routing_ctx:choosen_route(Ctx), + Route = hg_route:to_payment_route(ChoosenRoute), Candidates = ordsets:from_list([hg_route:to_payment_route(R) || R <- hg_routing_ctx:considered_candidates(Ctx)]), RouteScores = hg_routing_ctx:route_scores(Ctx), RouteLimits = hg_routing_ctx:route_limits(Ctx), Decision = build_route_decision_context(Route, Revision), - [?route_changed(Route, Candidates, RouteScores, RouteLimits, Decision)]. + [?route_changed(Route, Candidates, RouteScores, RouteLimits, Decision)] ++ + maybe_exchange_context_changed(ChoosenRoute). + +maybe_exchange_context_changed(#{exchange_context := ExchangeContext}) -> + [?invoice_payment_exchange_context_changed(ExchangeContext)]; +maybe_exchange_context_changed(_) -> + []. build_route_decision_context(Route, Revision) -> ProvisionTerms = hg_party:get_route_provision_terms(Route, #{}, Revision), @@ -2185,60 +2180,6 @@ mk_static_error([_ | _] = Codes) -> mk_static_error_(#payproc_error_GeneralFailu mk_static_error_(T, []) -> T; mk_static_error_(Sub, [Code | Codes]) -> mk_static_error_({Code, Sub}, Codes). --spec process_currency_exchange(action(), st()) -> machine_result(). -process_currency_exchange(Action, St) -> - Route = get_route(St), - Revision = get_payment_revision(St), - VS = get_varset(St, #{risk_score => get_risk_score(St)}), - case build_exchange_context(Route, VS, Revision, St) of - {ok, undefined} -> - %% no currency conversion required - %% go to cashflow building - process_cash_flow_building(Action, St#st{activity = {payment, cash_flow_building}}); - {ok, #{ - source := Src, - destination := Dst, - rate := Rate - }} -> - {next, {[?payment_currency_changed(Src, Dst, Rate)], hg_machine_action:set_timeout(0, Action)}}; - {error, Reason} -> - FailureReason = genlib:format("Payment currency conversion error: ~p", [Reason]), - Failure = payproc_errors:construct( - 'PaymentFailure', - {authorization_failed, {unknown, #payproc_error_GeneralFailure{}}}, - FailureReason - ), - {next, {[?payment_rollback_started({failure, Failure})], hg_machine_action:set_timeout(0, Action)}} - end. - -build_exchange_context(Route, VS, Revision, St) -> - #domain_Cash{ - currency = #domain_CurrencyRef{symbolic_code = PaymentCurrency} - } = get_payment_cost(get_payment(St)), - #domain_PaymentsProvisionTerms{ - currencies = {value, [#domain_CurrencyRef{symbolic_code = TerminalCurrency}]} - } = get_provider_terminal_terms(Route, VS, Revision), - case PaymentCurrency =:= TerminalCurrency of - true -> - {ok, undefined}; - false -> - get_exchange_context(PaymentCurrency, TerminalCurrency) - end. - --spec get_exchange_context(currency_code(), currency_code()) -> - {ok, exchange_context()} | {error, _Reason}. -get_exchange_context(SrcCurrency, DstCurrency) -> - case hg_exrates:get_exchange_rate(SrcCurrency, DstCurrency) of - {ok, #{p := P, q := Q}} -> - {ok, #{ - source => SrcCurrency, - destination => DstCurrency, - rate => #base_Rational{p = P, q = Q} - }}; - {error, _} = Error -> - Error - end. - -spec process_cash_flow_building(action(), st()) -> machine_result(). process_cash_flow_building(Action, St) -> Route = get_route(St), @@ -2251,7 +2192,7 @@ process_cash_flow_building(Action, St) -> ProviderTerms = get_provider_terminal_terms(Route, VS1, Revision), Allocation = get_allocation(St), ExchangeContext = get_exchange_context(St), - Context = #{ + Context = genlib_map:compact(#{ provision_terms => ProviderTerms, route => Route, payment => Payment, @@ -2260,7 +2201,7 @@ process_cash_flow_building(Action, St) -> revision => Revision, allocation => Allocation, exchange_context => ExchangeContext - }, + }), FinalCashflow = calculate_cashflow(Context, Opts), _ = rollback_unused_payment_limits(St), _Clock = hg_accounting:hold( @@ -2350,9 +2291,9 @@ process_accounter_update(Action, #st{partial_cash_flow = FinalCashflow, capture_ handle_callback({refund, ID}, Payload, _Session0, St) -> PaymentInfo = construct_payment_info(St, get_opts(St)), Refund = try_get_refund_state(ID, St), - Options = #{ + Options = genlib_map:compact(#{ exchange_context => get_exchange_context(St) - }, + }), {Resp, {Step, {Events0, Action}}} = hg_invoice_payment_refund:process_callback( Payload, PaymentInfo, Refund, Options ), @@ -2469,7 +2410,7 @@ process_result({payment, processing_accounter}, Action, #st{new_cash = Cost} = S Route = get_route(St1), ProviderTerms = hg_party:get_route_payment_terms(Route, VS, Revision), ExchangeContext = get_exchange_context(St1), - Context = #{ + Context = genlib_map:compact(#{ provision_terms => ProviderTerms, merchant_terms => MerchantTerms, route => Route, @@ -2478,7 +2419,7 @@ process_result({payment, processing_accounter}, Action, #st{new_cash = Cost} = S varset => VS, revision => Revision, exchange_context => ExchangeContext - }, + }), FinalCashflow = calculate_cashflow(Context, Opts), %% Hold limits (only for chosen route) for new cashflow {_PaymentInstitution, RouteVS, _Revision} = route_args(St1), @@ -2991,10 +2932,6 @@ commit_payment_cashflow(St) -> end ). -rollback_payment_cashflow(#st{cash_flow = undefined}) -> - %% This case is required to handle the situation where cascading operations is required, - %% but an error occurred before the cashflow were calculated - hg_accounting:empty_log(); rollback_payment_cashflow(St) -> Plan = get_cashflow_plan(St), do_try_with_ids( @@ -3116,7 +3053,7 @@ construct_proxy_payment( payment_service = hg_payment_tool:get_payment_service(PaymentTool, Revision), payer_session_info = PayerSessionInfo, cost = construct_proxy_cash(ConvertedCost), - original_cost = construct_proxy_cash(OriginalCost), + original_cost = maybe_construct_proxy_cash(OriginalCost), contact_info = ContactInfo, make_recurrent = MakeRecurrent, skip_recurrent = SkipRecurrent, @@ -3188,8 +3125,11 @@ construct_proxy_shop( location = Location }. -construct_proxy_cash(undefined) -> +maybe_construct_proxy_cash(undefined) -> undefined; +maybe_construct_proxy_cash(Cash) -> + construct_proxy_cash(Cash). + construct_proxy_cash(#domain_Cash{ amount = Amount, currency = CurrencyRef @@ -3207,7 +3147,7 @@ construct_proxy_capture(Cost) -> maybe_convert_cash(undefined, Cost) -> {Cost, undefined}; maybe_convert_cash( - #{source := PaymentCurrency} = ExchangeContext, + #domain_ExchangeContext{source_currency = PaymentCurrency} = ExchangeContext, #domain_Cash{currency = #domain_CurrencyRef{symbolic_code = PaymentCurrency}} = OriginalCash ) -> ConvertedCash = hg_currency_converter:convert_cash(ExchangeContext, OriginalCash), @@ -3216,7 +3156,7 @@ maybe_convert_cash( maybe_reverse_convert_cash(undefined, Cost) -> Cost; maybe_reverse_convert_cash( - #{destination := TerminalCurrency} = ExchangeContext, + #domain_ExchangeContext{destination_currency = TerminalCurrency} = ExchangeContext, #domain_Cash{currency = #domain_CurrencyRef{symbolic_code = TerminalCurrency}} = TerminalCash ) -> hg_currency_converter:reverse_convert_cash(ExchangeContext, TerminalCash). @@ -3402,24 +3342,19 @@ merge_change( exchange_context = undefined, routes = [Route | Routes], candidate_routes = ordsets:to_list(Candidates), - activity = {payment, exchange_context_building}, + activity = {payment, cash_flow_building}, route_scores = hg_maybe:apply(fun(S) -> maps:merge(RouteScores, S) end, Scores, RouteScores), route_limits = hg_maybe:apply(fun(L) -> maps:merge(RouteLimits, L) end, Limits, RouteLimits), payment = Payment1 }; merge_change( - Change = ?payment_currency_changed(SourceCurrency, DestinationCurrency, ExchangeRate), + Change = ?invoice_payment_exchange_context_changed(ExchangeContext), #st{} = St, Opts ) -> - _ = validate_transition([{payment, exchange_context_building}], Change, St, Opts), + _ = validate_transition([{payment, cash_flow_building}], Change, St, Opts), St#st{ - activity = {payment, cash_flow_building}, - exchange_context = #{ - source => SourceCurrency, - destination => DestinationCurrency, - rate => ExchangeRate - } + exchange_context = ExchangeContext }; merge_change(Change = ?payment_capture_started(Data), #st{} = St, Opts) -> _ = validate_transition([{payment, S} || S <- [flow_waiting]], Change, St, Opts), @@ -3433,7 +3368,6 @@ merge_change(Change = ?cash_flow_changed(CashFlow), #st{activity = Activity} = S [ {payment, S} || S <- [ - exchange_context_building, cash_flow_building, processing_capture, processing_accounter @@ -3449,10 +3383,7 @@ merge_change(Change = ?cash_flow_changed(CashFlow), #st{activity = Activity} = S case Activity of {payment, processing_accounter} -> St#st{new_cash = undefined, new_cash_flow = CashFlow}; - {payment, PaymentActivity} when - PaymentActivity =:= exchange_context_building; - PaymentActivity =:= cash_flow_building - -> + {payment, cash_flow_building} -> St#st{ cash_flow = CashFlow, activity = {payment, processing_session} @@ -3486,7 +3417,6 @@ merge_change(Change = ?payment_rollback_started(Failure), St, Opts) -> _ = validate_transition( [ {payment, shop_limit_finalizing}, - {payment, exchange_context_building}, {payment, cash_flow_building}, {payment, processing_session} ], @@ -3498,8 +3428,6 @@ merge_change(Change = ?payment_rollback_started(Failure), St, Opts) -> case St of #st{shop_limit_status = initialized} -> {payment, shop_limit_failure}; - #st{activity = {payment, exchange_context_building}} -> - {payment, processing_failure}; #st{cash_flow = undefined} -> {payment, routing_failure}; _ -> @@ -3517,7 +3445,6 @@ merge_change(Change = ?payment_status_changed({failed, _} = Status), #st{payment || S <- [ risk_scoring, routing, - exchange_context_building, cash_flow_building, shop_limit_failure, routing_failure, diff --git a/apps/hellgate/src/hg_invoice_payment_chargeback.erl b/apps/hellgate/src/hg_invoice_payment_chargeback.erl index 9d0931fe..0dc1418e 100644 --- a/apps/hellgate/src/hg_invoice_payment_chargeback.erl +++ b/apps/hellgate/src/hg_invoice_payment_chargeback.erl @@ -431,11 +431,9 @@ build_chargeback_final_cash_flow(State, Opts) -> PaymentsTerms = hg_party:get_route_payment_terms(Route, VS, Revision), ProviderTerms = get_provider_chargeback_terms(PaymentsTerms, Payment), ServiceCashFlow = get_chargeback_service_cash_flow(ServiceTerms), - ProviderCashFlow = maybe_convert_cashflow( - ExchangeContext, - get_chargeback_provider_cash_flow(ProviderTerms) - ), - ProviderFees = maybe_convert_fees(ExchangeContext, collect_chargeback_provider_fees(ProviderTerms)), + ProviderCashFlow = get_chargeback_provider_cash_flow(ProviderTerms), + ProviderFees = collect_chargeback_provider_fees(ProviderTerms), + ProviderOpts = genlib_map:compact(#{exchange_context => ExchangeContext}), PaymentInstitutionRef = Shop#domain_ShopConfig.payment_institution, PaymentInst = hg_payment_institution:compute_payment_institution(PaymentInstitutionRef, VS, Revision), Provider = get_route_provider(Route, Revision), @@ -453,7 +451,7 @@ build_chargeback_final_cash_flow(State, Opts) -> ServiceContext = build_service_cash_flow_context(State), ProviderContext = build_provider_cash_flow_context(State, ProviderFees), ServiceFinalCF = hg_cashflow:finalize(ServiceCashFlow, ServiceContext, AccountMap), - ProviderFinalCF = hg_cashflow:finalize(ProviderCashFlow, ProviderContext, AccountMap), + ProviderFinalCF = hg_cashflow:finalize(ProviderCashFlow, ProviderContext, AccountMap, ProviderOpts), ServiceFinalCF ++ ProviderFinalCF. build_service_cash_flow_context(State) -> @@ -470,21 +468,6 @@ build_provider_cash_flow_context(State, Fees) -> maps:merge(ComputedFees, #{operation_amount => get_body(State)}) end. -maybe_convert_cashflow(undefined, CashFlow) -> - CashFlow; -maybe_convert_cashflow(ExchangeContext, CashFlow) -> - hg_cashflow_utils:convert_cashflow(ExchangeContext, CashFlow). - -maybe_convert_fees(undefined, Fees) -> - Fees; -maybe_convert_fees(ExchangeContext, Fees) -> - maps:map( - fun(_Const, Volume) -> - hg_cashflow_utils:convert_volume(ExchangeContext, Volume) - end, - Fees - ). - get_chargeback_service_cash_flow( #domain_PaymentChargebackServiceTerms{fees = {value, V}} ) -> diff --git a/apps/hellgate/src/hg_invoice_payment_refund.erl b/apps/hellgate/src/hg_invoice_payment_refund.erl index cf6e3af0..60208dda 100644 --- a/apps/hellgate/src/hg_invoice_payment_refund.erl +++ b/apps/hellgate/src/hg_invoice_payment_refund.erl @@ -126,7 +126,7 @@ payment_id := payment_id(), repair_scenario => repair_scenario(), payment_info => payment_info(), - exchange_context => hg_invoice_payment:exchange_context() | undefined + exchange_context => hg_invoice_payment:exchange_context() }. -type options() :: #{ @@ -139,7 +139,7 @@ repair_scenario => repair_scenario(), payment_info => payment_info(), - exchange_context => hg_invoice_payment:exchange_context() | undefined + exchange_context => hg_invoice_payment:exchange_context() }. -type repair_scenario() :: {result, proxy_result()}. @@ -232,7 +232,7 @@ process_callback(Payload, PaymentInfo0, Refund) -> -spec process_callback(callback(), payment_info(), t(), options()) -> {callback_response(), machine_result()}. process_callback(Payload, PaymentInfo0, Refund0, Options) -> - Refund = inject_context(Options, Refund0), + Refund = Refund0#{injected_context => maps:with([exchange_context], Options)}, PaymentInfo1 = construct_payment_info(PaymentInfo0, Refund), Session0 = hg_session:set_payment_info(PaymentInfo1, session(Refund)), {Response, {Result, Session1}} = hg_session:process_callback(Payload, Session0), @@ -499,13 +499,13 @@ get_initial_retry_strategy() -> hg_retry:new_strategy(maps:get(refunded, PolicyConfig, no_retry)). inject_context(Options, Refund) -> - Invoice = maps:get(invoice, Options, undefined), - Payment = maps:get(payment, Options, undefined), - {InvoiceID, ShopConfigRef} = get_opts_invoice_data(Invoice), - {PaymentID, Revision} = get_opts_payment_data(Payment), - Party = maps:get(party, Options, undefined), - PartyConfigRef = maps:get(party_config_ref, Options, undefined), - {ShopConfigRef, Shop} = try_get_shop(ShopConfigRef, PartyConfigRef, Revision), + Invoice = maps:get(invoice, Options), + Payment = maps:get(payment, Options), + #domain_Invoice{id = InvoiceID, shop_ref = ShopConfigRef} = Invoice, + #domain_InvoicePayment{id = PaymentID, domain_revision = Revision} = Payment, + Party = maps:get(party, Options), + PartyConfigRef = maps:get(party_config_ref, Options), + {ShopConfigRef, Shop} = hg_party:get_shop(ShopConfigRef, PartyConfigRef, Revision), Context = genlib_map:compact(#{ party => Party, shop => Shop, @@ -521,24 +521,6 @@ inject_context(Options, Refund) -> }), Refund#{injected_context => Context}. -get_opts_invoice_data(undefined) -> - {undefined, undefined}; -get_opts_invoice_data(#domain_Invoice{id = InvoiceID, shop_ref = ShopConfigRef}) -> - {InvoiceID, ShopConfigRef}. - -get_opts_payment_data(undefined) -> - {undefined, undefined}; -get_opts_payment_data(#domain_InvoicePayment{id = PaymentID, domain_revision = Revision}) -> - {PaymentID, Revision}. - -try_get_shop(ShopConfigRef, PartyConfigRef, _Revision) when - ShopConfigRef =:= undefined; - PartyConfigRef =:= undefined --> - {undefined, undefined}; -try_get_shop(ShopConfigRef, PartyConfigRef, Revision) -> - hg_party:get_shop(ShopConfigRef, PartyConfigRef, Revision). - get_injected_party_config_ref(#{injected_context := #{party_config_ref := V}}) -> V. get_injected_invoice(#{injected_context := #{invoice := V}}) -> V. get_injected_payment(#{injected_context := #{payment := V}}) -> V. @@ -611,7 +593,7 @@ get_refund_created_at(#domain_InvoicePaymentRefund{created_at = CreatedAt}) -> construct_payment_info(PaymentInfo, Refund) -> ExchangeContext = get_injected_exchange_context(Refund), - {ConvertedCash, _OriginalCash} = maybe_convert_cash(ExchangeContext, cash(Refund)), + ConvertedCash = maybe_convert_cash(ExchangeContext, cash(Refund)), PaymentInfo#proxy_provider_PaymentInfo{ refund = #proxy_provider_InvoicePaymentRefund{ id = id(Refund), @@ -622,12 +604,9 @@ construct_payment_info(PaymentInfo, Refund) -> }. maybe_convert_cash(undefined, Cash) -> - {Cash, undefined}; + Cash; maybe_convert_cash(ExchangeContext, Cash) -> - { - hg_currency_converter:convert_cash(ExchangeContext, Cash), - Cash - }. + hg_currency_converter:convert_cash(ExchangeContext, Cash). construct_proxy_cash(#domain_Cash{ amount = Amount, diff --git a/apps/hellgate/src/hg_invoice_registered_payment.erl b/apps/hellgate/src/hg_invoice_registered_payment.erl index bb632fad..5257a3b7 100644 --- a/apps/hellgate/src/hg_invoice_registered_payment.erl +++ b/apps/hellgate/src/hg_invoice_registered_payment.erl @@ -73,7 +73,6 @@ init_(PaymentID, Params, #{timestamp := CreatedAt0} = Opts) -> MerchantTerms = get_merchant_payment_terms(Revision, Shop, VS), ProviderTerms = hg_invoice_payment:get_provider_terminal_terms(Route, VS, Revision), - {MaybeExchangeContext, MaybeCurrencyChangedEvents} = maybe_get_exchange_context(Cost1, ProviderTerms), CashflowContext = #{ provision_terms => ProviderTerms, @@ -82,8 +81,7 @@ init_(PaymentID, Params, #{timestamp := CreatedAt0} = Opts) -> payment => Payment, timestamp => CreatedAt1, varset => VS, - revision => Revision, - exchange_context => MaybeExchangeContext + revision => Revision }, FinalCashflow = hg_invoice_payment:calculate_cashflow(PaymentInstitution, CashflowContext, Opts), @@ -94,9 +92,8 @@ init_(PaymentID, Params, #{timestamp := CreatedAt0} = Opts) -> ?shop_limit_applied() ] ++ RiskScoreEventList ++ - [?route_changed(Route)] ++ - MaybeCurrencyChangedEvents ++ [ + ?route_changed(Route), ?cash_flow_changed(FinalCashflow), hg_session:wrap_event(?processed(), hg_session:create()) ] ++ @@ -271,31 +268,6 @@ collect_validation_varset( flow => instant }. -maybe_get_exchange_context( - #domain_Cash{ - currency = #domain_CurrencyRef{symbolic_code = PaymentCurrency} - }, - #domain_PaymentsProvisionTerms{ - currencies = {value, [#domain_CurrencyRef{symbolic_code = TerminalCurrency}]} - } -) when PaymentCurrency =:= TerminalCurrency -> - {undefined, []}; -maybe_get_exchange_context( - #domain_Cash{ - currency = #domain_CurrencyRef{symbolic_code = PaymentCurrency} - }, - #domain_PaymentsProvisionTerms{ - currencies = {value, [#domain_CurrencyRef{symbolic_code = TerminalCurrency}]} - } -) -> - {ok, - #{ - source := Src, - destination := Dst, - rate := Rate - } = ExchangeContext} = hg_invoice_payment:get_exchange_context(PaymentCurrency, TerminalCurrency), - {ExchangeContext, [?payment_currency_changed(Src, Dst, Rate)]}. - %% get_party_config_ref(#{party_config_ref := PartyConfigRef}) -> diff --git a/apps/hellgate/src/hg_invoice_repair.erl b/apps/hellgate/src/hg_invoice_repair.erl index 64dd5c0a..3a644f08 100644 --- a/apps/hellgate/src/hg_invoice_repair.erl +++ b/apps/hellgate/src/hg_invoice_repair.erl @@ -90,7 +90,6 @@ check_activity_compatibility(?SCENARIO_FAIL_PRE_PROCESSING, Activity) when Activity =:= {payment, new} orelse Activity =:= {payment, risk_scoring} orelse Activity =:= {payment, routing} orelse - Activity =:= {payment, exchange_context_building} orelse Activity =:= {payment, cash_flow_building} -> ok; diff --git a/apps/hellgate/test/hg_invoice_dummy_data.erl b/apps/hellgate/test/hg_invoice_dummy_data.erl index 9e36fb6b..60bc1120 100644 --- a/apps/hellgate/test/hg_invoice_dummy_data.erl +++ b/apps/hellgate/test/hg_invoice_dummy_data.erl @@ -96,61 +96,7 @@ construct_domain_fixture() -> ]), terms = #domain_ProvisionTermSet{ payments = #domain_PaymentsProvisionTerms{ - cash_limit = { - decisions, - [ - #domain_CashLimitDecision{ - if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, - then_ = { - value, - ?cashrng( - {inclusive, ?cash(10, <<"RUB">>)}, - {exclusive, ?cash(420000000, <<"RUB">>)} - ) - } - }, - #domain_CashLimitDecision{ - if_ = {condition, {currency_is, ?cur(<<"USD">>)}}, - then_ = { - value, - ?cashrng( - {inclusive, ?cash(10, <<"USD">>)}, - {exclusive, ?cash(420000000, <<"USD">>)} - ) - } - }, - #domain_CashLimitDecision{ - if_ = {condition, {currency_is, ?cur(<<"EUR">>)}}, - then_ = { - value, - ?cashrng( - {inclusive, ?cash(10, <<"EUR">>)}, - {exclusive, ?cash(420000000, <<"EUR">>)} - ) - } - }, - #domain_CashLimitDecision{ - if_ = {condition, {currency_is, ?cur(<<"JPY">>)}}, - then_ = { - value, - ?cashrng( - {inclusive, ?cash(10, <<"JPY">>)}, - {exclusive, ?cash(420000000, <<"JPY">>)} - ) - } - }, - #domain_CashLimitDecision{ - if_ = {condition, {currency_is, ?cur(<<"CNY">>)}}, - then_ = { - value, - ?cashrng( - {inclusive, ?cash(10, <<"CNY">>)}, - {exclusive, ?cash(420000000, <<"CNY">>)} - ) - } - } - ] - } + allow_exchange = {constant, true} } } } @@ -300,6 +246,13 @@ payment_provision_terms(Currency, Category) -> ) ] }, + cash_limit = { + value, + ?cashrng( + {inclusive, ?cash(10, Currency)}, + {exclusive, ?cash(420000000, Currency)} + ) + }, refunds = #domain_PaymentRefundsProvisionTerms{ cash_flow = { value, @@ -333,5 +286,6 @@ payment_provision_terms(Currency, Category) -> ) ] } - } + }, + allow_exchange = {constant, true} }. diff --git a/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl index e6cd2265..c94655af 100644 --- a/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_exchange_tests_SUITE.erl @@ -454,12 +454,8 @@ payment_failed_exchange_rate_unknown(C) -> C ), PaymentParams = hg_invoice_helper:make_payment_params(?pmt_sys(<<"visa-ref">>)), - ?payment_state(?payment(PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), - _ = hg_invoice_helper:start_payment_ev(InvoiceID, Client), - [ - ?payment_ev(PaymentID, ?payment_rollback_started(_)), - ?payment_ev(PaymentID, ?payment_status_changed(?failed(_))) - ] = hg_invoice_helper:next_changes(InvoiceID, 2, Client), + ?payment_state(?payment(_PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), + _ = hg_invoice_helper:fail_payment_no_route(InvoiceID, Client), ok. -spec payment_failed_exchange_rate_timeout(config()) -> test_return(). @@ -474,12 +470,8 @@ payment_failed_exchange_rate_timeout(C) -> C ), PaymentParams = hg_invoice_helper:make_payment_params(?pmt_sys(<<"visa-ref">>)), - ?payment_state(?payment(PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), - _ = hg_invoice_helper:start_payment_ev(InvoiceID, Client), - [ - ?payment_ev(PaymentID, ?payment_rollback_started(_)), - ?payment_ev(PaymentID, ?payment_status_changed(?failed(_))) - ] = hg_invoice_helper:next_changes(InvoiceID, 2, Client), + ?payment_state(?payment(_PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), + _ = hg_invoice_helper:fail_payment_no_route(InvoiceID, Client), ok. -spec payment_failed_exchange_rate_unexpected(config()) -> test_return(). @@ -494,12 +486,8 @@ payment_failed_exchange_rate_unexpected(C) -> C ), PaymentParams = hg_invoice_helper:make_payment_params(?pmt_sys(<<"visa-ref">>)), - ?payment_state(?payment(PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), - _ = hg_invoice_helper:start_payment_ev(InvoiceID, Client), - [ - ?payment_ev(PaymentID, ?payment_rollback_started(_)), - ?payment_ev(PaymentID, ?payment_status_changed(?failed(_))) - ] = hg_invoice_helper:next_changes(InvoiceID, 2, Client), + ?payment_state(?payment(_PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), + _ = hg_invoice_helper:fail_payment_no_route(InvoiceID, Client), ok. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff --git a/apps/hellgate/test/hg_invoice_helper.erl b/apps/hellgate/test/hg_invoice_helper.erl index c9b666f1..2c4f291e 100644 --- a/apps/hellgate/test/hg_invoice_helper.erl +++ b/apps/hellgate/test/hg_invoice_helper.erl @@ -27,6 +27,7 @@ register_payment/4, start_payment/3, start_payment_ev/2, + fail_payment_no_route/2, register_payment_ev_no_risk_scoring/2, await_payment_session_started/4, await_payment_capture/3, @@ -260,7 +261,7 @@ start_payment(InvoiceID, PaymentParams, Client) -> case next_change(InvoiceID, Client) of ?payment_ev(PaymentID, ?cash_flow_changed(_)) -> PaymentID; - ?payment_ev(PaymentID, ?payment_currency_changed(_, _, _)) -> + ?payment_ev(PaymentID, ?invoice_payment_exchange_context_changed(_)) -> ?payment_ev(PaymentID, ?cash_flow_changed(_)) = next_change(InvoiceID, Client), PaymentID end. @@ -276,6 +277,17 @@ start_payment_ev(InvoiceID, Client) -> ] = next_changes(InvoiceID, 5, Client), Route. +-spec fail_payment_no_route(_, _) -> _. +fail_payment_no_route(InvoiceID, Client) -> + [ + ?payment_ev(PaymentID, ?payment_started(?payment_w_status(?pending()))), + ?payment_ev(PaymentID, ?shop_limit_initiated()), + ?payment_ev(PaymentID, ?shop_limit_applied()), + ?payment_ev(PaymentID, ?risk_score_changed(_RiskScore)), + ?payment_ev(PaymentID, ?payment_status_changed(?failed(_))) + ] = next_changes(InvoiceID, 5, Client), + PaymentID. + -spec register_payment_ev_no_risk_scoring(_, _) -> _. register_payment_ev_no_risk_scoring(InvoiceID, Client) -> [ diff --git a/apps/routing/src/hg_route.erl b/apps/routing/src/hg_route.erl index c7b229f7..9955b185 100644 --- a/apps/routing/src/hg_route.erl +++ b/apps/routing/src/hg_route.erl @@ -52,7 +52,8 @@ route_data := route_data(), pin_data => pin_data(), fd_overrides => fd_overrides(), - rejection_reason => route_rejection_reason() | undefined + rejection_reason => route_rejection_reason() | undefined, + exchange_context => hg_invoice_payment:exchange_context() }. -type fd_score() :: #{ diff --git a/apps/routing/src/hg_route_collector.erl b/apps/routing/src/hg_route_collector.erl index 2a8c0da0..f6f89dcb 100644 --- a/apps/routing/src/hg_route_collector.erl +++ b/apps/routing/src/hg_route_collector.erl @@ -39,6 +39,10 @@ error => get_routes_error() }. +-type route_updates() :: #{ + exchange_context => hg_invoice_payment:exchange_context() +}. + -type get_routes_error() :: {misconfiguration, _Reason}. -type blacklist_context() :: hg_inspector:blacklist_context(). @@ -150,8 +154,8 @@ fill_accepted(Predestination, Revision, VS, Routes) -> PRef = hg_route:provider_ref(Route), TRef = hg_route:terminal_ref(Route), try - true = acceptable_terminal(Predestination, PRef, TRef, VS, Revision), - [Route | AccIn] + {ok, Updates} = acceptable_terminal(Predestination, PRef, TRef, VS, Revision), + [maps:merge(Route, Updates) | AccIn] catch {rejected, Reason} -> [hg_route:set_accepted({false, {rejected, Reason}}, Route) | AccIn]; @@ -249,7 +253,7 @@ gather_pin_info(#domain_RoutingPin{features = Features}, Ctx) -> hg_route:terminal_ref(), varset(), revision() -) -> true | no_return(). +) -> {ok, route_updates()} | no_return(). acceptable_terminal(Predestination, ProviderRef, TerminalRef, VS, Revision) -> {Client, Context} = get_party_client(), Result = party_client_thrift:compute_provider_terminal_terms( @@ -262,7 +266,12 @@ acceptable_terminal(Predestination, ProviderRef, TerminalRef, VS, Revision) -> ), case Result of {ok, ProvisionTermSet} -> - check_terms_acceptability(Predestination, ProvisionTermSet, VS); + {UpdVS, Updates} = maybe_currency_conversion( + ProvisionTermSet#domain_ProvisionTermSet.payments, + VS + ), + true = check_terms_acceptability(Predestination, ProvisionTermSet, UpdVS), + {ok, Updates}; {error, #payproc_ProvisionTermSetUndefined{}} -> throw(?rejected({'ProvisionTermSet', undefined})) end. @@ -273,6 +282,38 @@ get_party_client() -> Context = hg_context:get_party_client_context(HgContext), {Client, Context}. +maybe_currency_conversion( + #domain_PaymentsProvisionTerms{ + currencies = {value, [#domain_CurrencyRef{symbolic_code = DestinationCurrency} = DestiationCurrencyRef]}, + allow_exchange = {constant, true} + }, + #{currency := #domain_CurrencyRef{symbolic_code = SourceCurrency}} = VS +) when SourceCurrency =/= DestinationCurrency -> + case hg_exrates:get_exchange_rate(SourceCurrency, DestinationCurrency) of + {ok, #{p := P, q := Q}} -> + ExchangeContext = #domain_ExchangeContext{ + source_currency = SourceCurrency, + destination_currency = DestinationCurrency, + exchange_rate = #base_Rational{p = P, q = Q} + }, + UpdVS = VS#{ + currency => DestiationCurrencyRef, + cost => hg_currency_converter:convert_cash(ExchangeContext, getv(cost, VS)) + }, + {UpdVS, #{exchange_context => ExchangeContext}}; + {error, _} -> + throw(?rejected({'PaymentsProvisionTerms', exchange_error})) + end; +%% Check production domain config before use +%maybe_currency_conversion( +% #domain_PaymentsProvisionTerms{currencies = {value, Currencies}}, +% _VS +%) when erlang:length(Currencies) > 1 -> +% throw(?rejected({'PaymentsProvisionTerms', too_many_currencies})); +maybe_currency_conversion(_Terms, VS) -> + %% in other cases we rely on check_terms_acceptability/3 + {VS, #{}}. + check_terms_acceptability(payment, Terms, VS) -> acceptable_payment_terms(Terms#domain_ProvisionTermSet.payments, VS); check_terms_acceptability(recurrent_paytool, Terms, VS) -> @@ -398,14 +439,7 @@ try_accept_term(ParentName, Name, Value, Selector) -> test_term(Name, Value, Values) orelse throw(?rejected({ParentName, Name})). test_term(currency, V, Vs) -> - case application:get_env(hellgate, currency_exchange_enabled, false) of - true -> - %% currency conversion is allowed - %% ignore the currency difference - true; - false -> - ordsets:is_element(V, Vs) - end; + ordsets:is_element(V, Vs); test_term(category, V, Vs) -> ordsets:is_element(V, Vs); test_term(payment_tool, PT, PMs) -> diff --git a/compose.yaml b/compose.yaml index a8d495ea..32dfd090 100644 --- a/compose.yaml +++ b/compose.yaml @@ -30,7 +30,7 @@ services: command: /sbin/init dmt: - image: ghcr.io/valitydev/dominant-v2:sha-c9430b5 + image: ghcr.io/valitydev/dominant-v2:sha-51fb871-epic-XYZ-19-currency-exchange-support command: /opt/dmt/bin/dmt foreground environment: DMT_KAFKA_ENABLED: "0" @@ -112,7 +112,7 @@ services: retries: 20 party-management: - image: ghcr.io/valitydev/party-management:sha-8a58cd5 + image: ghcr.io/valitydev/party-management:sha-d4b8374-epic-XYZ-19-exchange-currency-support command: /opt/party-management/bin/party-management foreground volumes: - ./test/party-management/sys.config:/opt/party-management/releases/0.1/sys.config diff --git a/rebar.lock b/rebar.lock index 85e4b239..a306a52d 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,"9b725828e809190b01d5e53f4b49de6b9262057f"}}, + {ref,"389e46e908c21486d06bf5eea50bcd4b60b8cd45"}}, 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt-client.git", From 6cae140b34e6509e93fb05819e39679f9ec2ecd0 Mon Sep 17 00:00:00 2001 From: ttt161 Date: Thu, 25 Jun 2026 18:54:14 +0300 Subject: [PATCH 6/8] fix typos --- apps/hellgate/src/hg_invoice_payment.erl | 4 ++-- apps/hellgate/src/hg_invoice_registered_payment.erl | 1 - apps/routing/src/hg_route_collector.erl | 4 ++-- rebar.config | 2 +- rebar.lock | 2 +- 5 files changed, 6 insertions(+), 7 deletions(-) diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index 683b3ef9..21cdecbc 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -1007,7 +1007,7 @@ partial_capture(St0, Reason, Cost, Cart, Opts, MerchantTerms, Timestamp, Allocat ProviderTerms = hg_party:get_route_payment_terms(Route, VS, Revision), ok = validate_provider_holds_terms(ProviderTerms), ExchangeContext = get_exchange_context(St), - Context = #{ + Context = genlib_map:compact(#{ provision_terms => ProviderTerms, merchant_terms => MerchantTerms, route => Route, @@ -1017,7 +1017,7 @@ partial_capture(St0, Reason, Cost, Cart, Opts, MerchantTerms, Timestamp, Allocat revision => Revision, allocation => Allocation, exchange_context => ExchangeContext - }, + }), FinalCashflow = calculate_cashflow(Context, Opts), Changes = start_partial_capture(Reason, Cost, Cart, FinalCashflow, Allocation), {ok, {Changes, hg_machine_action:instant()}}. diff --git a/apps/hellgate/src/hg_invoice_registered_payment.erl b/apps/hellgate/src/hg_invoice_registered_payment.erl index 5257a3b7..8414ccaf 100644 --- a/apps/hellgate/src/hg_invoice_registered_payment.erl +++ b/apps/hellgate/src/hg_invoice_registered_payment.erl @@ -73,7 +73,6 @@ init_(PaymentID, Params, #{timestamp := CreatedAt0} = Opts) -> MerchantTerms = get_merchant_payment_terms(Revision, Shop, VS), ProviderTerms = hg_invoice_payment:get_provider_terminal_terms(Route, VS, Revision), - CashflowContext = #{ provision_terms => ProviderTerms, merchant_terms => MerchantTerms, diff --git a/apps/routing/src/hg_route_collector.erl b/apps/routing/src/hg_route_collector.erl index f6f89dcb..defa724b 100644 --- a/apps/routing/src/hg_route_collector.erl +++ b/apps/routing/src/hg_route_collector.erl @@ -284,7 +284,7 @@ get_party_client() -> maybe_currency_conversion( #domain_PaymentsProvisionTerms{ - currencies = {value, [#domain_CurrencyRef{symbolic_code = DestinationCurrency} = DestiationCurrencyRef]}, + currencies = {value, [#domain_CurrencyRef{symbolic_code = DestinationCurrency} = DestinationCurrencyRef]}, allow_exchange = {constant, true} }, #{currency := #domain_CurrencyRef{symbolic_code = SourceCurrency}} = VS @@ -297,7 +297,7 @@ maybe_currency_conversion( exchange_rate = #base_Rational{p = P, q = Q} }, UpdVS = VS#{ - currency => DestiationCurrencyRef, + currency => DestinationCurrencyRef, cost => hg_currency_converter:convert_cash(ExchangeContext, getv(cost, VS)) }, {UpdVS, #{exchange_context => ExchangeContext}}; diff --git a/rebar.config b/rebar.config index ec6e06d1..878c360a 100644 --- a/rebar.config +++ b/rebar.config @@ -38,7 +38,7 @@ {thrift, {git, "https://github.com/valitydev/thrift_erlang.git", {tag, "v1.0.0"}}}, %{damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.33"}}}, {damsel, {git, "https://github.com/valitydev/damsel.git", {branch, "XYZ-19/ft/multi-currency"}}}, - {exrates_proto, {git, "https://github.com/valitydev/exrates-proto.git", {branch, "fx/add-rebar-config"}}}, + {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"}}}, {dmt_client, {git, "https://github.com/valitydev/dmt-client.git", {tag, "v2.0.3"}}}, diff --git a/rebar.lock b/rebar.lock index a306a52d..7269ebf3 100644 --- a/rebar.lock +++ b/rebar.lock @@ -61,7 +61,7 @@ {<<"erlydtl">>,{pkg,<<"erlydtl">>,<<"0.14.0">>},2}, {<<"exrates_proto">>, {git,"https://github.com/valitydev/exrates-proto.git", - {ref,"e23772c160f10d141851e98a3a0995e297df9cb3"}}, + {ref,"924f813ca2bddfdcaa3b6570733212c810035304"}}, 0}, {<<"fault_detector_proto">>, {git,"https://github.com/valitydev/fault-detector-proto.git", From 3ab813c9b7bcd0a26ae176b7799ddb0462078c54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Fri, 26 Jun 2026 14:01:33 +0300 Subject: [PATCH 7/8] refactored --- apps/hellgate/src/hg_cashflow.erl | 7 +- apps/hellgate/src/hg_currency_converter.erl | 102 ++++++++++-------- apps/hellgate/src/hg_invoice_payment.erl | 36 +++---- .../src/hg_invoice_payment_refund.erl | 7 +- apps/routing/src/hg_route.erl | 29 +++-- apps/routing/src/hg_route_collector.erl | 45 ++++---- 6 files changed, 112 insertions(+), 114 deletions(-) diff --git a/apps/hellgate/src/hg_cashflow.erl b/apps/hellgate/src/hg_cashflow.erl index 258b5f37..3cbfd39b 100644 --- a/apps/hellgate/src/hg_cashflow.erl +++ b/apps/hellgate/src/hg_cashflow.erl @@ -85,18 +85,13 @@ compute_postings(CF, Context, AccountMap, Opts) -> ?final_posting( construct_final_account(Source, AccountMap), construct_final_account(Destination, AccountMap), - maybe_convert_cash(ExchangeContext, compute_volume(Volume, Context)), + hg_currency_converter:maybe_reverse_convert_cash(ExchangeContext, compute_volume(Volume, Context)), Details, ExchangeContext ) || ?posting(Source, Destination, Volume, Details) <- CF ]. -maybe_convert_cash(undefined, Cash) -> - Cash; -maybe_convert_cash(ExchangeContext, Cash) -> - hg_currency_converter:reverse_convert_cash(ExchangeContext, Cash). - -spec construct_final_account(account(), account_map()) -> final_cash_flow_account() | no_return(). construct_final_account(AccountType, AccountMap) -> #domain_FinalCashFlowAccount{ diff --git a/apps/hellgate/src/hg_currency_converter.erl b/apps/hellgate/src/hg_currency_converter.erl index d5406c12..577cdf14 100644 --- a/apps/hellgate/src/hg_currency_converter.erl +++ b/apps/hellgate/src/hg_currency_converter.erl @@ -4,62 +4,72 @@ -export([convert_cash/2]). -export([reverse_convert_cash/2]). +-export([maybe_convert_cash/2]). +-export([maybe_reverse_convert_cash/2]). + +-export_type([exchange_context/0]). -type cash() :: dmsl_domain_thrift:'Cash'(). +-type exchange_context() :: dmsl_domain_thrift:'ExchangeContext'(). + +-spec convert_cash(exchange_context(), cash()) -> cash(). +convert_cash(ExchangeContext, Cash) -> + do_convert_cash(ExchangeContext, Cash, forward). + +-spec reverse_convert_cash(exchange_context(), cash()) -> cash(). +reverse_convert_cash(ExchangeContext, Cash) -> + %% We do not use two-way exchange rates for currency pairs. + do_convert_cash(ExchangeContext, Cash, reverse). --spec convert_cash(hg_invoice_payment:exchange_context(), cash()) -> cash(). -convert_cash( +-spec maybe_convert_cash(exchange_context() | undefined, cash()) -> cash(). +maybe_convert_cash(undefined, Cash) -> + Cash; +maybe_convert_cash(ExchangeContext, Cash) -> + convert_cash(ExchangeContext, Cash). + +-spec maybe_reverse_convert_cash(exchange_context() | undefined, cash()) -> cash(). +maybe_reverse_convert_cash(undefined, Cash) -> + Cash; +maybe_reverse_convert_cash(ExchangeContext, Cash) -> + reverse_convert_cash(ExchangeContext, Cash). + +do_convert_cash( #domain_ExchangeContext{ + exchange_rate = ExchangeRate, source_currency = SourceCurrency, - destination_currency = DestinationCurrency, - exchange_rate = ExchangeRate - }, - #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = SourceCurrency}} -) -> - %% Example: - %% Amount: 1000, Src: RUB, Dst: USD, Rate: {P=100, Q=1} (1 USD = 100 RUB) - %% ConvertedAmountRational = {1000,100} - %% ConvertedAmount = 10 - #base_Rational{p = P, q = Q} = ExchangeRate, - RateRational = genlib_rational:new(P, Q), - AmountRational = genlib_rational:new(Amount), - ConvertedAmountRational = genlib_rational:dvd(AmountRational, RateRational), - Rounding = application:get_env(hellgate, exchange_rounding_method, round_half_away_from_zero), - ConvertedAmount = genlib_rational:round(ConvertedAmountRational, Rounding), - #domain_Cash{amount = ConvertedAmount, currency = #domain_CurrencyRef{symbolic_code = DestinationCurrency}}; -convert_cash( - #domain_ExchangeContext{ destination_currency = DestinationCurrency }, - #domain_Cash{currency = #domain_CurrencyRef{symbolic_code = DestinationCurrency}} = Cash + Cash, + Direction ) -> - %% already needed currency - %% skip conversion - Cash. + {InputCurrency, OutputCurrency, SkipCurrency} = + case Direction of + forward -> + {SourceCurrency, DestinationCurrency, DestinationCurrency}; + reverse -> + {DestinationCurrency, SourceCurrency, SourceCurrency} + end, + case Cash of + #domain_Cash{currency = #domain_CurrencyRef{symbolic_code = SkipCurrency}} -> + Cash; + #domain_Cash{ + amount = Amount, + currency = #domain_CurrencyRef{symbolic_code = InputCurrency} + } -> + convert_amount(Amount, ExchangeRate, OutputCurrency, Direction) + end. --spec reverse_convert_cash(hg_invoice_payment:exchange_context(), cash()) -> cash(). -reverse_convert_cash( - #domain_ExchangeContext{ - source_currency = SourceCurrency, - destination_currency = DestinationCurrency, - exchange_rate = ExchangeRate - }, - #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = DestinationCurrency}} -) -> - %% We do not use two-way exchange rates for currency pairs +convert_amount(Amount, ExchangeRate, OutputCurrency, Direction) -> #base_Rational{p = P, q = Q} = ExchangeRate, RateRational = genlib_rational:new(P, Q), AmountRational = genlib_rational:new(Amount), - ReConvertedAmountRational = genlib_rational:mul(AmountRational, RateRational), + ConvertedAmountRational = + case Direction of + forward -> + genlib_rational:dvd(AmountRational, RateRational); + reverse -> + genlib_rational:mul(AmountRational, RateRational) + end, Rounding = application:get_env(hellgate, exchange_rounding_method, round_half_away_from_zero), - ReConvertedAmount = genlib_rational:round(ReConvertedAmountRational, Rounding), - #domain_Cash{amount = ReConvertedAmount, currency = #domain_CurrencyRef{symbolic_code = SourceCurrency}}; -reverse_convert_cash( - #domain_ExchangeContext{ - source_currency = SourceCurrency - }, - #domain_Cash{currency = #domain_CurrencyRef{symbolic_code = SourceCurrency}} = Cash -) -> - %% already needed currency - %% skip conversion - Cash. + ConvertedAmount = genlib_rational:round(ConvertedAmountRational, Rounding), + #domain_Cash{amount = ConvertedAmount, currency = #domain_CurrencyRef{symbolic_code = OutputCurrency}}. diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index 21cdecbc..210de80d 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -238,7 +238,7 @@ exchange_context => exchange_context() }. --type exchange_context() :: dmsl_domain_thrift:'ExchangeContext'(). +-type exchange_context() :: hg_currency_converter:exchange_context(). %% @@ -3017,7 +3017,7 @@ construct_payment_info( PaymentInfo ) -> ExchangeContext = get_exchange_context(St), - {ConvertedCost, _OriginalCost} = maybe_convert_cash(ExchangeContext, Cost), + ConvertedCost = hg_currency_converter:maybe_convert_cash(ExchangeContext, Cost), PaymentInfo#proxy_provider_PaymentInfo{ capture = construct_proxy_capture(ConvertedCost) }; @@ -3033,7 +3033,7 @@ construct_proxy_payment( domain_revision = Revision, payer = Payer, payer_session_info = PayerSessionInfo, - cost = Cost, + cost = Cost0, make_recurrent = MakeRecurrent, skip_recurrent = SkipRecurrent, processing_deadline = Deadline @@ -3043,8 +3043,13 @@ construct_proxy_payment( ) -> ContactInfo = get_contact_info(Payer), PaymentTool = get_payer_payment_tool(Payer), - ExchangeContext = get_exchange_context(St), - {ConvertedCost, OriginalCost} = maybe_convert_cash(ExchangeContext, Cost), + {Cost1, OriginalCost} = + case get_exchange_context(St) of + undefined -> + {Cost0, undefined}; + ExchangeContext -> + {hg_currency_converter:convert_cash(ExchangeContext, Cost0), Cost0} + end, #proxy_provider_InvoicePayment{ id = ID, created_at = CreatedAt, @@ -3052,7 +3057,7 @@ construct_proxy_payment( payment_resource = construct_payment_resource(Payer, St), payment_service = hg_payment_tool:get_payment_service(PaymentTool, Revision), payer_session_info = PayerSessionInfo, - cost = construct_proxy_cash(ConvertedCost), + cost = construct_proxy_cash(Cost1), original_cost = maybe_construct_proxy_cash(OriginalCost), contact_info = ContactInfo, make_recurrent = MakeRecurrent, @@ -3144,23 +3149,6 @@ construct_proxy_capture(Cost) -> cost = construct_proxy_cash(Cost) }. -maybe_convert_cash(undefined, Cost) -> - {Cost, undefined}; -maybe_convert_cash( - #domain_ExchangeContext{source_currency = PaymentCurrency} = ExchangeContext, - #domain_Cash{currency = #domain_CurrencyRef{symbolic_code = PaymentCurrency}} = OriginalCash -) -> - ConvertedCash = hg_currency_converter:convert_cash(ExchangeContext, OriginalCash), - {ConvertedCash, OriginalCash}. - -maybe_reverse_convert_cash(undefined, Cost) -> - Cost; -maybe_reverse_convert_cash( - #domain_ExchangeContext{destination_currency = TerminalCurrency} = ExchangeContext, - #domain_Cash{currency = #domain_CurrencyRef{symbolic_code = TerminalCurrency}} = TerminalCash -) -> - hg_currency_converter:reverse_convert_cash(ExchangeContext, TerminalCash). - %% get_party_obj(#{party := Party, party_config_ref := PartyConfigRef}) -> @@ -3410,7 +3398,7 @@ merge_change(Change = ?cash_changed(_OldCash, NewCash), #st{} = St, Opts) -> ), Payment0 = get_payment(St), ExchangeContext = get_exchange_context(St), - ReConvertedNewCash = maybe_reverse_convert_cash(ExchangeContext, NewCash), + ReConvertedNewCash = hg_currency_converter:maybe_reverse_convert_cash(ExchangeContext, NewCash), Payment1 = Payment0#domain_InvoicePayment{changed_cost = ReConvertedNewCash}, St#st{new_cash = ReConvertedNewCash, new_cash_provided = true, payment = Payment1}; merge_change(Change = ?payment_rollback_started(Failure), St, Opts) -> diff --git a/apps/hellgate/src/hg_invoice_payment_refund.erl b/apps/hellgate/src/hg_invoice_payment_refund.erl index 60208dda..7b26b013 100644 --- a/apps/hellgate/src/hg_invoice_payment_refund.erl +++ b/apps/hellgate/src/hg_invoice_payment_refund.erl @@ -593,7 +593,7 @@ get_refund_created_at(#domain_InvoicePaymentRefund{created_at = CreatedAt}) -> construct_payment_info(PaymentInfo, Refund) -> ExchangeContext = get_injected_exchange_context(Refund), - ConvertedCash = maybe_convert_cash(ExchangeContext, cash(Refund)), + ConvertedCash = hg_currency_converter:maybe_convert_cash(ExchangeContext, cash(Refund)), PaymentInfo#proxy_provider_PaymentInfo{ refund = #proxy_provider_InvoicePaymentRefund{ id = id(Refund), @@ -603,11 +603,6 @@ construct_payment_info(PaymentInfo, Refund) -> } }. -maybe_convert_cash(undefined, Cash) -> - Cash; -maybe_convert_cash(ExchangeContext, Cash) -> - hg_currency_converter:convert_cash(ExchangeContext, Cash). - construct_proxy_cash(#domain_Cash{ amount = Amount, currency = CurrencyRef diff --git a/apps/routing/src/hg_route.erl b/apps/routing/src/hg_route.erl index 9955b185..8a442644 100644 --- a/apps/routing/src/hg_route.erl +++ b/apps/routing/src/hg_route.erl @@ -13,6 +13,8 @@ -export([set_availability/3]). -export([set_conversion/3]). -export([set_priority/2]). +-export([set_rejection_reason/2]). +-export([set_exchange_context/2]). -export([route_data/1]). -export([terminal_ref/1]). @@ -25,7 +27,7 @@ -export([fd_score/1]). -export([blacklisted/1]). -export([rejection_reason/1]). --export([set_rejection_reason/2]). +-export([exchange_context/1]). -export([score/1]). -export([equal/2]). @@ -52,7 +54,7 @@ route_data := route_data(), pin_data => pin_data(), fd_overrides => fd_overrides(), - rejection_reason => route_rejection_reason() | undefined, + rejection_reason => route_rejection_reason(), exchange_context => hg_invoice_payment:exchange_context() }. @@ -168,6 +170,18 @@ set_conversion(C, V, #{route_data := Data = #{fd_score := Score}} = R) -> set_priority(V, #{route_data := Data} = R) -> R#{route_data => Data#{priority => V}}. +-spec set_rejection_reason(route_rejection_reason(), t()) -> + t(). +set_rejection_reason(Reason, R) -> + R#{rejection_reason => Reason}. + +-spec set_exchange_context(hg_invoice_payment:exchange_context() | undefined, t()) -> + t(). +set_exchange_context(undefined, R) -> + R; +set_exchange_context(V, R) -> + R#{exchange_context => V}. + -spec provider_ref(t()) -> provider_ref(). provider_ref(#{provider_ref := Ref}) -> Ref. @@ -216,16 +230,17 @@ blacklisted(_) -> 0. -spec rejection_reason(t()) -> - route_rejection_reason(). + route_rejection_reason() | undefined. rejection_reason(#{rejection_reason := V}) -> V; rejection_reason(_) -> undefined. --spec set_rejection_reason(route_rejection_reason(), t()) -> - t(). -set_rejection_reason(Reason, R) -> - R#{rejection_reason => Reason}. +-spec exchange_context(t()) -> hg_invoice_payment:exchange_context() | undefined. +exchange_context(#{exchange_context := V}) -> + V; +exchange_context(_) -> + undefined. -spec score(t()) -> score(). score(R) -> diff --git a/apps/routing/src/hg_route_collector.erl b/apps/routing/src/hg_route_collector.erl index defa724b..91533b78 100644 --- a/apps/routing/src/hg_route_collector.erl +++ b/apps/routing/src/hg_route_collector.erl @@ -39,10 +39,6 @@ error => get_routes_error() }. --type route_updates() :: #{ - exchange_context => hg_invoice_payment:exchange_context() -}. - -type get_routes_error() :: {misconfiguration, _Reason}. -type blacklist_context() :: hg_inspector:blacklist_context(). @@ -150,17 +146,15 @@ get_table_prohibitions(Prohibitions, VS, Revision) -> [hg_route:t()]. fill_accepted(Predestination, Revision, VS, Routes) -> lists:foldr( - fun(Route, AccIn) -> - PRef = hg_route:provider_ref(Route), - TRef = hg_route:terminal_ref(Route), + fun(Route0, AccIn) -> try - {ok, Updates} = acceptable_terminal(Predestination, PRef, TRef, VS, Revision), - [maps:merge(Route, Updates) | AccIn] + Route1 = acceptable_terminal(Predestination, Route0, VS, Revision), + [Route1 | AccIn] catch {rejected, Reason} -> - [hg_route:set_accepted({false, {rejected, Reason}}, Route) | AccIn]; + [hg_route:set_accepted({false, {rejected, Reason}}, Route0) | AccIn]; error:{misconfiguration, Reason} -> - [hg_route:set_accepted({false, {misconfiguration, Reason}}, Route) | AccIn] + [hg_route:set_accepted({false, {misconfiguration, Reason}}, Route0) | AccIn] end end, [], @@ -249,29 +243,30 @@ gather_pin_info(#domain_RoutingPin{features = Features}, Ctx) -> -spec acceptable_terminal( route_predestination(), - hg_route:provider_ref(), - hg_route:terminal_ref(), + hg_route:t(), varset(), revision() -) -> {ok, route_updates()} | no_return(). -acceptable_terminal(Predestination, ProviderRef, TerminalRef, VS, Revision) -> +) -> hg_route:t() | no_return(). +acceptable_terminal(Predestination, Route, VS0, Revision) -> + ProviderRef = hg_route:provider_ref(Route), + TerminalRef = hg_route:terminal_ref(Route), {Client, Context} = get_party_client(), Result = party_client_thrift:compute_provider_terminal_terms( ProviderRef, TerminalRef, Revision, - hg_varset:prepare_varset(VS), + hg_varset:prepare_varset(VS0), Client, Context ), case Result of {ok, ProvisionTermSet} -> - {UpdVS, Updates} = maybe_currency_conversion( + {VS1, ExchangeContext} = maybe_currency_conversion( ProvisionTermSet#domain_ProvisionTermSet.payments, - VS + VS0 ), - true = check_terms_acceptability(Predestination, ProvisionTermSet, UpdVS), - {ok, Updates}; + true = check_terms_acceptability(Predestination, ProvisionTermSet, VS1), + hg_route:set_exchange_context(ExchangeContext, Route); {error, #payproc_ProvisionTermSetUndefined{}} -> throw(?rejected({'ProvisionTermSet', undefined})) end. @@ -287,7 +282,7 @@ maybe_currency_conversion( currencies = {value, [#domain_CurrencyRef{symbolic_code = DestinationCurrency} = DestinationCurrencyRef]}, allow_exchange = {constant, true} }, - #{currency := #domain_CurrencyRef{symbolic_code = SourceCurrency}} = VS + #{currency := #domain_CurrencyRef{symbolic_code = SourceCurrency}} = VS0 ) when SourceCurrency =/= DestinationCurrency -> case hg_exrates:get_exchange_rate(SourceCurrency, DestinationCurrency) of {ok, #{p := P, q := Q}} -> @@ -296,11 +291,11 @@ maybe_currency_conversion( destination_currency = DestinationCurrency, exchange_rate = #base_Rational{p = P, q = Q} }, - UpdVS = VS#{ + VS1 = VS0#{ currency => DestinationCurrencyRef, - cost => hg_currency_converter:convert_cash(ExchangeContext, getv(cost, VS)) + cost => hg_currency_converter:convert_cash(ExchangeContext, getv(cost, VS0)) }, - {UpdVS, #{exchange_context => ExchangeContext}}; + {VS1, ExchangeContext}; {error, _} -> throw(?rejected({'PaymentsProvisionTerms', exchange_error})) end; @@ -312,7 +307,7 @@ maybe_currency_conversion( % throw(?rejected({'PaymentsProvisionTerms', too_many_currencies})); maybe_currency_conversion(_Terms, VS) -> %% in other cases we rely on check_terms_acceptability/3 - {VS, #{}}. + {VS, undefined}. check_terms_acceptability(payment, Terms, VS) -> acceptable_payment_terms(Terms#domain_ProvisionTermSet.payments, VS); From 1f293719aba1cf126e033dc14068840ff45223a9 Mon Sep 17 00:00:00 2001 From: ttt161 Date: Mon, 29 Jun 2026 12:23:41 +0300 Subject: [PATCH 8/8] cleanup --- apps/routing/src/hg_route_collector.erl | 15 +++++++++------ compose.yaml | 4 ++-- rebar.config | 3 +-- rebar.lock | 2 +- 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/apps/routing/src/hg_route_collector.erl b/apps/routing/src/hg_route_collector.erl index 91533b78..64adecfb 100644 --- a/apps/routing/src/hg_route_collector.erl +++ b/apps/routing/src/hg_route_collector.erl @@ -299,12 +299,15 @@ maybe_currency_conversion( {error, _} -> throw(?rejected({'PaymentsProvisionTerms', exchange_error})) end; -%% Check production domain config before use -%maybe_currency_conversion( -% #domain_PaymentsProvisionTerms{currencies = {value, Currencies}}, -% _VS -%) when erlang:length(Currencies) > 1 -> -% throw(?rejected({'PaymentsProvisionTerms', too_many_currencies})); +maybe_currency_conversion( + #domain_PaymentsProvisionTerms{ + currencies = {value, Currencies}, + allow_exchange = {constant, true} + }, + _VS +) when erlang:length(Currencies) > 1 -> + %% If currency conversion is allowed, only one currency must be specified + throw(?rejected({'PaymentsProvisionTerms', too_many_currencies})); maybe_currency_conversion(_Terms, VS) -> %% in other cases we rely on check_terms_acceptability/3 {VS, undefined}. diff --git a/compose.yaml b/compose.yaml index 32dfd090..f8842668 100644 --- a/compose.yaml +++ b/compose.yaml @@ -30,7 +30,7 @@ services: command: /sbin/init dmt: - image: ghcr.io/valitydev/dominant-v2:sha-51fb871-epic-XYZ-19-currency-exchange-support + image: ghcr.io/valitydev/dominant-v2:sha-ab393d5 command: /opt/dmt/bin/dmt foreground environment: DMT_KAFKA_ENABLED: "0" @@ -112,7 +112,7 @@ services: retries: 20 party-management: - image: ghcr.io/valitydev/party-management:sha-d4b8374-epic-XYZ-19-exchange-currency-support + image: ghcr.io/valitydev/party-management:sha-e3fe0dc command: /opt/party-management/bin/party-management foreground volumes: - ./test/party-management/sys.config:/opt/party-management/releases/0.1/sys.config diff --git a/rebar.config b/rebar.config index 878c360a..e4fe4ed5 100644 --- a/rebar.config +++ b/rebar.config @@ -36,8 +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.33"}}}, - {damsel, {git, "https://github.com/valitydev/damsel.git", {branch, "XYZ-19/ft/multi-currency"}}}, + {damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.38"}}}, {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 7269ebf3..f1a84ab2 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,"389e46e908c21486d06bf5eea50bcd4b60b8cd45"}}, + {ref,"c529750144dea43ada113436762ee1e340f5503d"}}, 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt-client.git",