OPS-358: Split hg invoice (#90)

* refactored hg invoice

* fixed

* removed unnecessary adj creation

* removed for regular adj
This commit is contained in:
Артем 2023-09-27 11:57:42 +03:00 committed by GitHub
parent b8156b10e2
commit 8fa2d2a382
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 484 additions and 414 deletions

View File

@ -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.

View File

@ -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),

View File

@ -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).

View File

@ -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.

View File

@ -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(_))))