MSPF-456: Processing deadline (#352)

* Added payment deadlines

* Actualized code to match damsel, added deadline check

* Rewrote deadline check, added env deadline support

* Removed deadline creation

* Moved and renamed processing deadline validation

* Removed extra clause

* Codestyle fixes

* Handle payment_deadline_reached failure

* Added processing deadline test

* Added tests for different flows

* Uncommented tests

* Removed unnecessary sleeps from tests

* Check target type in validate_processing_deadline

* Stop session processing after deadline validation

* Fix test

* Review fixes

* Fixed a bunch of tests
This commit is contained in:
Toporkov Igor 2019-09-16 17:49:20 +03:00 committed by GitHub
parent 83925e1856
commit 137ceb9e66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 171 additions and 29 deletions

View File

@ -283,9 +283,10 @@ init_(PaymentID, Params, Opts) ->
MerchantTerms = get_merchant_terms(Opts, Revision),
VS1 = collect_validation_varset(Party, Shop, VS0),
Context = get_context_params(Params),
Deadline = get_processing_deadline(Params),
Payment = construct_payment(
PaymentID, CreatedAt, Cost, Payer, Flow, MerchantTerms, Party, Shop,
VS1, Revision, MakeRecurrent, Context, ExternalID
VS1, Revision, MakeRecurrent, Context, ExternalID, Deadline
),
Events = [?payment_started(Payment)],
{collapse_changes(Events, undefined), {Events, hg_machine_action:instant()}}.
@ -333,6 +334,9 @@ get_context_params(#payproc_InvoicePaymentParams{context = Context}) ->
get_external_id(#payproc_InvoicePaymentParams{external_id = ExternalID}) ->
ExternalID.
get_processing_deadline(#payproc_InvoicePaymentParams{processing_deadline = Deadline}) ->
Deadline.
construct_payer({payment_resource, #payproc_PaymentResourcePayerParams{
resource = Resource,
contact_info = ContactInfo
@ -401,7 +405,8 @@ construct_payment(
Revision,
MakeRecurrent,
Context,
ExternalID
ExternalID,
Deadline
) ->
#domain_TermSet{payments = PaymentTerms, recurrent_paytools = RecurrentTerms} = Terms,
PaymentTool = get_payer_payment_tool(Payer),
@ -437,19 +442,20 @@ construct_payment(
},
ok = validate_recurrent_intention(RecurrentValidationVarset, MakeRecurrent),
#domain_InvoicePayment{
id = PaymentID,
created_at = CreatedAt,
owner_id = Party#domain_Party.id,
shop_id = Shop#domain_Shop.id,
domain_revision = Revision,
party_revision = Party#domain_Party.revision,
status = ?pending(),
cost = Cost,
payer = Payer,
flow = Flow,
make_recurrent = MakeRecurrent,
context = Context,
external_id = ExternalID
id = PaymentID,
created_at = CreatedAt,
owner_id = Party#domain_Party.id,
shop_id = Shop#domain_Shop.id,
domain_revision = Revision,
party_revision = Party#domain_Party.revision,
status = ?pending(),
cost = Cost,
payer = Payer,
flow = Flow,
make_recurrent = MakeRecurrent,
context = Context,
external_id = ExternalID,
processing_deadline = Deadline
}.
construct_payment_flow({instant, _}, _CreatedAt, _Terms, _VS, _Revision) ->
@ -963,6 +969,19 @@ assert_capture_cost_currency(?cash(_, PassedSymCode), #domain_InvoicePayment{cos
passed_currency = PassedSymCode
}).
validate_processing_deadline(#domain_InvoicePayment{processing_deadline = Deadline}, _TargetType = processed) ->
case hg_invoice_utils:check_deadline(Deadline) of
ok ->
ok;
{error, deadline_reached} ->
{failure, payproc_errors:construct('PaymentFailure',
{authorization_failed, {processing_deadline_reached, #payprocerr_GeneralFailure{}}}
)}
end;
validate_processing_deadline(_, _TargetType) ->
ok.
assert_capture_cart(_Cost, undefined) ->
ok;
assert_capture_cart(Cost, Cart) ->
@ -1573,10 +1592,15 @@ process_session(Action, St) ->
process_session(Session, Action, St).
process_session(undefined, Action, St0) ->
Events = start_session(get_target(St0)),
St1 = collapse_changes(Events, St0),
Session = get_activity_session(St1),
process_session(Session, Action, Events, St1);
case validate_processing_deadline(get_payment(St0), get_target_type(get_target(St0))) of
ok ->
Events = start_session(get_target(St0)),
St1 = collapse_changes(Events, St0),
Result = {start_session(get_target(St0)), hg_machine_action:set_timeout(0, Action)},
finish_session_processing(Result, St1);
Failure ->
process_failure(get_activity(St0), [], Action, Failure, St0)
end;
process_session(Session, Action, St) ->
process_session(Session, Action, [], St).
@ -2025,7 +2049,8 @@ construct_proxy_payment(
created_at = CreatedAt,
payer = Payer,
cost = Cost,
make_recurrent = MakeRecurrent
make_recurrent = MakeRecurrent,
processing_deadline = Deadline
},
Trx
) ->
@ -2037,7 +2062,8 @@ construct_proxy_payment(
payment_resource = construct_payment_resource(Payer),
cost = construct_proxy_cash(Cost),
contact_info = ContactInfo,
make_recurrent = MakeRecurrent
make_recurrent = MakeRecurrent,
processing_deadline = Deadline
}.
construct_payment_resource(?payment_resource_payer(Resource, _)) ->

View File

@ -14,6 +14,7 @@
-export([assert_shop_operable/1]).
-export([compute_shop_terms/4]).
-export([get_cart_amount/1]).
-export([check_deadline/1]).
-type amount() :: dmsl_domain_thrift:'Amount'().
-type currency() :: dmsl_domain_thrift:'CurrencyRef'().
@ -132,3 +133,14 @@ get_line_amount(#domain_InvoiceLine{
}) ->
#domain_Cash{amount = Amount * Quantity, currency = Currency}.
-spec check_deadline(Deadline :: binary() | undefined) ->
ok | {error, deadline_reached}.
check_deadline(undefined) ->
ok;
check_deadline(Deadline) ->
case hg_datetime:compare(Deadline, hg_datetime:format_now()) of
later ->
ok;
_ ->
{error, deadline_reached}
end.

View File

@ -36,6 +36,7 @@
-export([payment_start_idempotency/1]).
-export([payment_success/1]).
-export([processing_deadline_reached_test/1]).
-export([payment_success_empty_cvv/1]).
-export([payment_success_additional_info/1]).
-export([payment_w_terminal_success/1]).
@ -64,6 +65,7 @@
-export([payment_hold_cancellation/1]).
-export([payment_hold_auto_cancellation/1]).
-export([payment_hold_capturing/1]).
-export([deadline_doesnt_affect_payment_capturing/1]).
-export([payment_hold_partial_capturing/1]).
-export([payment_hold_partial_capturing_with_cart/1]).
-export([payment_hold_partial_capturing_with_cart_missing_cash/1]).
@ -76,6 +78,7 @@
-export([invalid_refund_shop_status/1]).
-export([payment_refund_idempotency/1]).
-export([payment_refund_success/1]).
-export([deadline_doesnt_affect_payment_refund/1]).
-export([payment_manual_refund/1]).
-export([payment_partial_refunds_success/1]).
-export([payment_refund_id_types/1]).
@ -198,6 +201,7 @@ groups() ->
payment_start_idempotency,
payment_success,
processing_deadline_reached_test,
payment_success_empty_cvv,
payment_success_additional_info,
payment_w_terminal_success,
@ -231,6 +235,7 @@ groups() ->
retry_temporary_unavailability_refund,
payment_refund_idempotency,
payment_refund_success,
deadline_doesnt_affect_payment_refund,
payment_partial_refunds_success,
invalid_amount_payment_partial_refund,
invalid_amount_partial_capture_and_refund,
@ -246,6 +251,7 @@ groups() ->
payment_hold_cancellation,
payment_hold_auto_cancellation,
payment_hold_capturing,
deadline_doesnt_affect_payment_capturing,
invalid_currency_partial_capture,
invalid_amount_partial_capture,
payment_hold_partial_capturing,
@ -805,6 +811,27 @@ payment_success(C) ->
?payment_w_status(PaymentID, ?captured()) = Payment,
?payment_w_context(Context) = Payment.
-spec processing_deadline_reached_test(config()) -> test_return().
processing_deadline_reached_test(C) ->
Client = cfg(client, C),
InvoiceID = start_invoice(<<"rubberduck">>, make_due_date(10), 42000, C),
Context = #'Content'{
type = <<"application/x-erlang-binary">>,
data = erlang:term_to_binary({you, 643, "not", [<<"welcome">>, here]})
},
PaymentParams0 = set_payment_context(Context, make_payment_params()),
Deadline = hg_datetime:format_now(),
PaymentParams = PaymentParams0#payproc_InvoicePaymentParams{processing_deadline = Deadline},
PaymentID = start_payment(InvoiceID, PaymentParams, Client),
PaymentID = await_sessions_restarts(PaymentID, ?processed(), InvoiceID, Client, 0),
[?payment_ev(PaymentID, ?payment_status_changed(?failed({failure, Failure})))] = next_event(InvoiceID, Client),
ok = payproc_errors:match(
'PaymentFailure',
Failure,
fun({authorization_failed, {processing_deadline_reached, _}}) -> ok end
).
-spec payment_success_empty_cvv(config()) -> test_return().
payment_success_empty_cvv(C) ->
@ -1774,6 +1801,55 @@ payment_refund_success(C) ->
?invalid_payment_status(?refunded()) =
hg_client_invoicing:refund_payment(InvoiceID, PaymentID, RefundParams, Client).
-spec deadline_doesnt_affect_payment_refund(config()) -> _ | no_return().
deadline_doesnt_affect_payment_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),
ProcessingDeadline = 4000, % ms
PaymentParams = set_processing_deadline(ProcessingDeadline, make_payment_params()),
PaymentID = process_payment(InvoiceID, PaymentParams, Client),
RefundParams = make_refund_params(),
% not finished yet
?invalid_payment_status(?processed()) =
hg_client_invoicing:refund_payment(InvoiceID, PaymentID, RefundParams, Client),
PaymentID = await_payment_capture(InvoiceID, PaymentID, Client),
timer:sleep(ProcessingDeadline),
% not enough funds on the merchant account
Failure = {failure, payproc_errors:construct('RefundFailure',
{terms_violated, {insufficient_merchant_funds, #payprocerr_GeneralFailure{}}}
)},
Refund0 = #domain_InvoicePaymentRefund{id = RefundID0} =
hg_client_invoicing:refund_payment(InvoiceID, PaymentID, RefundParams, Client),
PaymentID = refund_payment(InvoiceID, PaymentID, RefundID0, Refund0, Client),
[
?payment_ev(PaymentID, ?refund_ev(RefundID0, ?refund_status_changed(?refund_failed(Failure))))
] = next_event(InvoiceID, 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),
% create a refund finally
Refund = #domain_InvoicePaymentRefund{id = RefundID} =
hg_client_invoicing:refund_payment(InvoiceID, PaymentID, RefundParams, Client),
Refund =
hg_client_invoicing:get_payment_refund(InvoiceID, PaymentID, RefundID, Client),
PaymentID = refund_payment(InvoiceID, PaymentID, RefundID, Refund, Client),
PaymentID = await_refund_session_started(InvoiceID, PaymentID, RefundID, Client),
[
?payment_ev(PaymentID, ?refund_ev(ID, ?session_ev(?refunded(), ?trx_bound(_)))),
?payment_ev(PaymentID, ?refund_ev(ID, ?session_ev(?refunded(), ?session_finished(?session_succeeded()))))
] = next_event(InvoiceID, Client),
[
?payment_ev(PaymentID, ?refund_ev(ID, ?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).
-spec payment_manual_refund(config()) -> _ | no_return().
payment_manual_refund(C) ->
@ -2147,8 +2223,19 @@ payment_hold_auto_cancellation(C) ->
payment_hold_capturing(C) ->
Client = cfg(client, C),
InvoiceID = start_invoice(<<"rubberduck">>, make_due_date(10), 42000, C),
PaymentParams = make_payment_params({hold, cancel}),
PaymentID = process_payment(InvoiceID, make_payment_params({hold, cancel}), Client),
ok = hg_client_invoicing:capture_payment(InvoiceID, PaymentID, <<"ok">>, Client),
PaymentID = await_payment_capture(InvoiceID, PaymentID, <<"ok">>, Client).
-spec deadline_doesnt_affect_payment_capturing(config()) -> _ | no_return().
deadline_doesnt_affect_payment_capturing(C) ->
Client = cfg(client, C),
InvoiceID = start_invoice(<<"rubberduck">>, make_due_date(10), 42000, C),
ProcessingDeadline = 4000, % ms
PaymentParams = set_processing_deadline(ProcessingDeadline, make_payment_params({hold, cancel})),
PaymentID = process_payment(InvoiceID, PaymentParams, Client),
timer:sleep(ProcessingDeadline),
ok = hg_client_invoicing:capture_payment(InvoiceID, PaymentID, <<"ok">>, Client),
PaymentID = await_payment_capture(InvoiceID, PaymentID, <<"ok">>, Client).
@ -2435,9 +2522,11 @@ adhoc_repair_failed_succeeded(C) ->
PaymentParams = make_payment_params(PaymentTool, Session),
PaymentID = start_payment(InvoiceID, PaymentParams, Client),
[
?payment_ev(PaymentID, ?session_ev(?processed(), ?session_started())),
?payment_ev(PaymentID, ?session_ev(?processed(), ?trx_bound(?trx_info(PaymentID))))
?payment_ev(PaymentID, ?session_ev(?processed(), ?session_started()))
] = next_event(InvoiceID, Client),
[
?payment_ev(PaymentID, ?session_ev(?processed(), ?trx_bound(?trx_info(PaymentID))))
] = next_event(InvoiceID, Client),
% assume no more events here since machine is FUBAR already
timeout = next_event(InvoiceID, 2000, Client),
Changes = [
@ -2477,7 +2566,9 @@ adhoc_repair_invalid_changes_failed(C) ->
PaymentParams = make_payment_params(PaymentTool, Session),
PaymentID = start_payment(InvoiceID, PaymentParams, Client),
[
?payment_ev(PaymentID, ?session_ev(?processed(), ?session_started())),
?payment_ev(PaymentID, ?session_ev(?processed(), ?session_started()))
] = next_event(InvoiceID, Client),
[
?payment_ev(PaymentID, ?session_ev(?processed(), ?trx_bound(?trx_info(PaymentID))))
] = next_event(InvoiceID, Client),
timeout = next_event(InvoiceID, 1000, Client),
@ -2652,7 +2743,9 @@ repair_fail_session_succeeded(C) ->
PaymentParams = make_payment_params(PaymentTool, Session),
PaymentID = start_payment(InvoiceID, PaymentParams, Client),
[
?payment_ev(PaymentID, ?session_ev(?processed(), ?session_started())),
?payment_ev(PaymentID, ?session_ev(?processed(), ?session_started()))
] = next_event(InvoiceID, Client),
[
?payment_ev(PaymentID, ?session_ev(?processed(), ?trx_bound(?trx_info(PaymentID))))
] = next_event(InvoiceID, Client),
@ -2701,7 +2794,9 @@ repair_complex_succeeded_second(C) ->
PaymentParams = make_payment_params(PaymentTool, Session),
PaymentID = start_payment(InvoiceID, PaymentParams, Client),
[
?payment_ev(PaymentID, ?session_ev(?processed(), ?session_started())),
?payment_ev(PaymentID, ?session_ev(?processed(), ?session_started()))
] = next_event(InvoiceID, Client),
[
?payment_ev(PaymentID, ?session_ev(?processed(), ?trx_bound(?trx_info(PaymentID))))
] = next_event(InvoiceID, Client),
@ -3059,10 +3154,14 @@ await_payment_session_started(InvoiceID, PaymentID, Client, Target) ->
PaymentID.
await_payment_process_interaction(InvoiceID, PaymentID, Client) ->
Events0 = next_event(InvoiceID, Client),
[
?payment_ev(PaymentID, ?session_ev(?processed(), ?session_started()))
] = Events0,
Events1 = next_event(InvoiceID, Client),
[
?payment_ev(PaymentID, ?session_ev(?processed(), ?session_started())),
?payment_ev(PaymentID, ?session_ev(?processed(), ?interaction_requested(UserInteraction)))
] = next_event(InvoiceID, Client),
] = Events1,
UserInteraction.
await_payment_process_finish(InvoiceID, PaymentID, Client) ->
@ -4580,3 +4679,8 @@ construct_term_set_for_partial_capture_provider_permit(Revision) ->
}
}}
].
% Deadline as timeout()
set_processing_deadline(Timeout, PaymentParams) ->
Deadline = woody_deadline:to_binary(woody_deadline:from_timeout(Timeout)),
PaymentParams#payproc_InvoicePaymentParams{processing_deadline = Deadline}.