ED-165: split support (#554)

This commit is contained in:
dinama 2021-10-26 20:09:23 +03:00 committed by GitHub
parent bf17c53853
commit 2a8d8ade02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 694 additions and 43 deletions

View File

@ -0,0 +1,336 @@
-module(capi_allocation).
-include_lib("damsel/include/dmsl_payment_processing_thrift.hrl").
-export([validate/1]).
-export([transaction_error/1]).
-export([encode/2]).
-export([decode/1]).
-type allocation() :: dmsl_domain_thrift:'Allocation'().
-type allocation_prototype() :: dmsl_domain_thrift:'AllocationPrototype'().
-type decode_data() :: _.
-type validate_error() :: allocation_duplicate | allocation_wrong_cart.
-type invalid_transaction() :: dmsl_payment_processing_thrift:'AllocationInvalidTransaction'().
-spec validate(list() | undefined) -> ok | validate_error().
validate(undefined) ->
ok;
validate([]) ->
allocation_wrong_cart;
validate(Transactions) ->
Uniq = lists:usort([maps:get(<<"shopID">>, Target) || #{<<"target">> := Target} <- Transactions]),
case erlang:length(Uniq) =/= erlang:length(Transactions) of
true -> allocation_duplicate;
_ -> ok
end.
-spec transaction_error(invalid_transaction()) -> binary().
transaction_error(#payproc_AllocationInvalidTransaction{reason = Reason, transaction = Transaction}) ->
ShopID =
case Transaction of
{transaction, #domain_AllocationTransaction{target = {shop, Target}}} ->
Target#domain_AllocationTransactionTargetShop.shop_id;
{transaction_prototype, #domain_AllocationTransactionPrototype{target = {shop, Target}}} ->
Target#domain_AllocationTransactionTargetShop.shop_id
end,
Message = io_lib:format("Invalid allocation transaction with shop_id \"~ts\" and error \"~ts\"", [ShopID, Reason]),
genlib:to_binary(Message).
-spec encode(list() | undefined, binary()) -> allocation_prototype() | undefined.
encode(undefined, _PartyID) ->
undefined;
encode(Transactions, PartyID) ->
#domain_AllocationPrototype{
transactions = [encode_transaction(PartyID, Transaction) || Transaction <- Transactions]
}.
encode_transaction(PartyID, Transaction) ->
#domain_AllocationTransactionPrototype{
target = encode_target(PartyID, maps:get(<<"target">>, Transaction)),
body = encode_body(Transaction),
details = encode_details(Transaction)
}.
encode_target(PartyID, #{<<"allocationTargetType">> := <<"AllocationTargetShop">>} = Target) ->
{shop, #domain_AllocationTransactionTargetShop{
owner_id = PartyID,
shop_id = maps:get(<<"shopID">>, Target)
}}.
encode_details(#{<<"cart">> := _Cart} = Transaction) ->
#domain_AllocationTransactionDetails{
cart = capi_handler_encoder:encode_invoice_cart(Transaction)
};
encode_details(_) ->
undefined.
encode_body(#{<<"allocationBodyType">> := <<"AllocationBodyAmount">>} = Transaction) ->
{amount, #domain_AllocationTransactionPrototypeBodyAmount{
amount = capi_handler_encoder:encode_cash(Transaction)
}};
encode_body(#{<<"allocationBodyType">> := <<"AllocationBodyTotal">>} = Transaction) ->
Currency = maps:get(<<"currency">>, Transaction),
Total = maps:get(<<"total">>, Transaction),
Fee = maps:get(<<"fee">>, Transaction),
{total, #domain_AllocationTransactionPrototypeBodyTotal{
total = capi_handler_encoder:encode_cash(Total, Currency),
fee = encode_fee(Fee, Currency)
}}.
encode_fee(#{<<"allocationFeeType">> := <<"AllocationFeeFixed">>} = Fee, Currency) ->
Amount = maps:get(<<"amount">>, Fee),
{fixed, #domain_AllocationTransactionPrototypeFeeFixed{
amount = capi_handler_encoder:encode_cash(Amount, Currency)
}};
encode_fee(#{<<"allocationFeeType">> := <<"AllocationFeeShare">>} = Fee, _Currency) ->
Share = maps:get(<<"share">>, Fee),
{share, #domain_AllocationTransactionFeeShare{
parts = encode_parts(Share)
}}.
encode_parts(#{<<"m">> := M, <<"exp">> := Exp}) ->
case Exp < 0 of
true ->
Q = erlang:trunc(math:pow(10, -Exp)),
#'Rational'{p = M, q = Q};
_ ->
P = M * erlang:trunc(math:pow(10, Exp)),
#'Rational'{p = P, q = 1}
end.
-spec decode(allocation() | undefined) -> decode_data() | undefined.
decode(undefined) ->
undefined;
decode(#domain_Allocation{transactions = Transactions}) ->
[decode_transaction(L) || L <- Transactions].
decode_transaction(Transaction) ->
Amount = Transaction#domain_AllocationTransaction.amount,
Map0 = capi_handler_utils:merge_and_compact(
#{
<<"target">> => decode_target(Transaction#domain_AllocationTransaction.target)
},
decode_body(Transaction#domain_AllocationTransaction.body, Amount)
),
capi_handler_utils:merge_and_compact(
Map0,
decode_details(Transaction#domain_AllocationTransaction.details)
).
decode_target({shop, AllocationShop}) ->
#{
<<"allocationTargetType">> => <<"AllocationTargetShop">>,
<<"shopID">> => AllocationShop#domain_AllocationTransactionTargetShop.shop_id
}.
decode_details(undefined) ->
undefined;
decode_details(AllocationDetails) ->
#{
<<"cart">> => capi_handler_decoder_invoicing:decode_invoice_cart(
AllocationDetails#domain_AllocationTransactionDetails.cart
)
}.
decode_body(undefined, TransactionAmount) ->
#{
<<"allocationBodyType">> => <<"AllocationBodyAmount">>,
<<"amount">> => TransactionAmount#domain_Cash.amount,
<<"currency">> => capi_handler_decoder_utils:decode_currency(TransactionAmount#domain_Cash.currency)
};
decode_body(Body, TransactionAmount) ->
TotalAmount = Body#domain_AllocationTransactionBodyTotal.total,
FeeAmount = Body#domain_AllocationTransactionBodyTotal.fee_amount,
FeeTarget = Body#domain_AllocationTransactionBodyTotal.fee_target,
Fee = Body#domain_AllocationTransactionBodyTotal.fee,
#{
<<"allocationBodyType">> => <<"AllocationBodyTotal">>,
<<"currency">> => capi_handler_decoder_utils:decode_currency(TotalAmount#domain_Cash.currency),
<<"total">> => TotalAmount#domain_Cash.amount,
<<"amount">> => TransactionAmount#domain_Cash.amount,
<<"fee">> => decode_fee(Fee, FeeTarget, FeeAmount)
}.
decode_fee(undefined, FeeTarget, FeeAmount) ->
#{
<<"allocationFeeType">> => <<"AllocationFeeFixed">>,
<<"target">> => decode_target(FeeTarget),
<<"amount">> => FeeAmount#domain_Cash.amount
};
decode_fee(Fee, FeeTarget, FeeAmount) ->
#{
<<"allocationFeeType">> => <<"AllocationFeeShare">>,
<<"target">> => decode_target(FeeTarget),
<<"amount">> => FeeAmount#domain_Cash.amount,
<<"share">> => decode_parts(Fee#domain_AllocationTransactionFeeShare.parts)
}.
decode_parts(Parts) ->
#'Rational'{p = P, q = Q} = Parts,
Exponent = erlang:trunc(math:log10(Q)),
#{
<<"m">> => P,
<<"exp">> => -Exponent
}.
-ifdef(EUNIT).
-include_lib("eunit/include/eunit.hrl").
-spec test() -> _.
-spec validate_test_() -> _.
validate_test_() ->
[
?_assertEqual(allocation_wrong_cart, validate([])),
?_assertEqual(
allocation_duplicate,
validate([
#{<<"target">> => #{<<"shopID">> => <<"shop1">>}},
#{<<"target">> => #{<<"shopID">> => <<"shop2">>}},
#{<<"target">> => #{<<"shopID">> => <<"shop1">>}}
])
)
].
-spec encode_parts_test_() -> _.
encode_parts_test_() ->
[
?_assertEqual(make_rational(8, 1), encode_parts(make_decimal(8, 0))),
?_assertEqual(make_rational(10, 10), encode_parts(make_decimal(10, -1))),
?_assertEqual(make_rational(11000700, 100000000), encode_parts(make_decimal(11000700, -8)))
].
-spec encode_test() -> _.
encode_test() ->
AllocationCart = [
#{
<<"product">> => <<"info">>,
<<"quantity">> => 2,
<<"price">> => 16
}
],
Allocation = [
#{
<<"target">> => #{
<<"allocationTargetType">> => <<"AllocationTargetShop">>,
<<"shopID">> => <<"shopID1">>
},
<<"allocationBodyType">> => <<"AllocationBodyTotal">>,
<<"total">> => 32,
<<"currency">> => <<"RUB">>,
<<"fee">> => #{
<<"target">> => #{
<<"allocationTargetType">> => <<"AllocationTargetShop">>,
<<"shopID">> => <<"shopID2">>
},
<<"allocationFeeType">> => <<"AllocationFeeShare">>,
<<"amount">> => 24,
<<"share">> => make_decimal(80, -1)
},
<<"cart">> => AllocationCart
}
],
Expected = #domain_AllocationPrototype{
transactions = [
#domain_AllocationTransactionPrototype{
target =
{shop, #domain_AllocationTransactionTargetShop{
owner_id = <<"partyID">>,
shop_id = <<"shopID1">>
}},
body =
{total, #domain_AllocationTransactionPrototypeBodyTotal{
total = make_cash(32),
fee =
{share, #domain_AllocationTransactionFeeShare{
parts = make_rational(80, 10)
}}
}},
details = #domain_AllocationTransactionDetails{
cart = capi_handler_encoder:encode_invoice_cart(AllocationCart, <<"RUB">>)
}
}
]
},
Result = encode(Allocation, <<"partyID">>),
?assertEqual(Expected, Result).
-spec decode_parts_test_() -> _.
decode_parts_test_() ->
[
?_assertEqual(make_decimal(8, 0), decode_parts(make_rational(8, 1))),
?_assertEqual(make_decimal(10, -1), decode_parts(make_rational(10, 10))),
?_assertEqual(make_decimal(11000700, -8), decode_parts(make_rational(11000700, 100000000)))
].
-spec decode_test() -> _.
decode_test() ->
AllocationCart = #domain_InvoiceCart{
lines = [
#domain_InvoiceLine{
product = <<"info">>,
quantity = 2,
price = make_cash(16)
}
]
},
Allocation = #domain_Allocation{
transactions = [
#domain_AllocationTransaction{
id = <<"0">>,
target =
{shop, #domain_AllocationTransactionTargetShop{
owner_id = <<"partyID1">>,
shop_id = <<"shopID1">>
}},
amount = make_cash(32),
body = #domain_AllocationTransactionBodyTotal{
fee_target =
{shop, #domain_AllocationTransactionTargetShop{
owner_id = <<"partyID2">>,
shop_id = <<"shopID2">>
}},
total = make_cash(16),
fee_amount = make_cash(8),
fee = #domain_AllocationTransactionFeeShare{
parts = make_rational(80, 10)
}
},
details = #domain_AllocationTransactionDetails{
cart = AllocationCart
}
}
]
},
Expected = [
#{
<<"target">> => #{
<<"allocationTargetType">> => <<"AllocationTargetShop">>,
<<"shopID">> => <<"shopID1">>
},
<<"allocationBodyType">> => <<"AllocationBodyTotal">>,
<<"currency">> => <<"RUB">>,
<<"total">> => 16,
<<"amount">> => 32,
<<"fee">> => #{
<<"target">> => #{
<<"allocationTargetType">> => <<"AllocationTargetShop">>,
<<"shopID">> => <<"shopID2">>
},
<<"allocationFeeType">> => <<"AllocationFeeShare">>,
<<"amount">> => 8,
<<"share">> => decode_parts(make_rational(80, 10))
},
<<"cart">> => capi_handler_decoder_invoicing:decode_invoice_cart(AllocationCart)
}
],
Result = decode(Allocation),
?assertEqual(Expected, Result).
make_cash(Amount) -> #domain_Cash{amount = Amount, currency = #domain_CurrencyRef{symbolic_code = <<"RUB">>}}.
make_decimal(M, E) -> #{<<"m">> => M, <<"exp">> => E}.
make_rational(P, Q) -> #'Rational'{p = P, q = Q}.
-endif.

View File

@ -57,6 +57,14 @@
-define(email, 51).
-define(phone_number, 52).
-define(allocation, 53).
-define(target, 54).
-define(total, 55).
-define(fee, 56).
-define(share, 57).
-define(matisse, 58).
-define(exponent, 59).
-export([payment/0]).
-export([invoice/0]).
-export([invoice_template/0]).
@ -103,7 +111,8 @@ invoice() ->
?due_date => [<<"dueDate">>],
?cart => [<<"cart">>, {set, cart_line_schema()}],
?bank_account => [<<"bankAccount">>, bank_account_schema()],
?invoice_template_id => [<<"invoiceTemplateID">>]
?invoice_template_id => [<<"invoiceTemplateID">>],
?allocation => [<<"allocation">>, {set, allocation_transaction()}]
}.
-spec invoice_template() -> schema().
@ -134,7 +143,8 @@ refund() ->
#{
?amount => [<<"amount">>],
?currency => [<<"currency">>],
?cart => [<<"cart">>, {set, cart_line_schema()}]
?cart => [<<"cart">>, {set, cart_line_schema()}],
?allocation => [<<"allocation">>, {set, allocation_transaction()}]
}.
-spec customer() -> schema().
@ -181,6 +191,40 @@ payment_tool_schema() ->
}
}.
-spec allocation_transaction() -> schema().
allocation_transaction() ->
#{
?target => [<<"target">>, allocation_target()],
?discriminator => [<<"allocationBodyType">>],
?amount => [<<"amount">>],
?total => [<<"total">>],
?currency => [<<"currency">>],
?fee => [
<<"fee">>,
#{
?target => [<<"target">>, allocation_target()],
?discriminator => [<<"allocationFeeType">>],
?amount => [<<"amount">>],
?share => [<<"share">>, decimal()]
}
],
?cart => [<<"cart">>, {set, cart_line_schema()}]
}.
-spec allocation_target() -> schema().
allocation_target() ->
#{
?discriminator => [<<"allocationTargetType">>],
?shop_id => [<<"shopID">>]
}.
-spec decimal() -> schema().
decimal() ->
#{
?matisse => [<<"m">>],
?exponent => [<<"exp">>]
}.
-spec cart_line_schema() -> schema().
cart_line_schema() ->
#{
@ -470,7 +514,8 @@ read_invoice_features_test() ->
[1, Product],
[0, Product2]
],
?invoice_template_id => undefined
?invoice_template_id => undefined,
?allocation => undefined
},
Request = #{
<<"externalID">> => <<"externalID">>,
@ -750,6 +795,123 @@ compare_invoice_template_features_test() ->
<<"details.cart">>
]).
-spec read_allocation_transaction_test_() -> _.
read_allocation_transaction_test_() ->
Request1 = ?ALLOCATION_TRANSACTION_PARAMS,
Features1 = #{
?target => #{
?discriminator => hash(<<"AllocationTargetShop">>),
?shop_id => hash(?STRING)
},
?discriminator => hash(<<"AllocationBodyTotal">>),
?amount => undefined,
?total => hash(?INTEGER),
?currency => hash(?USD),
?fee => #{
?target => #{
?discriminator => hash(<<"AllocationTargetShop">>),
?shop_id => hash(?STRING)
},
?discriminator => hash(<<"AllocationFeeShare">>),
?amount => hash(?INTEGER),
?share => #{
?matisse => hash(?INTEGER),
?exponent => hash(?INTEGER)
}
},
?cart => [
[
0,
#{
?product => hash(?STRING),
?quantity => hash(?INTEGER),
?price => hash(?INTEGER),
?tax => undefined
}
]
]
},
Request2 = Request1#{
<<"fee">> => #{
<<"target">> => ?ALLOCATION_TARGET,
<<"allocationFeeType">> => <<"AllocationFeeFixed">>,
<<"amount">> => 1024
}
},
Features2 = Features1#{
?fee => #{
?target => #{
?discriminator => hash(<<"AllocationTargetShop">>),
?shop_id => hash(?STRING)
},
?discriminator => hash(<<"AllocationFeeFixed">>),
?amount => hash(1024),
?share => undefined
}
},
[
?_assertEqual(Features1, read(allocation_transaction(), Request1)),
?_assertEqual(Features2, read(allocation_transaction(), Request2))
].
-spec compare_allocation_transaction_test() -> _.
compare_allocation_transaction_test() ->
Request1 = ?ALLOCATION_TRANSACTION_PARAMS,
Request2 = ?ALLOCATION_TRANSACTION_PARAMS#{
<<"total">> => 1024,
<<"amount">> => 512,
<<"fee">> => #{
<<"target">> => ?ALLOCATION_TARGET,
<<"allocationFeeType">> => <<"AllocationFeeFixed">>,
<<"amount">> => ?INTEGER,
<<"share">> => undefined
}
},
Request3 = #{
<<"target">> => ?ALLOCATION_TARGET#{<<"shopID">> => <<"SomeShop">>},
<<"allocationBodyType">> => <<"AllocationBodyAmount">>,
<<"amount">> => ?INTEGER,
<<"currency">> => ?RUB,
<<"cart">> => [
#{<<"product">> => ?STRING, <<"quantity">> => 1, <<"price">> => ?INTEGER}
]
},
Request4 = Request1#{
<<"fee">> => deep_merge(maps:get(<<"fee">>, Request1), #{
<<"amount">> => 1024,
<<"share">> => #{<<"m">> => 1024, <<"exp">> => 1024}
})
},
common_compare_tests(allocation_transaction(), Request1, Request2, [
<<"amount">>, <<"total">>, <<"fee">>
]),
common_compare_tests(allocation_transaction(), Request1, Request3, [
<<"target.shopID">>, <<"allocationBodyType">>, <<"currency">>, <<"amount">>, <<"cart.0.quantity">>
]),
common_compare_tests(allocation_transaction(), Request1, Request4, [
<<"fee.amount">>, <<"fee.share.m">>, <<"fee.share.exp">>
]).
-spec demo_compare_allocation_transaction_test() -> _.
demo_compare_allocation_transaction_test() ->
Request1 = ?ALLOCATION_TRANSACTION_PARAMS,
Request2 = #{
<<"allocationBodyType">> => <<"AllocationBodyAmount">>
},
Request3 = #{
<<"fee">> => deep_merge(maps:get(<<"fee">>, Request1), #{
<<"allocationFeeType">> => <<"AllocationFeeFixed">>
})
},
common_compare_tests(allocation_transaction(), Request1, Request2, [
<<"allocationBodyType">>
]),
common_compare_tests(allocation_transaction(), Request1, Request3, [
<<"fee">>
]).
%%
payment_resource(Session, Tool) ->
#{
<<"paymentResource">> => #{

View File

@ -159,7 +159,8 @@ decode_invoice_payment(InvoiceID, InvoicePayment = #payproc_InvoicePayment{payme
capi_handler_utils:merge_and_compact(
decode_payment(InvoiceID, Payment, Context),
#{
<<"transactionInfo">> => decode_last_tx_info(InvoicePayment#payproc_InvoicePayment.last_transaction_info)
<<"transactionInfo">> => decode_last_tx_info(InvoicePayment#payproc_InvoicePayment.last_transaction_info),
<<"allocation">> => capi_allocation:decode(InvoicePayment#payproc_InvoicePayment.allocation)
}
).
@ -327,7 +328,8 @@ decode_refund(Refund, Context) ->
<<"reason">> => Refund#domain_InvoicePaymentRefund.reason,
<<"amount">> => Amount,
<<"currency">> => capi_handler_decoder_utils:decode_currency(Currency),
<<"externalID">> => Refund#domain_InvoicePaymentRefund.external_id
<<"externalID">> => Refund#domain_InvoicePaymentRefund.external_id,
<<"allocation">> => capi_allocation:decode(Refund#domain_InvoicePaymentRefund.allocation)
},
decode_refund_status(Refund#domain_InvoicePaymentRefund.status, Context)
).
@ -403,7 +405,8 @@ decode_invoice(Invoice) ->
<<"description">> => Details#domain_InvoiceDetails.description,
<<"cart">> => decode_invoice_cart(Details#domain_InvoiceDetails.cart),
<<"bankAccount">> => decode_invoice_bank_account(Details#domain_InvoiceDetails.bank_account),
<<"invoiceTemplateID">> => Invoice#domain_Invoice.template_id
<<"invoiceTemplateID">> => Invoice#domain_Invoice.template_id,
<<"allocation">> => capi_allocation:decode(Invoice#domain_Invoice.allocation)
},
decode_invoice_status(Invoice#domain_Invoice.status)
).
@ -421,7 +424,7 @@ decode_invoice_status({Status, StatusInfo}) ->
}.
-spec decode_invoice_cart(capi_handler_encoder:encode_data() | undefined) ->
decode_data() | undefined.
[capi_handler_decoder_utils:decode_data()] | undefined.
decode_invoice_cart(#domain_InvoiceCart{lines = Lines}) ->
[decode_invoice_line(L) || L <- Lines];
decode_invoice_cart(undefined) ->

View File

@ -1,5 +1,6 @@
-module(capi_handler_decoder_utils).
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
-include_lib("damsel/include/dmsl_payment_processing_thrift.hrl").
-include_lib("damsel/include/dmsl_merch_stat_thrift.hrl").
@ -18,12 +19,13 @@
-export_type([decode_data/0]).
-type decode_data() :: #{binary() => term()}.
-type encoded_currency() :: dmsl_domain_thrift:'Currency'() | dmsl_domain_thrift:'CurrencyRef'().
-spec decode_map(map(), fun((_) -> any())) -> [any()].
decode_map(Items, Fun) ->
lists:map(Fun, maps:values(Items)).
-spec decode_currency(capi_handler_encoder:encode_data()) -> binary().
-spec decode_currency(encoded_currency()) -> binary().
decode_currency(#domain_Currency{symbolic_code = SymbolicCode}) -> SymbolicCode;
decode_currency(#domain_CurrencyRef{symbolic_code = SymbolicCode}) -> SymbolicCode.

View File

@ -10,6 +10,7 @@
-export([encode_cash/2]).
-export([encode_currency/1]).
-export([encode_invoice_cart/1]).
-export([encode_invoice_cart/2]).
-export([encode_invoice_bank_account/1]).
-export([encode_stat_request/1]).
-export([encode_invoice_context/1]).
@ -169,6 +170,7 @@ encode_invoice_cart(Params) ->
Currency = genlib_map:get(<<"currency">>, Params),
encode_invoice_cart(Cart, Currency).
-spec encode_invoice_cart(list(), binary()) -> encode_data().
encode_invoice_cart(Cart, Currency) when Cart =/= undefined, Cart =/= [] ->
#domain_InvoiceCart{
lines = [encode_invoice_line(Line, Currency) || Line <- Cart]

View File

@ -29,6 +29,8 @@ prepare('CreateInvoice' = OperationID, Req, Context) ->
end,
Process = fun() ->
try
Allocation = maps:get(<<"allocation">>, InvoiceParams, undefined),
ok = validate_allocation(Allocation),
case create_invoice(PartyID, InvoiceParams, Context, OperationID) of
{ok, #'payproc_Invoice'{invoice = Invoice}} ->
{ok, {201, #{}, capi_handler_decoder_invoicing:make_invoice_and_token(Invoice, Context)}};
@ -46,16 +48,27 @@ prepare('CreateInvoice' = OperationID, Req, Context) ->
#payproc_InvalidShopStatus{} ->
{ok, logic_error(invalidShopStatus, <<"Invalid shop status">>)};
#payproc_InvoiceTermsViolated{} ->
{ok, logic_error(invoiceTermsViolated, <<"Invoice parameters violate contract terms">>)}
{ok, logic_error(invoiceTermsViolated, <<"Invoice parameters violate contract terms">>)};
#payproc_AllocationNotAllowed{} ->
{ok, logic_error(allocationNotPermitted, <<"Not allowed">>)};
#payproc_AllocationExceededPaymentAmount{} ->
{ok, logic_error(invalidAllocation, <<"Exceeded payment amount">>)};
#payproc_AllocationInvalidTransaction{} = InvalidTransaction ->
Message = capi_allocation:transaction_error(InvalidTransaction),
{ok, logic_error(invalidAllocation, Message)}
end
end
catch
invoice_cart_empty ->
throw:invoice_cart_empty ->
{ok, logic_error(invalidInvoiceCart, <<"Wrong size. Path to item: cart">>)};
invalid_invoice_cost ->
throw:invalid_invoice_cost ->
{ok, logic_error(invalidInvoiceCost, <<"Invalid invoice amount">>)};
{external_id_conflict, InvoiceID, ExternalID, _Schema} ->
{ok, logic_error(externalIDConflict, {InvoiceID, ExternalID})}
throw:{external_id_conflict, InvoiceID, ExternalID, _Schema} ->
{ok, logic_error(externalIDConflict, {InvoiceID, ExternalID})};
throw:allocation_wrong_cart ->
{ok, logic_error(invalidAllocation, <<"Wrong cart">>)};
throw:allocation_duplicate ->
{ok, logic_error(invalidAllocation, <<"Duplicate shop">>)}
end
end,
{ok, #{authorize => Authorize, process => Process}};
@ -267,6 +280,12 @@ prepare(_OperationID, _Req, _Context) ->
%%
validate_allocation(Allocation) ->
case capi_allocation:validate(Allocation) of
ok -> ok;
Error -> throw(Error)
end.
create_invoice(PartyID, InvoiceParams, Context, BenderPrefix) ->
#{woody_context := WoodyCtx} = Context,
ExternalID = maps:get(<<"externalID">>, InvoiceParams, undefined),
@ -281,6 +300,7 @@ encode_invoice_params(ID, PartyID, InvoiceParams) ->
Currency = genlib_map:get(<<"currency">>, InvoiceParams),
Cart = genlib_map:get(<<"cart">>, InvoiceParams),
ClientInfo = genlib_map:get(<<"clientInfo">>, InvoiceParams),
Allocation = genlib_map:get(<<"allocation">>, InvoiceParams),
#payproc_InvoiceParams{
id = ID,
party_id = PartyID,
@ -290,7 +310,8 @@ encode_invoice_params(ID, PartyID, InvoiceParams) ->
context = capi_handler_encoder:encode_invoice_context(InvoiceParams),
shop_id = genlib_map:get(<<"shopID">>, InvoiceParams),
external_id = genlib_map:get(<<"externalID">>, InvoiceParams, undefined),
client_info = encode_client_info(ClientInfo)
client_info = encode_client_info(ClientInfo),
allocation = capi_allocation:encode(Allocation, PartyID)
}.
encode_client_info(undefined) ->

View File

@ -154,6 +154,7 @@ prepare(OperationID = 'GetPaymentByExternalID', Req, Context) ->
prepare(OperationID = 'CapturePayment', Req, Context) ->
InvoiceID = maps:get(invoiceID, Req),
PaymentID = maps:get(paymentID, Req),
PartyID = capi_handler_utils:get_party_id(Context),
Invoice = get_invoice_by_id(InvoiceID, Context),
Authorize = fun() ->
Prototypes = [
@ -165,10 +166,13 @@ prepare(OperationID = 'CapturePayment', Req, Context) ->
Process = fun() ->
Params = maps:get('CaptureParams', Req),
try
Allocation = maps:get(<<"allocation">>, Params, undefined),
ok = validate_allocation(Allocation),
CaptureParams = #payproc_InvoicePaymentCaptureParams{
reason = maps:get(<<"reason">>, Params),
cash = encode_optional_cash(Params, InvoiceID, PaymentID, Context),
cart = capi_handler_encoder:encode_invoice_cart(Params)
cart = capi_handler_encoder:encode_invoice_cart(Params),
allocation = capi_allocation:encode(Allocation, PartyID)
},
CallArgs = {InvoiceID, PaymentID, CaptureParams},
Call = {invoicing, 'CapturePayment', CallArgs},
@ -210,11 +214,22 @@ prepare(OperationID = 'CapturePayment', Req, Context) ->
logic_error(
amountExceededCaptureBalance,
io_lib:format("Max amount: ~p", [PaymentAmount])
)}
)};
#payproc_AllocationNotAllowed{} ->
{ok, logic_error(allocationNotPermitted, <<"Not allowed">>)};
#payproc_AllocationExceededPaymentAmount{} ->
{ok, logic_error(invalidAllocation, <<"Exceeded payment amount">>)};
#payproc_AllocationInvalidTransaction{} = InvalidTransaction ->
Message = capi_allocation:transaction_error(InvalidTransaction),
{ok, logic_error(invalidAllocation, Message)}
end
catch
throw:invoice_cart_empty ->
{ok, logic_error(invalidInvoiceCart, <<"Wrong size. Path to item: cart">>)}
{ok, logic_error(invalidInvoiceCart, <<"Wrong size. Path to item: cart">>)};
throw:allocation_wrong_cart ->
{ok, logic_error(invalidAllocation, <<"Wrong cart">>)};
throw:allocation_duplicate ->
{ok, logic_error(invalidAllocation, <<"Duplicate shop">>)}
end
end,
{ok, #{authorize => Authorize, process => Process}};
@ -263,7 +278,7 @@ prepare(OperationID = 'CancelPayment', Req, Context) ->
end,
{ok, #{authorize => Authorize, process => Process}};
prepare(OperationID = 'CreateRefund', Req, Context) ->
InvoiceID = maps:get('invoiceID', Req),
InvoiceID = maps:get(invoiceID, Req),
PaymentID = maps:get(paymentID, Req),
RefundParams = maps:get('RefundParams', Req),
Authorize = fun() ->
@ -274,7 +289,10 @@ prepare(OperationID = 'CreateRefund', Req, Context) ->
{ok, capi_auth:authorize_operation(Prototypes, Context)}
end,
Process = fun() ->
try create_refund(InvoiceID, PaymentID, RefundParams, Context, OperationID) of
try
ok = validate_refund(RefundParams),
create_refund(InvoiceID, PaymentID, RefundParams, Context, OperationID)
of
{ok, Refund} ->
{ok, {201, #{}, capi_handler_decoder_invoicing:decode_refund(Refund, Context)}};
{exception, Exception} ->
@ -327,13 +345,28 @@ prepare(OperationID = 'CreateRefund', Req, Context) ->
{ok, ErrorResp};
#'InvalidRequest'{errors = Errors} ->
FormattedErrors = capi_handler_utils:format_request_errors(Errors),
{ok, logic_error(invalidRequest, FormattedErrors)}
{ok, logic_error(invalidRequest, FormattedErrors)};
#payproc_AllocationNotAllowed{} ->
{ok, logic_error(allocationNotPermitted, <<"Not allowed">>)};
#payproc_AllocationExceededPaymentAmount{} ->
{ok, logic_error(invalidAllocation, <<"Exceeded payment amount">>)};
#payproc_AllocationInvalidTransaction{} = InvalidTransaction ->
Message = capi_allocation:transaction_error(InvalidTransaction),
{ok, logic_error(invalidAllocation, Message)};
#payproc_AllocationNotFound{} ->
{ok, logic_error(invalidAllocation, <<"Not found">>)}
end
catch
throw:invoice_cart_empty ->
{ok, logic_error(invalidInvoiceCart, <<"Wrong size. Path to item: cart">>)};
throw:{external_id_conflict, RefundID, ExternalID, _Schema} ->
{ok, logic_error(externalIDConflict, {RefundID, ExternalID})}
{ok, logic_error(externalIDConflict, {RefundID, ExternalID})};
throw:allocation_duplicate ->
{ok, logic_error(invalidAllocation, <<"Duplicate shop">>)};
throw:allocation_wrong_cart ->
{ok, logic_error(invalidAllocation, <<"Wrong cart">>)};
throw:refund_cart_conflict ->
{ok, logic_error(refundCartConflict, <<"Inconsistent Refund Cart">>)}
end
end,
{ok, #{authorize => Authorize, process => Process}};
@ -477,6 +510,22 @@ prepare(_OperationID, _Req, _Context) ->
%%
validate_allocation(Allocation) ->
case capi_allocation:validate(Allocation) of
ok -> ok;
Error -> throw(Error)
end.
validate_refund(Params) ->
Allocation = maps:get(<<"allocation">>, Params, undefined),
ok = validate_allocation(Allocation),
RefundCart = maps:get(<<"cart">>, Params, undefined),
case {RefundCart, Allocation} of
{undefined, _} -> ok;
{_, undefined} -> ok;
_ -> throw(refund_cart_conflict)
end.
create_payment(Invoice, PaymentParams, Context, OperationID, PaymentTool) ->
InvoiceID = Invoice#domain_Invoice.id,
PaymentID = create_payment_id(Invoice, PaymentParams, Context, OperationID, PaymentTool),
@ -734,7 +783,6 @@ default_processing_deadline() ->
create_refund(InvoiceID, PaymentID, RefundParams, Context, BenderPrefix) ->
PartyID = capi_handler_utils:get_party_id(Context),
RefundParamsFull = RefundParams#{<<"invoiceID">> => InvoiceID, <<"paymentID">> => PaymentID},
ExternalID = maps:get(<<"externalID">>, RefundParams, undefined),
IdempotentKey = {BenderPrefix, PartyID, ExternalID},
Identity = {schema, capi_feature_schemas:refund(), RefundParamsFull, RefundParams},
@ -748,11 +796,14 @@ create_refund(InvoiceID, PaymentID, RefundParams, Context, BenderPrefix) ->
refund_payment(RefundID, InvoiceID, PaymentID, RefundParams, Context) ->
ExternalID = maps:get(<<"externalID">>, RefundParams, undefined),
Allocation = maps:get(<<"allocation">>, RefundParams, undefined),
PartyID = capi_handler_utils:get_party_id(Context),
Params = #payproc_InvoicePaymentRefundParams{
external_id = ExternalID,
reason = genlib_map:get(<<"reason">>, RefundParams),
cash = encode_optional_cash(RefundParams, InvoiceID, PaymentID, Context),
cart = capi_handler_encoder:encode_invoice_cart(RefundParams)
cart = capi_handler_encoder:encode_invoice_cart(RefundParams),
allocation = capi_allocation:encode(Allocation, PartyID)
},
CallArgs = {
InvoiceID,

View File

@ -43,9 +43,7 @@ prepare(OperationID, Req, Context) when OperationID =:= 'SearchRefunds' ->
Process = fun() ->
Query = make_query(Context, Req),
Opts = #{
%% TODO no special fun for refunds so we can use any
%% should be fixed in new magista
thrift_fun => 'GetPayments',
thrift_fun => 'GetRefunds',
decode_fun => fun decode_stat_refund/2
},
process_search_request(refunds, Query, Req, Context, Opts)
@ -161,7 +159,8 @@ decode_stat_invoice(Invoice, _Context) ->
<<"metadata">> => capi_handler_decoder_utils:decode_context(Invoice#merchstat_StatInvoice.context),
<<"product">> => Invoice#merchstat_StatInvoice.product,
<<"description">> => Invoice#merchstat_StatInvoice.description,
<<"cart">> => capi_handler_decoder_invoicing:decode_invoice_cart(Invoice#merchstat_StatInvoice.cart)
<<"cart">> => capi_handler_decoder_invoicing:decode_invoice_cart(Invoice#merchstat_StatInvoice.cart),
<<"allocation">> => capi_allocation:decode(Invoice#merchstat_StatInvoice.allocation)
},
decode_stat_invoice_status(Invoice#merchstat_StatInvoice.status)
).
@ -187,18 +186,19 @@ decode_stat_payment(Stat, Context) ->
<<"shopID">> => Stat#merchstat_StatPayment.shop_id,
<<"createdAt">> => Stat#merchstat_StatPayment.created_at,
<<"amount">> => Stat#merchstat_StatPayment.amount,
<<"flow">> => decode_stat_payment_flow(Stat#merchstat_StatPayment.flow),
<<"fee">> => Stat#merchstat_StatPayment.fee,
<<"currency">> => Stat#merchstat_StatPayment.currency_symbolic_code,
<<"payer">> => decode_stat_payer(Stat#merchstat_StatPayment.payer),
<<"flow">> => decode_stat_payment_flow(Stat#merchstat_StatPayment.flow),
<<"geoLocationInfo">> => decode_geo_location_info(Stat#merchstat_StatPayment.location_info),
<<"metadata">> => capi_handler_decoder_utils:decode_context(Stat#merchstat_StatPayment.context),
<<"transactionInfo">> => decode_stat_tx_info(Stat#merchstat_StatPayment.additional_transaction_info),
<<"statusChangedAt">> => decode_status_changed_at(Stat#merchstat_StatPayment.status),
<<"makeRecurrent">> => capi_handler_decoder_invoicing:decode_make_recurrent(
Stat#merchstat_StatPayment.make_recurrent
),
<<"statusChangedAt">> => decode_status_changed_at(Stat#merchstat_StatPayment.status),
<<"cart">> => capi_handler_decoder_invoicing:decode_invoice_cart(Stat#merchstat_StatPayment.cart)
<<"cart">> => capi_handler_decoder_invoicing:decode_invoice_cart(Stat#merchstat_StatPayment.cart),
<<"allocation">> => capi_allocation:decode(Stat#merchstat_StatPayment.allocation)
},
decode_stat_payment_status(Stat#merchstat_StatPayment.status, Context)
).
@ -472,13 +472,15 @@ decode_stat_refund(Refund, Context) ->
<<"invoiceID">> => Refund#merchstat_StatRefund.invoice_id,
<<"paymentID">> => Refund#merchstat_StatRefund.payment_id,
<<"id">> => Refund#merchstat_StatRefund.id,
<<"externalID">> => Refund#merchstat_StatRefund.external_id,
<<"createdAt">> => Refund#merchstat_StatRefund.created_at,
<<"amount">> => Refund#merchstat_StatRefund.amount,
<<"currency">> => Refund#merchstat_StatRefund.currency_symbolic_code,
<<"reason">> => Refund#merchstat_StatRefund.reason,
<<"cart">> => capi_handler_decoder_invoicing:decode_invoice_cart(
Refund#merchstat_StatRefund.cart
)
),
<<"allocation">> => capi_allocation:decode(Refund#merchstat_StatRefund.allocation)
},
decode_stat_refund_status(Refund#merchstat_StatRefund.status, Context)
).

View File

@ -2017,7 +2017,7 @@ search_refunds_ok_test(Config) ->
[
{invoicing, fun('Get', _) -> {ok, ?PAYPROC_INVOICE} end},
{customer_management, fun('Get', _) -> {ok, ?CUSTOMER} end},
{merchant_stat, fun('GetPayments', _) -> {ok, ?STAT_RESPONSE_REFUNDS} end}
{merchant_stat, fun('GetRefunds', _) -> {ok, ?STAT_RESPONSE_REFUNDS} end}
],
Config
),

View File

@ -17,6 +17,8 @@
-define(TEST_RULESET_ID, <<"test/api">>).
-define(API_TOKEN, <<"letmein">>).
-define(RATIONAL, #'Rational'{p = ?INTEGER, q = ?INTEGER}).
-define(DETAILS, #domain_InvoiceDetails{
product = ?STRING,
description = ?STRING,
@ -75,12 +77,14 @@
external_id = EID
}).
-define(INVOICE_CART_TAXMODE, #{
<<"type">> => <<"InvoiceLineTaxVAT">>,
<<"rate">> => <<"10%">>
}).
-define(INVOICE_CART, [
#{
<<"taxMode">> => #{
<<"type">> => <<"InvoiceLineTaxVAT">>,
<<"rate">> => <<"10%">>
},
<<"taxMode">> => ?INVOICE_CART_TAXMODE,
<<"product">> => ?STRING,
<<"price">> => ?INTEGER,
<<"quantity">> => ?INTEGER
@ -118,6 +122,46 @@
metadata = #{?STRING := {obj, #{}}}
}).
-define(ALLOCATION_CART, #domain_InvoiceCart{
lines = [
#domain_InvoiceLine{
product = ?STRING,
quantity = ?INTEGER,
price = ?CASH,
metadata = #{<<"TaxMode">> => {str, <<"10%">>}}
}
]
}).
-define(ALLOCATION, #domain_Allocation{
transactions = [
#domain_AllocationTransaction{
id = ?STRING,
target =
{shop, #domain_AllocationTransactionTargetShop{
owner_id = ?STRING,
shop_id = ?STRING
}},
amount = ?CASH,
body = #domain_AllocationTransactionBodyTotal{
fee_target =
{shop, #domain_AllocationTransactionTargetShop{
owner_id = ?STRING,
shop_id = ?STRING
}},
total = ?CASH,
fee_amount = ?CASH,
fee = #domain_AllocationTransactionFeeShare{
parts = ?RATIONAL
}
},
details = #domain_AllocationTransactionDetails{
cart = ?ALLOCATION_CART
}
}
]
}).
-define(THRIFT_INVOICE_CART, #domain_InvoiceCart{
lines = [
#domain_InvoiceLine{
@ -285,7 +329,8 @@
sessions = [],
legacy_refunds = Refunds,
adjustments = Adjustments,
last_transaction_info = ?TX_INFO
last_transaction_info = ?TX_INFO,
allocation = ?ALLOCATION
}).
-define(PAYPROC_PAYMENT, ?PAYPROC_PAYMENT(?PAYMENT, [?REFUND], [?ADJUSTMENT], [?PAYPROC_CHARGEBACK])).
@ -837,7 +882,9 @@
due = ?TIMESTAMP,
amount = ?INTEGER,
currency_symbolic_code = ?RUB,
context = ?CONTENT
context = ?CONTENT,
external_id = ?STRING,
allocation = ?ALLOCATION
}).
-define(STAT_PAYMENT(Payer, Status), #merchstat_StatPayment{
@ -848,13 +895,15 @@
created_at = ?TIMESTAMP,
status = Status,
amount = ?INTEGER,
flow = {instant, #merchstat_InvoicePaymentFlowInstant{}},
fee = ?INTEGER,
currency_symbolic_code = ?RUB,
payer = Payer,
context = ?CONTENT,
flow = {instant, #merchstat_InvoicePaymentFlowInstant{}},
domain_revision = ?INTEGER,
additional_transaction_info = ?ADDITIONAL_TX_INFO
additional_transaction_info = ?ADDITIONAL_TX_INFO,
external_id = ?STRING,
allocation = ?ALLOCATION
}).
-define(TX_INFO, #domain_TransactionInfo{
@ -927,7 +976,9 @@
created_at = ?TIMESTAMP,
amount = ?INTEGER,
fee = ?INTEGER,
currency_symbolic_code = ?RUB
currency_symbolic_code = ?RUB,
external_id = ?STRING,
allocation = ?ALLOCATION
}).
-define(STAT_PAYOUT(Type), #merchstat_StatPayout{
@ -1359,3 +1410,24 @@
<<"metadata">> => ?JSON,
<<"processingDeadline">> => <<"5m">>
}).
-define(ALLOCATION_TARGET, #{
<<"allocationTargetType">> => <<"AllocationTargetShop">>,
<<"shopID">> => ?STRING
}).
-define(ALLOCATION_TRANSACTION_PARAMS, #{
<<"target">> => ?ALLOCATION_TARGET,
<<"allocationBodyType">> => <<"AllocationBodyTotal">>,
<<"total">> => ?INTEGER,
<<"currency">> => ?USD,
<<"fee">> => #{
<<"target">> => ?ALLOCATION_TARGET,
<<"allocationFeeType">> => <<"AllocationFeeShare">>,
<<"amount">> => ?INTEGER,
<<"share">> => #{<<"m">> => ?INTEGER, <<"exp">> => ?INTEGER}
},
<<"cart">> => [
#{<<"product">> => ?STRING, <<"quantity">> => ?INTEGER, <<"price">> => ?INTEGER}
]
}).

View File

@ -33,7 +33,7 @@
{woody, {git, "https://github.com/rbkmoney/woody_erlang.git", {branch, "master"}}},
{woody_user_identity, {git, "https://github.com/rbkmoney/woody_erlang_user_identity.git", {branch, "master"}}},
{woody_api_hay, {git, "https://github.com/rbkmoney/woody_api_hay.git", {branch, "master"}}},
{damsel, {git, "https://github.com/rbkmoney/damsel.git", {branch, "release/erlang/master"}}},
{damsel, {git, "https://github.com/rbkmoney/damsel.git", {branch, "master"}}},
{bender_proto, {git, "https://github.com/rbkmoney/bender-proto.git", {branch, "master"}}},
{bender_client, {git, "https://github.com/rbkmoney/bender_client_erlang.git", {branch, "master"}}},
{reporter_proto, {git, "https://github.com/rbkmoney/reporter-proto.git", {branch, "master"}}},

View File

@ -39,7 +39,7 @@
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.11.0">>},1},
{<<"damsel">>,
{git,"https://github.com/rbkmoney/damsel.git",
{ref,"1c0b21608844cc6203f211ee4dce89c1d1041029"}},
{ref,"cbd4efcac03bdae3fea77d3f69629cf6cfae0999"}},
0},
{<<"dmt_client">>,
{git,"https://github.com/rbkmoney/dmt_client.git",