From 8fa2d2a38263966f2029c6765acce0fbe3667ede Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=90=D1=80=D1=82=D0=B5=D0=BC?= Date: Wed, 27 Sep 2023 11:57:42 +0300 Subject: [PATCH] OPS-358: Split hg invoice (#90) * refactored hg invoice * fixed * removed unnecessary adj creation * removed for regular adj --- apps/hellgate/include/hg_invoice.hrl | 11 + apps/hellgate/src/hellgate.erl | 2 +- apps/hellgate/src/hg_invoice.erl | 449 ++---------------- apps/hellgate/src/hg_invoice_handler.erl | 430 +++++++++++++++++ apps/hellgate/test/hg_invoice_tests_SUITE.erl | 6 - 5 files changed, 484 insertions(+), 414 deletions(-) create mode 100644 apps/hellgate/include/hg_invoice.hrl create mode 100644 apps/hellgate/src/hg_invoice_handler.erl diff --git a/apps/hellgate/include/hg_invoice.hrl b/apps/hellgate/include/hg_invoice.hrl new file mode 100644 index 0000000..211febd --- /dev/null +++ b/apps/hellgate/include/hg_invoice.hrl @@ -0,0 +1,11 @@ +-ifndef(__hg_invoice__). +-define(__hg_invoice__, true). + +-record(st, { + activity :: undefined | hg_invoice:activity(), + invoice :: undefined | hg_invoice:invoice(), + payments = [] :: [{hg_invoice:payment_id(), hg_invoice:payment_st()}], + party :: undefined | hg_invoice:party() +}). + +-endif. diff --git a/apps/hellgate/src/hellgate.erl b/apps/hellgate/src/hellgate.erl index c08d805..bc3b8e1 100644 --- a/apps/hellgate/src/hellgate.erl +++ b/apps/hellgate/src/hellgate.erl @@ -72,7 +72,7 @@ get_api_child_spec(MachineHandlers, Opts) -> event_handler => {scoper_woody_event_handler, EventHandlerOpts}, handlers => hg_machine:get_service_handlers(MachineHandlers, Opts) ++ [ - construct_service_handler(invoicing, hg_invoice, Opts), + construct_service_handler(invoicing, hg_invoice_handler, Opts), construct_service_handler(invoice_templating, hg_invoice_template, Opts), construct_service_handler(customer_management, hg_customer, Opts), construct_service_handler(recurrent_paytool, hg_recurrent_paytool, Opts), diff --git a/apps/hellgate/src/hg_invoice.erl b/apps/hellgate/src/hg_invoice.erl index ddda030..fedb937 100644 --- a/apps/hellgate/src/hg_invoice.erl +++ b/apps/hellgate/src/hg_invoice.erl @@ -17,6 +17,7 @@ -include("payment_events.hrl"). -include("invoice_events.hrl"). -include("domain.hrl"). +-include("hg_invoice.hrl"). -include_lib("damsel/include/dmsl_repair_thrift.hrl"). @@ -24,17 +25,21 @@ -export([process_callback/2]). +-export_type([activity/0]). +-export_type([invoice/0]). +-export_type([payment_id/0]). +-export_type([payment_st/0]). +-export_type([party/0]). + %% Public interface -export([get/1]). -export([get_payment/2]). -export([get_payment_opts/1]). - -%% Woody handler called by hg_woody_service_wrapper - --behaviour(hg_woody_service_wrapper). - --export([handle_function/3]). +-export([create/5]). +-export([marshal_invoice/1]). +-export([unmarshal_history/1]). +-export([collapse_history/1]). %% Machine callbacks @@ -55,24 +60,24 @@ assert_party_operable/1, assert_party_unblocked/1, assert_shop_operable/1, - assert_shop_unblocked/1, - assert_shop_exists/1 + assert_shop_unblocked/1 ]). %% Internal types -define(invalid_invoice_status(Status), #payproc_InvalidInvoiceStatus{status = Status}). - --record(st, { - activity :: undefined | activity(), - invoice :: undefined | invoice(), - payments = [] :: [{payment_id(), payment_st()}], - party :: undefined | party() -}). +-define(payment_pending(PaymentID), #payproc_InvoicePaymentPending{id = PaymentID}). -type st() :: #st{}. -type invoice_change() :: dmsl_payproc_thrift:'InvoiceChange'(). +-type invoice_params() :: dmsl_payproc_thrift:'InvoiceParams'(). +-type invoice() :: dmsl_domain_thrift:'Invoice'(). +-type allocation() :: dmsl_domain_thrift:'Allocation'(). +-type party() :: dmsl_domain_thrift:'Party'(). + +-type payment_id() :: dmsl_domain_thrift:'InvoicePaymentID'(). +-type payment_st() :: hg_invoice_payment:st(). -type activity() :: invoice @@ -121,172 +126,28 @@ get_payment_opts(Revision, _, St = #st{invoice = Invoice}) -> timestamp => hg_datetime:format_now() }. -%% - --spec handle_function(woody:func(), woody:args(), hg_woody_service_wrapper:handler_opts()) -> term() | no_return(). -handle_function(Func, Args, Opts) -> - scoper:scope( - invoicing, - fun() -> - handle_function_(Func, Args, Opts) - end - ). - --spec handle_function_(woody:func(), woody:args(), hg_woody_service_wrapper:handler_opts()) -> term() | no_return(). -handle_function_('Create', {InvoiceParams}, _Opts) -> - DomainRevision = hg_domain:head(), - InvoiceID = InvoiceParams#payproc_InvoiceParams.id, - _ = set_invoicing_meta(InvoiceID), - PartyID = InvoiceParams#payproc_InvoiceParams.party_id, - ShopID = InvoiceParams#payproc_InvoiceParams.shop_id, - Party = hg_party:get_party(PartyID), - Shop = assert_shop_exists(hg_party:get_shop(ShopID, Party)), - _ = assert_party_shop_operable(Shop, Party), - VS = #{ - cost => InvoiceParams#payproc_InvoiceParams.cost, - shop_id => Shop#domain_Shop.id - }, - MerchantTerms = hg_invoice_utils:get_merchant_terms(Party, Shop, DomainRevision, hg_datetime:format_now(), VS), - ok = validate_invoice_params(InvoiceParams, Shop, MerchantTerms), - AllocationPrototype = InvoiceParams#payproc_InvoiceParams.allocation, - Cost = InvoiceParams#payproc_InvoiceParams.cost, - Allocation = maybe_allocation(AllocationPrototype, Cost, MerchantTerms, Party, Shop), - ok = ensure_started(InvoiceID, undefined, Party#domain_Party.revision, InvoiceParams, Allocation), - get_invoice_state(get_state(InvoiceID)); -handle_function_('CreateWithTemplate', {Params}, _Opts) -> - DomainRevision = hg_domain:head(), - InvoiceID = Params#payproc_InvoiceWithTemplateParams.id, - _ = set_invoicing_meta(InvoiceID), - TplID = Params#payproc_InvoiceWithTemplateParams.template_id, - {Party, Shop, InvoiceParams} = make_invoice_params(Params), - VS = #{ - cost => InvoiceParams#payproc_InvoiceParams.cost, - shop_id => Shop#domain_Shop.id - }, - MerchantTerms = hg_invoice_utils:get_merchant_terms(Party, Shop, DomainRevision, hg_datetime:format_now(), VS), - ok = validate_invoice_params(InvoiceParams, Shop, MerchantTerms), - AllocationPrototype = InvoiceParams#payproc_InvoiceParams.allocation, - Cost = InvoiceParams#payproc_InvoiceParams.cost, - Allocation = maybe_allocation(AllocationPrototype, Cost, MerchantTerms, Party, Shop), - ok = ensure_started(InvoiceID, TplID, Party#domain_Party.revision, InvoiceParams, Allocation), - get_invoice_state(get_state(InvoiceID)); -handle_function_('CapturePaymentNew', Args, Opts) -> - handle_function_('CapturePayment', Args, Opts); -handle_function_('Get', {InvoiceID, #payproc_EventRange{'after' = AfterID, limit = Limit}}, _Opts) -> - _ = set_invoicing_meta(InvoiceID), - St = get_state(InvoiceID, AfterID, Limit), - get_invoice_state(St); -%% TODO Удалить после перехода на новый протокол -handle_function_('Get', {InvoiceID, undefined}, _Opts) -> - _ = set_invoicing_meta(InvoiceID), - St = get_state(InvoiceID), - get_invoice_state(St); -handle_function_('GetEvents', {InvoiceID, Range}, _Opts) -> - _ = set_invoicing_meta(InvoiceID), - get_public_history(InvoiceID, Range); -handle_function_('GetPayment', {InvoiceID, PaymentID}, _Opts) -> - _ = set_invoicing_meta(InvoiceID, PaymentID), - St = get_state(InvoiceID), - get_payment_state(get_payment_session(PaymentID, St)); -handle_function_('GetPaymentRefund', {InvoiceID, PaymentID, ID}, _Opts) -> - _ = set_invoicing_meta(InvoiceID, PaymentID), - St = get_state(InvoiceID), - hg_invoice_payment:get_refund(ID, get_payment_session(PaymentID, St)); -handle_function_('GetPaymentChargeback', {InvoiceID, PaymentID, ID}, _Opts) -> - _ = set_invoicing_meta(InvoiceID, PaymentID), - St = get_state(InvoiceID), - CBSt = hg_invoice_payment:get_chargeback_state(ID, get_payment_session(PaymentID, St)), - hg_invoice_payment_chargeback:get(CBSt); -handle_function_('GetPaymentAdjustment', {InvoiceID, PaymentID, ID}, _Opts) -> - _ = set_invoicing_meta(InvoiceID, PaymentID), - St = get_state(InvoiceID), - hg_invoice_payment:get_adjustment(ID, get_payment_session(PaymentID, St)); -handle_function_('ComputeTerms', {InvoiceID, PartyRevision0}, _Opts) -> - _ = set_invoicing_meta(InvoiceID), - St = get_state(InvoiceID), - Timestamp = get_created_at(St), - VS = hg_varset:prepare_shop_terms_varset(#{ - cost => get_cost(St) - }), - hg_invoice_utils:compute_shop_terms( - get_party_id(St), - get_shop_id(St), - Timestamp, - hg_maybe:get_defined(PartyRevision0, {timestamp, Timestamp}), - VS - ); -handle_function_(Fun, Args, _Opts) when - Fun =:= 'StartPayment' orelse - Fun =:= 'RegisterPayment' orelse - Fun =:= 'CapturePayment' orelse - Fun =:= 'CancelPayment' orelse - Fun =:= 'RefundPayment' orelse - Fun =:= 'CreateManualRefund' orelse - Fun =:= 'CreateChargeback' orelse - Fun =:= 'CancelChargeback' orelse - Fun =:= 'AcceptChargeback' orelse - Fun =:= 'RejectChargeback' orelse - Fun =:= 'ReopenChargeback' orelse - Fun =:= 'CreatePaymentAdjustment' orelse - Fun =:= 'Fulfill' orelse - Fun =:= 'Rescind' --> - InvoiceID = erlang:element(1, Args), - _ = set_invoicing_meta(InvoiceID), - call(InvoiceID, Fun, Args); -handle_function_('Repair', {InvoiceID, Changes, Action, Params}, _Opts) -> - _ = set_invoicing_meta(InvoiceID), - repair(InvoiceID, {changes, Changes, Action, Params}); -handle_function_('RepairWithScenario', {InvoiceID, Scenario}, _Opts) -> - _ = set_invoicing_meta(InvoiceID), - repair(InvoiceID, {scenario, Scenario}); -handle_function_('GetPaymentRoutesLimitValues', {InvoiceID, PaymentID}, _Opts) -> - _ = set_invoicing_meta(InvoiceID, PaymentID), - St = get_state(InvoiceID), - hg_invoice_payment:get_limit_values(get_payment_session(PaymentID, St), get_payment_opts(St)). - -maybe_allocation(undefined, _Cost, _MerchantTerms, _Party, _Shop) -> - undefined; -maybe_allocation(AllocationPrototype, Cost, MerchantTerms, Party, Shop) -> - PaymentTerms = MerchantTerms#domain_TermSet.payments, - AllocationSelector = PaymentTerms#domain_PaymentsServiceTerms.allocations, - case - hg_allocation:calculate( - AllocationPrototype, - Party, - Shop, - Cost, - AllocationSelector - ) - of - {ok, A} -> - A; - {error, allocation_not_allowed} -> - throw(#payproc_AllocationNotAllowed{}); - {error, amount_exceeded} -> - throw(#payproc_AllocationExceededPaymentAmount{}); - {error, {invalid_transaction, Transaction, Details}} -> - throw(#payproc_AllocationInvalidTransaction{ - transaction = marshal_transaction(Transaction), - reason = marshal_allocation_details(Details) - }) - end. - -marshal_transaction(#domain_AllocationTransaction{} = T) -> - {transaction, T}; -marshal_transaction(#domain_AllocationTransactionPrototype{} = TP) -> - {transaction_prototype, TP}. - -marshal_allocation_details(negative_amount) -> - <<"Transaction amount is negative">>; -marshal_allocation_details(zero_amount) -> - <<"Transaction amount is zero">>; -marshal_allocation_details(target_conflict) -> - <<"Transaction with similar target">>; -marshal_allocation_details(currency_mismatch) -> - <<"Transaction currency mismatch">>; -marshal_allocation_details(payment_institutions_mismatch) -> - <<"Transaction target shop Payment Institution mismatch">>. +-spec create(hg_machine:id(), undefined | hg_machine:id(), hg_party:party_revision(), invoice_params(), allocation()) -> + invoice(). +create(ID, InvoiceTplID, PartyRevision, V = #payproc_InvoiceParams{}, Allocation) -> + OwnerID = V#payproc_InvoiceParams.party_id, + ShopID = V#payproc_InvoiceParams.shop_id, + Cost = V#payproc_InvoiceParams.cost, + #domain_Invoice{ + id = ID, + shop_id = ShopID, + owner_id = OwnerID, + party_revision = PartyRevision, + created_at = hg_datetime:format_now(), + status = ?invoice_unpaid(), + cost = Cost, + due = V#payproc_InvoiceParams.due, + details = V#payproc_InvoiceParams.details, + context = V#payproc_InvoiceParams.context, + template_id = InvoiceTplID, + external_id = V#payproc_InvoiceParams.external_id, + client_info = V#payproc_InvoiceParams.client_info, + allocation = Allocation + }. %%----------------- invoice asserts assert_invoice(Checks, #st{} = St) when is_list(Checks) -> @@ -318,15 +179,6 @@ assert_party_shop_unblocked(Shop, Party) -> _ = assert_shop_unblocked(Shop), ok. -get_invoice_state(#st{invoice = Invoice, payments = Payments}) -> - #payproc_Invoice{ - invoice = Invoice, - payments = [ - get_payment_state(PaymentSession) - || {_PaymentID, PaymentSession} <- Payments - ] - }. - get_payment_state(PaymentSession) -> Refunds = hg_invoice_payment:get_refunds(PaymentSession), LegacyRefunds = @@ -349,12 +201,6 @@ get_payment_state(PaymentSession) -> allocation = hg_invoice_payment:get_allocation(PaymentSession) }. -set_invoicing_meta(InvoiceID) -> - scoper:add_meta(#{invoice_id => InvoiceID}). - -set_invoicing_meta(InvoiceID, PaymentID) -> - scoper:add_meta(#{invoice_id => InvoiceID, payment_id => PaymentID}). - %% -type tag() :: dmsl_base_thrift:'Tag'(). @@ -393,73 +239,6 @@ fail(Id) -> %% -get_history(ID) -> - History = hg_machine:get_history(?NS, ID), - unmarshal_history(map_history_error(History)). - -get_history(ID, AfterID, Limit) -> - History = hg_machine:get_history(?NS, ID, AfterID, Limit), - unmarshal_history(map_history_error(History)). - -get_state(ID) -> - collapse_history(get_history(ID)). - -get_state(ID, AfterID, Limit) -> - collapse_history(get_history(ID, AfterID, Limit)). - -get_public_history(InvoiceID, #payproc_EventRange{'after' = AfterID, limit = Limit}) -> - [publish_invoice_event(InvoiceID, Ev) || Ev <- get_history(InvoiceID, AfterID, Limit)]. - -publish_invoice_event(InvoiceID, {ID, Dt, Event}) -> - #payproc_Event{ - id = ID, - source = {invoice_id, InvoiceID}, - created_at = Dt, - payload = ?invoice_ev(Event) - }. - -ensure_started(ID, TemplateID, PartyRevision, Params, Allocation) -> - Invoice = create_invoice(ID, TemplateID, PartyRevision, Params, Allocation), - case hg_machine:start(?NS, ID, marshal_invoice(Invoice)) of - {ok, _} -> ok; - {error, exists} -> ok; - {error, Reason} -> erlang:error(Reason) - end. - -call(ID, Function, Args) -> - case hg_machine:thrift_call(?NS, ID, invoicing, {'Invoicing', Function}, Args) of - ok -> ok; - {ok, Reply} -> Reply; - {exception, Exception} -> erlang:throw(Exception); - {error, notfound} -> erlang:throw(#payproc_InvoiceNotFound{}); - {error, Error} -> erlang:error(Error) - end. - -repair(ID, Args) -> - case hg_machine:repair(?NS, ID, Args) of - {ok, _Result} -> ok; - {error, notfound} -> erlang:throw(#payproc_InvoiceNotFound{}); - {error, working} -> erlang:throw(#base_InvalidRequest{errors = [<<"No need to repair">>]}); - {error, Reason} -> erlang:error(Reason) - end. - -map_history_error({ok, Result}) -> - Result; -map_history_error({error, notfound}) -> - throw(#payproc_InvoiceNotFound{}). - -%% - --type invoice() :: dmsl_domain_thrift:'Invoice'(). --type party() :: dmsl_domain_thrift:'Party'(). - --type payment_id() :: dmsl_domain_thrift:'InvoicePaymentID'(). --type payment_st() :: hg_invoice_payment:st(). - --define(payment_pending(PaymentID), #payproc_InvoicePaymentPending{id = PaymentID}). - -%% - -spec namespace() -> hg_machine:ns(). namespace() -> ?NS. @@ -982,27 +761,6 @@ get_chargeback_state(ID, PaymentState) -> %% -create_invoice(ID, InvoiceTplID, PartyRevision, V = #payproc_InvoiceParams{}, Allocation) -> - OwnerID = V#payproc_InvoiceParams.party_id, - ShopID = V#payproc_InvoiceParams.shop_id, - Cost = V#payproc_InvoiceParams.cost, - #domain_Invoice{ - id = ID, - shop_id = ShopID, - owner_id = OwnerID, - party_revision = PartyRevision, - created_at = hg_datetime:format_now(), - status = ?invoice_unpaid(), - cost = Cost, - due = V#payproc_InvoiceParams.due, - details = V#payproc_InvoiceParams.details, - context = V#payproc_InvoiceParams.context, - template_id = InvoiceTplID, - external_id = V#payproc_InvoiceParams.external_id, - client_info = V#payproc_InvoiceParams.client_info, - allocation = Allocation - }. - create_payment_id(#st{payments = Payments}) -> integer_to_binary(length(Payments) + 1). @@ -1101,12 +859,6 @@ get_party_id(#st{invoice = #domain_Invoice{owner_id = PartyID}}) -> get_shop_id(#st{invoice = #domain_Invoice{shop_id = ShopID}}) -> ShopID. -get_created_at(#st{invoice = #domain_Invoice{created_at = CreatedAt}}) -> - CreatedAt. - -get_cost(#st{invoice = #domain_Invoice{cost = Cash}}) -> - Cash. - get_payment_session(PaymentID, St) -> case try_get_payment_session(PaymentID, St) of PaymentSession when PaymentSession /= undefined -> @@ -1128,123 +880,6 @@ set_payment_session(PaymentID, PaymentSession, St = #st{payments = Payments}) -> %% -make_invoice_params(Params) -> - #payproc_InvoiceWithTemplateParams{ - id = InvoiceID, - template_id = TplID, - cost = Cost, - context = Context, - external_id = ExternalID - } = Params, - #domain_InvoiceTemplate{ - owner_id = PartyID, - shop_id = ShopID, - invoice_lifetime = Lifetime, - product = Product, - description = Description, - details = TplDetails, - context = TplContext - } = hg_invoice_template:get(TplID), - Party = hg_party:get_party(PartyID), - Shop = assert_shop_exists(hg_party:get_shop(ShopID, Party)), - _ = assert_party_shop_operable(Shop, Party), - Cart = make_invoice_cart(Cost, TplDetails, Shop), - InvoiceDetails = #domain_InvoiceDetails{ - product = Product, - description = Description, - cart = Cart - }, - InvoiceCost = hg_invoice_utils:get_cart_amount(Cart), - InvoiceDue = make_invoice_due_date(Lifetime), - InvoiceContext = make_invoice_context(Context, TplContext), - InvoiceParams = #payproc_InvoiceParams{ - id = InvoiceID, - party_id = PartyID, - shop_id = ShopID, - details = InvoiceDetails, - due = InvoiceDue, - cost = InvoiceCost, - context = InvoiceContext, - external_id = ExternalID - }, - {Party, Shop, InvoiceParams}. - -validate_invoice_params(#payproc_InvoiceParams{cost = Cost}, Shop, MerchantTerms) -> - ok = validate_invoice_cost(Cost, Shop, MerchantTerms), - ok. - -validate_invoice_cost(Cost, Shop, #domain_TermSet{payments = PaymentTerms}) -> - _ = hg_invoice_utils:validate_cost(Cost, Shop), - _ = hg_invoice_utils:assert_cost_payable(Cost, PaymentTerms), - ok. - -make_invoice_cart(_, {cart, Cart}, _Shop) -> - Cart; -make_invoice_cart(Cost, {product, TplProduct}, Shop) -> - #domain_InvoiceTemplateProduct{ - product = Product, - price = TplPrice, - metadata = Metadata - } = TplProduct, - #domain_InvoiceCart{ - lines = [ - #domain_InvoiceLine{ - product = Product, - quantity = 1, - price = get_templated_price(Cost, TplPrice, Shop), - metadata = Metadata - } - ] - }. - -get_templated_price(undefined, {fixed, Cost}, Shop) -> - get_cost(Cost, Shop); -get_templated_price(undefined, _, _Shop) -> - throw(#base_InvalidRequest{errors = [?INVOICE_TPL_NO_COST]}); -get_templated_price(Cost, {fixed, Cost}, Shop) -> - get_cost(Cost, Shop); -get_templated_price(_Cost, {fixed, _CostTpl}, _Shop) -> - throw(#base_InvalidRequest{errors = [?INVOICE_TPL_BAD_COST]}); -get_templated_price(Cost, {range, Range}, Shop) -> - _ = assert_cost_in_range(Cost, Range), - get_cost(Cost, Shop); -get_templated_price(Cost, {unlim, _}, Shop) -> - get_cost(Cost, Shop). - -get_cost(Cost, Shop) -> - ok = hg_invoice_utils:validate_cost(Cost, Shop), - Cost. - -assert_cost_in_range( - #domain_Cash{amount = Amount, currency = Currency}, - #domain_CashRange{ - upper = {UType, #domain_Cash{amount = UAmount, currency = Currency}}, - lower = {LType, #domain_Cash{amount = LAmount, currency = Currency}} - } -) -> - _ = assert_less_than(LType, LAmount, Amount), - _ = assert_less_than(UType, Amount, UAmount), - ok; -assert_cost_in_range(_, _) -> - throw(#base_InvalidRequest{errors = [?INVOICE_TPL_BAD_CURRENCY]}). - -assert_less_than(inclusive, Less, More) when Less =< More -> - ok; -assert_less_than(exclusive, Less, More) when Less < More -> - ok; -assert_less_than(_, _, _) -> - throw(#base_InvalidRequest{errors = [?INVOICE_TPL_BAD_AMOUNT]}). - -make_invoice_due_date(#domain_LifetimeInterval{years = YY, months = MM, days = DD}) -> - hg_datetime:add_interval(hg_datetime:format_now(), {YY, MM, DD}). - -make_invoice_context(undefined, TplContext) -> - TplContext; -make_invoice_context(Context, _) -> - Context. - -%% - log_changes(Changes, St) -> lists:foreach(fun(C) -> log_change(C, St) end, Changes). diff --git a/apps/hellgate/src/hg_invoice_handler.erl b/apps/hellgate/src/hg_invoice_handler.erl new file mode 100644 index 0000000..19737f4 --- /dev/null +++ b/apps/hellgate/src/hg_invoice_handler.erl @@ -0,0 +1,430 @@ +-module(hg_invoice_handler). + +-include("payment_events.hrl"). +-include("invoice_events.hrl"). +-include("domain.hrl"). +-include("hg_invoice.hrl"). + +%% Woody handler called by hg_woody_service_wrapper + +-behaviour(hg_woody_service_wrapper). + +-export([handle_function/3]). + +%% Internal + +-import(hg_invoice_utils, [ + assert_party_operable/1, + assert_shop_operable/1, + assert_shop_exists/1 +]). + +%% API + +-spec handle_function(woody:func(), woody:args(), hg_woody_service_wrapper:handler_opts()) -> term() | no_return(). +handle_function(Func, Args, Opts) -> + scoper:scope( + invoicing, + fun() -> + handle_function_(Func, Args, Opts) + end + ). + +-spec handle_function_(woody:func(), woody:args(), hg_woody_service_wrapper:handler_opts()) -> term() | no_return(). +handle_function_('Create', {InvoiceParams}, _Opts) -> + DomainRevision = hg_domain:head(), + InvoiceID = InvoiceParams#payproc_InvoiceParams.id, + _ = set_invoicing_meta(InvoiceID), + PartyID = InvoiceParams#payproc_InvoiceParams.party_id, + ShopID = InvoiceParams#payproc_InvoiceParams.shop_id, + Party = hg_party:get_party(PartyID), + Shop = assert_shop_exists(hg_party:get_shop(ShopID, Party)), + _ = assert_party_shop_operable(Shop, Party), + VS = #{ + cost => InvoiceParams#payproc_InvoiceParams.cost, + shop_id => Shop#domain_Shop.id + }, + MerchantTerms = hg_invoice_utils:get_merchant_terms(Party, Shop, DomainRevision, hg_datetime:format_now(), VS), + ok = validate_invoice_params(InvoiceParams, Shop, MerchantTerms), + AllocationPrototype = InvoiceParams#payproc_InvoiceParams.allocation, + Cost = InvoiceParams#payproc_InvoiceParams.cost, + Allocation = maybe_allocation(AllocationPrototype, Cost, MerchantTerms, Party, Shop), + ok = ensure_started(InvoiceID, undefined, Party#domain_Party.revision, InvoiceParams, Allocation), + get_invoice_state(get_state(InvoiceID)); +handle_function_('CreateWithTemplate', {Params}, _Opts) -> + DomainRevision = hg_domain:head(), + InvoiceID = Params#payproc_InvoiceWithTemplateParams.id, + _ = set_invoicing_meta(InvoiceID), + TplID = Params#payproc_InvoiceWithTemplateParams.template_id, + {Party, Shop, InvoiceParams} = make_invoice_params(Params), + VS = #{ + cost => InvoiceParams#payproc_InvoiceParams.cost, + shop_id => Shop#domain_Shop.id + }, + MerchantTerms = hg_invoice_utils:get_merchant_terms(Party, Shop, DomainRevision, hg_datetime:format_now(), VS), + ok = validate_invoice_params(InvoiceParams, Shop, MerchantTerms), + AllocationPrototype = InvoiceParams#payproc_InvoiceParams.allocation, + Cost = InvoiceParams#payproc_InvoiceParams.cost, + Allocation = maybe_allocation(AllocationPrototype, Cost, MerchantTerms, Party, Shop), + ok = ensure_started(InvoiceID, TplID, Party#domain_Party.revision, InvoiceParams, Allocation), + get_invoice_state(get_state(InvoiceID)); +handle_function_('CapturePaymentNew', Args, Opts) -> + handle_function_('CapturePayment', Args, Opts); +handle_function_('Get', {InvoiceID, #payproc_EventRange{'after' = AfterID, limit = Limit}}, _Opts) -> + _ = set_invoicing_meta(InvoiceID), + St = get_state(InvoiceID, AfterID, Limit), + get_invoice_state(St); +handle_function_('GetEvents', {InvoiceID, Range}, _Opts) -> + _ = set_invoicing_meta(InvoiceID), + get_public_history(InvoiceID, Range); +handle_function_('GetPayment', {InvoiceID, PaymentID}, _Opts) -> + _ = set_invoicing_meta(InvoiceID, PaymentID), + St = get_state(InvoiceID), + get_payment_state(get_payment_session(PaymentID, St)); +handle_function_('GetPaymentRefund', {InvoiceID, PaymentID, ID}, _Opts) -> + _ = set_invoicing_meta(InvoiceID, PaymentID), + St = get_state(InvoiceID), + hg_invoice_payment:get_refund(ID, get_payment_session(PaymentID, St)); +handle_function_('GetPaymentChargeback', {InvoiceID, PaymentID, ID}, _Opts) -> + _ = set_invoicing_meta(InvoiceID, PaymentID), + St = get_state(InvoiceID), + CBSt = hg_invoice_payment:get_chargeback_state(ID, get_payment_session(PaymentID, St)), + hg_invoice_payment_chargeback:get(CBSt); +handle_function_('GetPaymentAdjustment', {InvoiceID, PaymentID, ID}, _Opts) -> + _ = set_invoicing_meta(InvoiceID, PaymentID), + St = get_state(InvoiceID), + hg_invoice_payment:get_adjustment(ID, get_payment_session(PaymentID, St)); +handle_function_('ComputeTerms', {InvoiceID, PartyRevision0}, _Opts) -> + _ = set_invoicing_meta(InvoiceID), + St = get_state(InvoiceID), + Timestamp = get_created_at(St), + VS = hg_varset:prepare_shop_terms_varset(#{ + cost => get_cost(St) + }), + hg_invoice_utils:compute_shop_terms( + get_party_id(St), + get_shop_id(St), + Timestamp, + hg_maybe:get_defined(PartyRevision0, {timestamp, Timestamp}), + VS + ); +handle_function_(Fun, Args, _Opts) when + Fun =:= 'StartPayment' orelse + Fun =:= 'RegisterPayment' orelse + Fun =:= 'CapturePayment' orelse + Fun =:= 'CancelPayment' orelse + Fun =:= 'RefundPayment' orelse + Fun =:= 'CreateManualRefund' orelse + Fun =:= 'CreateChargeback' orelse + Fun =:= 'CancelChargeback' orelse + Fun =:= 'AcceptChargeback' orelse + Fun =:= 'RejectChargeback' orelse + Fun =:= 'ReopenChargeback' orelse + Fun =:= 'CreatePaymentAdjustment' orelse + Fun =:= 'Fulfill' orelse + Fun =:= 'Rescind' +-> + InvoiceID = erlang:element(1, Args), + _ = set_invoicing_meta(InvoiceID), + call(InvoiceID, Fun, Args); +handle_function_('Repair', {InvoiceID, Changes, Action, Params}, _Opts) -> + _ = set_invoicing_meta(InvoiceID), + repair(InvoiceID, {changes, Changes, Action, Params}); +handle_function_('RepairWithScenario', {InvoiceID, Scenario}, _Opts) -> + _ = set_invoicing_meta(InvoiceID), + repair(InvoiceID, {scenario, Scenario}); +handle_function_('GetPaymentRoutesLimitValues', {InvoiceID, PaymentID}, _Opts) -> + _ = set_invoicing_meta(InvoiceID, PaymentID), + St = get_state(InvoiceID), + hg_invoice_payment:get_limit_values(get_payment_session(PaymentID, St), hg_invoice:get_payment_opts(St)). + +ensure_started(ID, TemplateID, PartyRevision, Params, Allocation) -> + Invoice = hg_invoice:create(ID, TemplateID, PartyRevision, Params, Allocation), + case hg_machine:start(hg_invoice:namespace(), ID, hg_invoice:marshal_invoice(Invoice)) of + {ok, _} -> ok; + {error, exists} -> ok; + {error, Reason} -> erlang:error(Reason) + end. + +call(ID, Function, Args) -> + case hg_machine:thrift_call(hg_invoice:namespace(), ID, invoicing, {'Invoicing', Function}, Args) of + ok -> ok; + {ok, Reply} -> Reply; + {exception, Exception} -> erlang:throw(Exception); + {error, notfound} -> erlang:throw(#payproc_InvoiceNotFound{}); + {error, Error} -> erlang:error(Error) + end. + +repair(ID, Args) -> + case hg_machine:repair(hg_invoice:namespace(), ID, Args) of + {ok, _Result} -> ok; + {error, notfound} -> erlang:throw(#payproc_InvoiceNotFound{}); + {error, working} -> erlang:throw(#base_InvalidRequest{errors = [<<"No need to repair">>]}); + {error, Reason} -> erlang:error(Reason) + end. + +maybe_allocation(undefined, _Cost, _MerchantTerms, _Party, _Shop) -> + undefined; +maybe_allocation(AllocationPrototype, Cost, MerchantTerms, Party, Shop) -> + PaymentTerms = MerchantTerms#domain_TermSet.payments, + AllocationSelector = PaymentTerms#domain_PaymentsServiceTerms.allocations, + case + hg_allocation:calculate( + AllocationPrototype, + Party, + Shop, + Cost, + AllocationSelector + ) + of + {ok, A} -> + A; + {error, allocation_not_allowed} -> + throw(#payproc_AllocationNotAllowed{}); + {error, amount_exceeded} -> + throw(#payproc_AllocationExceededPaymentAmount{}); + {error, {invalid_transaction, Transaction, Details}} -> + throw(#payproc_AllocationInvalidTransaction{ + transaction = marshal_transaction(Transaction), + reason = marshal_allocation_details(Details) + }) + end. + +marshal_transaction(#domain_AllocationTransaction{} = T) -> + {transaction, T}; +marshal_transaction(#domain_AllocationTransactionPrototype{} = TP) -> + {transaction_prototype, TP}. + +marshal_allocation_details(negative_amount) -> + <<"Transaction amount is negative">>; +marshal_allocation_details(zero_amount) -> + <<"Transaction amount is zero">>; +marshal_allocation_details(target_conflict) -> + <<"Transaction with similar target">>; +marshal_allocation_details(currency_mismatch) -> + <<"Transaction currency mismatch">>; +marshal_allocation_details(payment_institutions_mismatch) -> + <<"Transaction target shop Payment Institution mismatch">>. + +%%----------------- invoice asserts + +assert_party_shop_operable(Shop, Party) -> + _ = assert_party_operable(Party), + _ = assert_shop_operable(Shop), + ok. + +get_invoice_state(#st{invoice = Invoice, payments = Payments}) -> + #payproc_Invoice{ + invoice = Invoice, + payments = [ + get_payment_state(PaymentSession) + || {_PaymentID, PaymentSession} <- Payments + ] + }. + +get_payment_state(PaymentSession) -> + Refunds = hg_invoice_payment:get_refunds(PaymentSession), + LegacyRefunds = + lists:map( + fun(#payproc_InvoicePaymentRefund{refund = R}) -> + R + end, + Refunds + ), + #payproc_InvoicePayment{ + payment = hg_invoice_payment:get_payment(PaymentSession), + adjustments = hg_invoice_payment:get_adjustments(PaymentSession), + chargebacks = hg_invoice_payment:get_chargebacks(PaymentSession), + route = hg_invoice_payment:get_route(PaymentSession), + cash_flow = hg_invoice_payment:get_final_cashflow(PaymentSession), + legacy_refunds = LegacyRefunds, + refunds = Refunds, + sessions = hg_invoice_payment:get_sessions(PaymentSession), + last_transaction_info = hg_invoice_payment:get_trx(PaymentSession), + allocation = hg_invoice_payment:get_allocation(PaymentSession) + }. + +set_invoicing_meta(InvoiceID) -> + scoper:add_meta(#{invoice_id => InvoiceID}). + +set_invoicing_meta(InvoiceID, PaymentID) -> + scoper:add_meta(#{invoice_id => InvoiceID, payment_id => PaymentID}). + +%% + +get_state(ID) -> + hg_invoice:collapse_history(get_history(ID)). + +get_state(ID, AfterID, Limit) -> + hg_invoice:collapse_history(get_history(ID, AfterID, Limit)). + +get_history(ID) -> + History = hg_machine:get_history(hg_invoice:namespace(), ID), + hg_invoice:unmarshal_history(map_history_error(History)). + +get_history(ID, AfterID, Limit) -> + History = hg_machine:get_history(hg_invoice:namespace(), ID, AfterID, Limit), + hg_invoice:unmarshal_history(map_history_error(History)). + +get_public_history(InvoiceID, #payproc_EventRange{'after' = AfterID, limit = Limit}) -> + [publish_invoice_event(InvoiceID, Ev) || Ev <- get_history(InvoiceID, AfterID, Limit)]. + +publish_invoice_event(InvoiceID, {ID, Dt, Event}) -> + #payproc_Event{ + id = ID, + source = {invoice_id, InvoiceID}, + created_at = Dt, + payload = ?invoice_ev(Event) + }. + +map_history_error({ok, Result}) -> + Result; +map_history_error({error, notfound}) -> + throw(#payproc_InvoiceNotFound{}). + +%% + +get_party_id(#st{invoice = #domain_Invoice{owner_id = PartyID}}) -> + PartyID. + +get_shop_id(#st{invoice = #domain_Invoice{shop_id = ShopID}}) -> + ShopID. + +get_created_at(#st{invoice = #domain_Invoice{created_at = CreatedAt}}) -> + CreatedAt. + +get_cost(#st{invoice = #domain_Invoice{cost = Cash}}) -> + Cash. + +get_payment_session(PaymentID, St) -> + case try_get_payment_session(PaymentID, St) of + PaymentSession when PaymentSession /= undefined -> + PaymentSession; + undefined -> + throw(#payproc_InvoicePaymentNotFound{}) + end. + +try_get_payment_session(PaymentID, #st{payments = Payments}) -> + case lists:keyfind(PaymentID, 1, Payments) of + {PaymentID, PaymentSession} -> + PaymentSession; + false -> + undefined + end. + +%% + +make_invoice_params(Params) -> + #payproc_InvoiceWithTemplateParams{ + id = InvoiceID, + template_id = TplID, + cost = Cost, + context = Context, + external_id = ExternalID + } = Params, + #domain_InvoiceTemplate{ + owner_id = PartyID, + shop_id = ShopID, + invoice_lifetime = Lifetime, + product = Product, + description = Description, + details = TplDetails, + context = TplContext + } = hg_invoice_template:get(TplID), + Party = hg_party:get_party(PartyID), + Shop = assert_shop_exists(hg_party:get_shop(ShopID, Party)), + _ = assert_party_shop_operable(Shop, Party), + Cart = make_invoice_cart(Cost, TplDetails, Shop), + InvoiceDetails = #domain_InvoiceDetails{ + product = Product, + description = Description, + cart = Cart + }, + InvoiceCost = hg_invoice_utils:get_cart_amount(Cart), + InvoiceDue = make_invoice_due_date(Lifetime), + InvoiceContext = make_invoice_context(Context, TplContext), + InvoiceParams = #payproc_InvoiceParams{ + id = InvoiceID, + party_id = PartyID, + shop_id = ShopID, + details = InvoiceDetails, + due = InvoiceDue, + cost = InvoiceCost, + context = InvoiceContext, + external_id = ExternalID + }, + {Party, Shop, InvoiceParams}. + +validate_invoice_params(#payproc_InvoiceParams{cost = Cost}, Shop, MerchantTerms) -> + ok = validate_invoice_cost(Cost, Shop, MerchantTerms), + ok. + +validate_invoice_cost(Cost, Shop, #domain_TermSet{payments = PaymentTerms}) -> + _ = hg_invoice_utils:validate_cost(Cost, Shop), + _ = hg_invoice_utils:assert_cost_payable(Cost, PaymentTerms), + ok. + +make_invoice_cart(_, {cart, Cart}, _Shop) -> + Cart; +make_invoice_cart(Cost, {product, TplProduct}, Shop) -> + #domain_InvoiceTemplateProduct{ + product = Product, + price = TplPrice, + metadata = Metadata + } = TplProduct, + #domain_InvoiceCart{ + lines = [ + #domain_InvoiceLine{ + product = Product, + quantity = 1, + price = get_templated_price(Cost, TplPrice, Shop), + metadata = Metadata + } + ] + }. + +get_templated_price(undefined, {fixed, Cost}, Shop) -> + get_cost(Cost, Shop); +get_templated_price(undefined, _, _Shop) -> + throw(#base_InvalidRequest{errors = [?INVOICE_TPL_NO_COST]}); +get_templated_price(Cost, {fixed, Cost}, Shop) -> + get_cost(Cost, Shop); +get_templated_price(_Cost, {fixed, _CostTpl}, _Shop) -> + throw(#base_InvalidRequest{errors = [?INVOICE_TPL_BAD_COST]}); +get_templated_price(Cost, {range, Range}, Shop) -> + _ = assert_cost_in_range(Cost, Range), + get_cost(Cost, Shop); +get_templated_price(Cost, {unlim, _}, Shop) -> + get_cost(Cost, Shop). + +get_cost(Cost, Shop) -> + ok = hg_invoice_utils:validate_cost(Cost, Shop), + Cost. + +assert_cost_in_range( + #domain_Cash{amount = Amount, currency = Currency}, + #domain_CashRange{ + upper = {UType, #domain_Cash{amount = UAmount, currency = Currency}}, + lower = {LType, #domain_Cash{amount = LAmount, currency = Currency}} + } +) -> + _ = assert_less_than(LType, LAmount, Amount), + _ = assert_less_than(UType, Amount, UAmount), + ok; +assert_cost_in_range(_, _) -> + throw(#base_InvalidRequest{errors = [?INVOICE_TPL_BAD_CURRENCY]}). + +assert_less_than(inclusive, Less, More) when Less =< More -> + ok; +assert_less_than(exclusive, Less, More) when Less < More -> + ok; +assert_less_than(_, _, _) -> + throw(#base_InvalidRequest{errors = [?INVOICE_TPL_BAD_AMOUNT]}). + +make_invoice_due_date(#domain_LifetimeInterval{years = YY, months = MM, days = DD}) -> + hg_datetime:add_interval(hg_datetime:format_now(), {YY, MM, DD}). + +make_invoice_context(undefined, TplContext) -> + TplContext; +make_invoice_context(Context, _) -> + Context. diff --git a/apps/hellgate/test/hg_invoice_tests_SUITE.erl b/apps/hellgate/test/hg_invoice_tests_SUITE.erl index e35801a..0bdb7ea 100644 --- a/apps/hellgate/test/hg_invoice_tests_SUITE.erl +++ b/apps/hellgate/test/hg_invoice_tests_SUITE.erl @@ -2405,9 +2405,6 @@ payment_adjustment_success(C) -> hg_client_invoicing:get_payment_adjustment(InvoiceID, PaymentID, AdjustmentID, Client), ?payment_ev(PaymentID, ?adjustment_ev(AdjustmentID, ?adjustment_created(Adjustment))) = next_change(InvoiceID, Client), - %% no way to create another one yet - ?invalid_adjustment_pending(AdjustmentID) = - hg_client_invoicing:create_payment_adjustment(InvoiceID, PaymentID, make_adjustment_params(), Client), [ ?payment_ev(PaymentID, ?adjustment_ev(AdjustmentID, ?adjustment_status_changed(?adjustment_processed()))), ?payment_ev(PaymentID, ?adjustment_ev(AdjustmentID, ?adjustment_status_changed(?adjustment_captured(_)))) @@ -2761,9 +2758,6 @@ registered_payment_adjustment_success(C) -> hg_client_invoicing:get_payment_adjustment(InvoiceID, PaymentID, AdjustmentID, Client), ?payment_ev(PaymentID, ?adjustment_ev(AdjustmentID, ?adjustment_created(Adjustment))) = next_change(InvoiceID, Client), - %% no way to create another one yet - ?invalid_adjustment_pending(AdjustmentID) = - hg_client_invoicing:create_payment_adjustment(InvoiceID, PaymentID, make_adjustment_params(), Client), [ ?payment_ev(PaymentID, ?adjustment_ev(AdjustmentID, ?adjustment_status_changed(?adjustment_processed()))), ?payment_ev(PaymentID, ?adjustment_ev(AdjustmentID, ?adjustment_status_changed(?adjustment_captured(_))))