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);