HG-461: Add manual refund handle (#292)

This commit is contained in:
Alexey 2019-02-28 16:30:02 +03:00 committed by GitHub
parent 79fc57713a
commit f10e9a1e51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 137 additions and 12 deletions

View File

@ -191,6 +191,11 @@ handle_function_('RefundPayment', [UserInfo, InvoiceID, PaymentID, Params], _Opt
_ = set_invoicing_meta(InvoiceID, PaymentID), _ = set_invoicing_meta(InvoiceID, PaymentID),
call(InvoiceID, {refund_payment, PaymentID, Params}); call(InvoiceID, {refund_payment, PaymentID, Params});
handle_function_('CreateManualRefund', [UserInfo, InvoiceID, PaymentID, Params], _Opts) ->
ok = assume_user_identity(UserInfo),
_ = set_invoicing_meta(InvoiceID, PaymentID),
call(InvoiceID, {manual_refund, PaymentID, Params});
handle_function_('GetPaymentRefund', [UserInfo, InvoiceID, PaymentID, ID], _Opts) -> handle_function_('GetPaymentRefund', [UserInfo, InvoiceID, PaymentID, ID], _Opts) ->
ok = assume_user_identity(UserInfo), ok = assume_user_identity(UserInfo),
_ = set_invoicing_meta(InvoiceID, PaymentID), _ = set_invoicing_meta(InvoiceID, PaymentID),
@ -599,6 +604,16 @@ handle_call({refund_payment, PaymentID, Params}, St) ->
St St
); );
handle_call({manual_refund, PaymentID, Params}, St) ->
_ = assert_invoice_accessible(St),
_ = assert_invoice_operable(St),
PaymentSession = get_payment_session(PaymentID, St),
wrap_payment_impact(
PaymentID,
hg_invoice_payment:manual_refund(Params, PaymentSession, get_payment_opts(St)),
St
);
handle_call({create_payment_adjustment, PaymentID, Params}, St) -> handle_call({create_payment_adjustment, PaymentID, Params}, St) ->
_ = assert_invoice_accessible(St), _ = assert_invoice_accessible(St),
PaymentSession = get_payment_session(PaymentID, St), PaymentSession = get_payment_session(PaymentID, St),

View File

@ -45,6 +45,8 @@
-export([cancel/2]). -export([cancel/2]).
-export([refund/3]). -export([refund/3]).
-export([manual_refund/3]).
-export([create_adjustment/4]). -export([create_adjustment/4]).
-export([capture_adjustment/3]). -export([capture_adjustment/3]).
-export([cancel_adjustment/3]). -export([cancel_adjustment/3]).
@ -921,18 +923,51 @@ refund(Params, St0, Opts) ->
St = St0#st{opts = Opts}, St = St0#st{opts = Opts},
Revision = hg_domain:head(), Revision = hg_domain:head(),
Payment = get_payment(St), Payment = get_payment(St),
Refund =
prepare_refund(Params, Payment, Revision, St, Opts),
{AccountMap, FinalCashflow} =
prepare_refund_cashflow(Refund, Payment, Revision, St, Opts),
Changes = [
?refund_created(Refund, FinalCashflow),
?session_ev(?refunded(), ?session_started())
],
try_commit_refund(Refund, Changes, AccountMap, St).
-spec manual_refund(refund_params(), st(), opts()) ->
{refund(), result()}.
manual_refund(Params, St0, Opts) ->
St = St0#st{opts = Opts},
Revision = hg_domain:head(),
Payment = get_payment(St),
Refund =
prepare_refund(Params, Payment, Revision, St, Opts),
{AccountMap, FinalCashflow} =
prepare_refund_cashflow(Refund, Payment, Revision, St, Opts),
TransactionInfo = Params#payproc_InvoicePaymentRefundParams.transaction_info,
Changes = [
?refund_created(Refund, FinalCashflow),
?session_ev(?refunded(), ?session_started())
]
++ make_transaction_event(TransactionInfo) ++
[
?session_ev(?refunded(), ?session_finished(?session_succeeded()))
],
try_commit_refund(Refund, Changes, AccountMap, St).
make_transaction_event(undefined) ->
[];
make_transaction_event(TransactionInfo) ->
[?session_ev(?refunded(), ?trx_bound(TransactionInfo))].
prepare_refund(Params, Payment, Revision, St, Opts) ->
_ = assert_payment_status(captured, Payment), _ = assert_payment_status(captured, Payment),
Route = get_route(St),
Shop = get_shop(Opts),
PartyRevision = get_opts_party_revision(Opts), PartyRevision = get_opts_party_revision(Opts),
PaymentInstitution = get_payment_institution(Opts, Revision),
Provider = get_route_provider(Route, Revision),
_ = assert_previous_refunds_finished(St), _ = assert_previous_refunds_finished(St),
VS0 = collect_validation_varset(St, Opts),
Cash = define_refund_cash(Params#payproc_InvoicePaymentRefundParams.cash, Payment), Cash = define_refund_cash(Params#payproc_InvoicePaymentRefundParams.cash, Payment),
_ = assert_refund_cash(Cash, St), _ = assert_refund_cash(Cash, St),
ID = construct_refund_id(St), ID = construct_refund_id(St),
Refund = #domain_InvoicePaymentRefund{ #domain_InvoicePaymentRefund {
id = ID, id = ID,
created_at = hg_datetime:format_now(), created_at = hg_datetime:format_now(),
domain_revision = Revision, domain_revision = Revision,
@ -940,18 +975,25 @@ refund(Params, St0, Opts) ->
status = ?refund_pending(), status = ?refund_pending(),
reason = Params#payproc_InvoicePaymentRefundParams.reason, reason = Params#payproc_InvoicePaymentRefundParams.reason,
cash = Cash cash = Cash
}, }.
prepare_refund_cashflow(Refund, Payment, Revision, St, Opts) ->
Route = get_route(St),
Shop = get_shop(Opts),
MerchantTerms = get_merchant_refunds_terms(get_merchant_payments_terms(Opts, Revision)), MerchantTerms = get_merchant_refunds_terms(get_merchant_payments_terms(Opts, Revision)),
VS0 = collect_validation_varset(St, Opts),
VS1 = validate_refund(MerchantTerms, Refund, Payment, VS0, Revision), VS1 = validate_refund(MerchantTerms, Refund, Payment, VS0, Revision),
ProviderPaymentsTerms = get_provider_payments_terms(Route, Revision), ProviderPaymentsTerms = get_provider_payments_terms(Route, Revision),
ProviderTerms = get_provider_refunds_terms(ProviderPaymentsTerms, Refund, Payment, VS1, Revision), ProviderTerms = get_provider_refunds_terms(ProviderPaymentsTerms, Refund, Payment, VS1, Revision),
Cashflow = collect_refund_cashflow(MerchantTerms, ProviderTerms, VS1, Revision), Cashflow = collect_refund_cashflow(MerchantTerms, ProviderTerms, VS1, Revision),
PaymentInstitution = get_payment_institution(Opts, Revision),
Provider = get_route_provider(Route, Revision),
AccountMap = collect_account_map(Payment, Shop, PaymentInstitution, Provider, VS1, Revision), AccountMap = collect_account_map(Payment, Shop, PaymentInstitution, Provider, VS1, Revision),
FinalCashflow = construct_final_cashflow(Cashflow, collect_cash_flow_context(Refund), AccountMap), FinalCashflow = construct_final_cashflow(Cashflow, collect_cash_flow_context(Refund), AccountMap),
Changes = [ {AccountMap, FinalCashflow}.
?refund_created(Refund, FinalCashflow),
?session_ev(?refunded(), ?session_started()) try_commit_refund(Refund, Changes, AccountMap, St) ->
], ID = Refund#domain_InvoicePaymentRefund.id,
RefundSt = collapse_refund_changes(Changes), RefundSt = collapse_refund_changes(Changes),
AffectedAccounts = prepare_refund_cashflow(RefundSt, St), AffectedAccounts = prepare_refund_cashflow(RefundSt, St),
% NOTE we assume that posting involving merchant settlement account MUST be present in the cashflow % NOTE we assume that posting involving merchant settlement account MUST be present in the cashflow

View File

@ -66,6 +66,7 @@
-export([invalid_refund_party_status/1]). -export([invalid_refund_party_status/1]).
-export([invalid_refund_shop_status/1]). -export([invalid_refund_shop_status/1]).
-export([payment_refund_success/1]). -export([payment_refund_success/1]).
-export([payment_manual_refund/1]).
-export([payment_partial_refunds_success/1]). -export([payment_partial_refunds_success/1]).
-export([payment_temporary_unavailability_retry_success/1]). -export([payment_temporary_unavailability_retry_success/1]).
-export([payment_temporary_unavailability_too_many_retries/1]). -export([payment_temporary_unavailability_too_many_retries/1]).
@ -204,6 +205,7 @@ groups() ->
invalid_refund_party_status, invalid_refund_party_status,
invalid_refund_shop_status, invalid_refund_shop_status,
retry_temporary_unavailability_refund, retry_temporary_unavailability_refund,
payment_manual_refund,
payment_refund_success, payment_refund_success,
payment_partial_refunds_success, payment_partial_refunds_success,
invalid_amount_payment_partial_refund, invalid_amount_payment_partial_refund,
@ -1447,6 +1449,53 @@ payment_refund_success(C) ->
?invalid_payment_status(?refunded()) = ?invalid_payment_status(?refunded()) =
hg_client_invoicing:refund_payment(InvoiceID, PaymentID, RefundParams, Client). hg_client_invoicing:refund_payment(InvoiceID, PaymentID, RefundParams, Client).
-spec payment_manual_refund(config()) -> _ | no_return().
payment_manual_refund(C) ->
Client = cfg(client, C),
PartyClient = cfg(party_client, C),
ShopID = hg_ct_helper:create_battle_ready_shop(?cat(2), <<"RUB">>, ?tmpl(2), ?pinst(2), PartyClient),
InvoiceID = start_invoice(ShopID, <<"rubberduck">>, make_due_date(10), 42000, C),
PaymentID = process_payment(InvoiceID, make_payment_params(), Client),
TrxInfo = ?trx_info(<<"test">>, #{}),
RefundParams = #payproc_InvoicePaymentRefundParams {
reason = <<"manual">>,
transaction_info = TrxInfo
},
PaymentID = await_payment_capture(InvoiceID, PaymentID, Client),
% not enough funds on the merchant account
?insufficient_account_balance() =
hg_client_invoicing:refund_payment_manual(InvoiceID, PaymentID, RefundParams, Client),
% top up merchant account
InvoiceID2 = start_invoice(ShopID, <<"rubberduck">>, make_due_date(10), 42000, C),
PaymentID2 = process_payment(InvoiceID2, make_payment_params(), Client),
PaymentID2 = await_payment_capture(InvoiceID2, PaymentID2, Client),
% prevent proxy access
OriginalRevision = hg_domain:head(),
Fixture = payment_manual_refund_fixture(OriginalRevision),
ok = hg_domain:upsert(Fixture),
% create refund
Refund = #domain_InvoicePaymentRefund{id = RefundID} =
hg_client_invoicing:refund_payment_manual(InvoiceID, PaymentID, RefundParams, Client),
Refund =
hg_client_invoicing:get_payment_refund(InvoiceID, PaymentID, RefundID, Client),
[
?payment_ev(PaymentID, ?refund_ev(RefundID, ?refund_created(Refund, _))),
?payment_ev(PaymentID, ?refund_ev(RefundID, ?session_ev(?refunded(), ?session_started()))),
?payment_ev(PaymentID, ?refund_ev(RefundID, ?session_ev(?refunded(), ?trx_bound(TrxInfo)))),
?payment_ev(PaymentID, ?refund_ev(RefundID, ?session_ev(?refunded(), ?session_finished(?session_succeeded()))))
] = next_event(InvoiceID, Client),
[
?payment_ev(PaymentID, ?refund_ev(RefundID, ?refund_status_changed(?refund_succeeded()))),
?payment_ev(PaymentID, ?payment_status_changed(?refunded()))
] = next_event(InvoiceID, Client),
#domain_InvoicePaymentRefund{status = ?refund_succeeded()} =
hg_client_invoicing:get_payment_refund(InvoiceID, PaymentID, RefundID, Client),
?invalid_payment_status(?refunded()) =
hg_client_invoicing:refund_payment_manual(InvoiceID, PaymentID, RefundParams, Client),
% reenable proxy
ok = hg_domain:reset(OriginalRevision).
-spec payment_partial_refunds_success(config()) -> _ | no_return(). -spec payment_partial_refunds_success(config()) -> _ | no_return().
payment_partial_refunds_success(C) -> payment_partial_refunds_success(C) ->
@ -3702,6 +3751,18 @@ payments_w_bank_conditions_fixture(_Revision) ->
hg_ct_fixture:construct_contract_template(?tmpl(4), ?trms(4)) hg_ct_fixture:construct_contract_template(?tmpl(4), ?trms(4))
]. ].
payment_manual_refund_fixture(_Revision) ->
[
{proxy, #domain_ProxyObject{
ref = ?prx(1),
data = #domain_ProxyDefinition{
name = <<"undefined">>,
description = <<"undefined">>,
url = <<"undefined">>,
options = #{}
}
}}
].
construct_term_set_for_partial_capture_service_permit() -> construct_term_set_for_partial_capture_service_permit() ->
TermSet = #domain_TermSet{ TermSet = #domain_TermSet{

View File

@ -23,6 +23,7 @@
-export([new_capture_payment/4]). -export([new_capture_payment/4]).
-export([refund_payment/4]). -export([refund_payment/4]).
-export([refund_payment_manual/4]).
-export([get_payment_refund/4]). -export([get_payment_refund/4]).
-export([create_adjustment/4]). -export([create_adjustment/4]).
@ -205,6 +206,12 @@ capture_payment(InvoiceID, PaymentID, Reason, Cash, Client) ->
refund_payment(InvoiceID, PaymentID, Params, Client) -> refund_payment(InvoiceID, PaymentID, Params, Client) ->
map_result_error(gen_server:call(Client, {call, 'RefundPayment', [InvoiceID, PaymentID, Params]})). map_result_error(gen_server:call(Client, {call, 'RefundPayment', [InvoiceID, PaymentID, Params]})).
-spec refund_payment_manual(invoice_id(), payment_id(), refund_params(), pid()) ->
refund() | woody_error:business_error().
refund_payment_manual(InvoiceID, PaymentID, Params, Client) ->
map_result_error(gen_server:call(Client, {call, 'CreateManualRefund', [InvoiceID, PaymentID, Params]})).
-spec get_payment_refund(invoice_id(), payment_id(), refund_id(), pid()) -> -spec get_payment_refund(invoice_id(), payment_id(), refund_id(), pid()) ->
refund() | woody_error:business_error(). refund() | woody_error:business_error().

View File

@ -10,7 +10,7 @@
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"1.0.2">>},2}, {<<"cowlib">>,{pkg,<<"cowlib">>,<<"1.0.2">>},2},
{<<"dmsl">>, {<<"dmsl">>,
{git,"git@github.com:rbkmoney/damsel.git", {git,"git@github.com:rbkmoney/damsel.git",
{ref,"15c8db48ccc6778c4fc7cc998724136e38b6c728"}}, {ref,"b966bd5b02dcd2968e46070a75e6c9e35c8b7f82"}},
0}, 0},
{<<"dmt_client">>, {<<"dmt_client">>,
{git,"git@github.com:rbkmoney/dmt_client.git", {git,"git@github.com:rbkmoney/dmt_client.git",