diff --git a/apps/hellgate/include/cashreg_events.hrl b/apps/hellgate/include/cashreg_events.hrl new file mode 100644 index 000000000..9cae88a10 --- /dev/null +++ b/apps/hellgate/include/cashreg_events.hrl @@ -0,0 +1,55 @@ +-ifndef(__hellgate_cashreg_events__). +-define(__hellgate_cashreg_events__, 42). + +% Events + +-define(cashreg_receipt_created(ReceiptParams, Adapter), + {receipt_created, #cashreg_proc_ReceiptCreated{ + receipt_params = ReceiptParams, + adapter = Adapter + }} +). + +-define(cashreg_receipt_registered(ReceiptRegEntry), + {receipt_registered, #cashreg_proc_ReceiptRegistered{receipt_reg_entry = ReceiptRegEntry}}). + +-define(cashreg_receipt_failed(Failure), + {receipt_failed, #cashreg_proc_ReceiptFailed{failure = Failure}}). + +-define(cashreg_receipt_session_changed(Payload), + {receipt_session_changed, #cashreg_proc_ReceiptSessionChange{ + payload = Payload + }} +). + +%% Sessions + +-define(cashreg_receipt_session_started(), + {session_started, + #cashreg_proc_SessionStarted{} + } +). +-define(cashreg_receipt_session_finished(Result), + {session_finished, + #cashreg_proc_SessionFinished{result = Result} + } +). +-define(cashreg_receipt_session_suspended(Tag), + {session_suspended, + #cashreg_proc_SessionSuspended{tag = Tag} + } +). +-define(cashreg_receipt_adapter_st_changed(AdapterSt), + {session_adapter_state_changed, + #cashreg_proc_SessionAdapterStateChanged{adapter_state = AdapterSt} + } +). + +-define(cashreg_receipt_session_succeeded(), + {succeeded, #cashreg_proc_SessionSucceeded{}} +). +-define(cashreg_receipt_session_failed(Failure), + {failed, #cashreg_proc_SessionFailed{failure = Failure}} +). + +-endif. diff --git a/apps/hellgate/include/party_events.hrl b/apps/hellgate/include/party_events.hrl index dd5deadad..e62f13c8c 100644 --- a/apps/hellgate/include/party_events.hrl +++ b/apps/hellgate/include/party_events.hrl @@ -53,6 +53,9 @@ -define(proxy_modification(Proxy), {proxy_modification, #payproc_ProxyModification{proxy = Proxy}}). +-define(cashreg_modification(CashRegister), + {cash_register_modification, #payproc_CashRegModification{cash_register = CashRegister}}). + -define(contract_effect(ID, Effect), {contract_effect, #payproc_ContractEffectUnit{contract_id = ID, effect = Effect}}). diff --git a/apps/hellgate/include/payment_events.hrl b/apps/hellgate/include/payment_events.hrl index 498573f9c..3770007da 100644 --- a/apps/hellgate/include/payment_events.hrl +++ b/apps/hellgate/include/payment_events.hrl @@ -149,4 +149,23 @@ -define(refund_failed(Failure), {failed, #domain_InvoicePaymentRefundFailed{failure = Failure}}). +%% Receipt + +-define(receipt_ev(ReceiptID, Payload, EventID), + {invoice_payment_receipt_change, #payproc_InvoicePaymentReceiptChange{ + id = ReceiptID, + payload = Payload, + event_id = EventID + }} +). + +-define(receipt_created(), + {invoice_payment_receipt_created, #payproc_InvoicePaymentReceiptCreated{}}). + +-define(receipt_registered(), + {invoice_payment_receipt_registered, #payproc_InvoicePaymentReceiptRegistered{}}). + +-define(receipt_failed(Failure), + {invoice_payment_receipt_failed, #payproc_InvoicePaymentReceiptFailed{failure = Failure}}). + -endif. diff --git a/apps/hellgate/src/hellgate.erl b/apps/hellgate/src/hellgate.erl index 06015681d..24869239c 100644 --- a/apps/hellgate/src/hellgate.erl +++ b/apps/hellgate/src/hellgate.erl @@ -40,7 +40,8 @@ init([]) -> hg_invoice, hg_invoice_template, hg_customer, - hg_recurrent_paytool + hg_recurrent_paytool, + hg_cashreg_controller ], {ok, { #{strategy => one_for_all, intensity => 6, period => 30}, @@ -67,7 +68,10 @@ get_api_child_spec(MachineHandlers) -> construct_service_handler(recurrent_paytool , hg_recurrent_paytool ), construct_service_handler(recurrent_paytool_eventsink , hg_recurrent_paytool ), construct_service_handler(proxy_host_provider , hg_proxy_host_provider), - construct_service_handler(payment_processing_eventsink , hg_event_sink_handler ) + construct_service_handler(payment_processing_eventsink , hg_event_sink_handler ), + construct_service_handler(cashreg , hg_cashreg_controller ), + construct_service_handler(cashreg_host_provider , hg_cashreg_controller ), + construct_service_handler(cashreg_eventsink , hg_cashreg_controller ) ] } ). diff --git a/apps/hellgate/src/hg_cashreg_controller.erl b/apps/hellgate/src/hg_cashreg_controller.erl new file mode 100644 index 000000000..6540caddd --- /dev/null +++ b/apps/hellgate/src/hg_cashreg_controller.erl @@ -0,0 +1,841 @@ +-module(hg_cashreg_controller). + +-include_lib("cashreg_proto/include/cashreg_proto_adapter_provider_thrift.hrl"). +-include_lib("cashreg_proto/include/cashreg_proto_processing_thrift.hrl"). + +-define(NS, <<"cashreg">>). + +-export([process_callback/2]). + +%% Woody handler called by hg_woody_wrapper + +-behaviour(hg_woody_wrapper). +-export([handle_function/3]). + +%% Machine callbacks + +-behaviour(hg_machine). +-export([namespace /0]). +-export([init /2]). +-export([process_signal/3]). +-export([process_call /3]). + +% Types +-record(st, { + receipt_params :: undefined | receipt_params(), + adapter :: undefined | adapter(), + session :: undefined | session(), + receipt_status :: undefined | receipt_status() +}). +% -type st() :: #st{}. + +-type session() :: #{ + status := undefined | suspended | finished, + result => session_result(), + adapter_state => adapter_state() +}. + +-type receipt_params() :: cashreg_proto_main_thrift:'ReceiptParams'(). +-type receipt_id() :: cashreg_proto_main_thrift:'ReceiptID'(). +-type receipt_status() :: cashreg_proto_main_thrift:'ReceiptStatus'(). +-type session_result() :: cashreg_proto_processing_thrift:'SessionResult'(). +-type adapter() :: cashreg_proto_adapter_provider_thrift:'Adapter'(). +-type adapter_state() :: cashreg_proto_adapter_provider_thrift:'AdapterState'(). +-type tag() :: cashreg_proto_adapter_provider_thrift:'Tag'(). +-type callback() :: cashreg_proto_adapter_provider_thrift:'Callback'(). +-type callback_response() :: _. + +%% Woody handler + +-spec handle_function(woody:func(), woody:args(), hg_woody_wrapper:handler_opts()) -> + term() | no_return(). + +handle_function('GetEvents', [#cashreg_proc_EventRange{'after' = After, limit = Limit}], _Opts) -> + case hg_event_sink:get_events(?NS, After, Limit) of + {ok, Events} -> + publish_events(Events); + {error, event_not_found} -> + throw(#cashreg_proc_EventNotFound{}) + end; +handle_function('GetLastEventID', [], _Opts) -> + case hg_event_sink:get_last_event_id(?NS) of + {ok, ID} -> + ID; + {error, no_last_event} -> + throw(#cashreg_proc_NoLastEvent{}) + end; +handle_function(Func, Args, Opts) -> + scoper:scope(cashreg, + fun() -> handle_function_(Func, Args, Opts) end + ). + +handle_function_('CreateReceipt', [ReceiptParams, AdapterOptions], _Opts) -> + ReceiptID = hg_utils:unique_id(), + ok = set_meta(ReceiptID), + ok = start(ReceiptID, [ReceiptParams, AdapterOptions]), + Status = {created, #cashreg_main_ReceiptCreated{}}, + construct_receipt(ReceiptID, Status, ReceiptParams); +handle_function_('GetReceipt', [ReceiptID], _Opts) -> + ok = set_meta(ReceiptID), + get_receipt(ReceiptID); +handle_function_('GetReceiptEvents', [ReceiptID, Range], _Opts) -> + ok = set_meta(ReceiptID), + get_public_history(ReceiptID, Range); +handle_function_('ProcessReceiptCallback', [Tag, Callback], _) -> + map_error(process_callback(Tag, {provider, Callback})). + +%% Event sink + +publish_events(Events) -> + [publish_event(Event) || Event <- Events]. + +publish_event({ID, Ns, SourceID, {EventID, Dt, Payload}}) -> + hg_event_provider:publish_event(Ns, ID, SourceID, {EventID, Dt, hg_msgpack_marshalling:unmarshal(Payload)}). + +%% + +set_meta(ID) -> + scoper:add_meta(#{id => ID}). + +start(ID, Args) -> + map_start_error(hg_machine:start(?NS, ID, Args)). + +map_start_error({ok, _}) -> + ok; +map_start_error({error, Reason}) -> + error(Reason). + +get_public_history(ReceiptID, #cashreg_proc_EventRange{'after' = AfterID, limit = Limit}) -> + [publish_receipt_event(ReceiptID, Ev) || Ev <- get_history(ReceiptID, AfterID, Limit)]. + +publish_receipt_event(ReceiptID, {ID, Dt, Payload}) -> + #cashreg_proc_ReceiptEvent{ + id = ID, + created_at = Dt, + source = ReceiptID, + payload = Payload + }. + +get_receipt(ReceiptID) -> + St = collapse_history(get_history(ReceiptID)), + Status = St#st.receipt_status, + ReceiptParams = St#st.receipt_params, + construct_receipt(ReceiptID, Status, ReceiptParams). + +get_history(Ref) -> + History = hg_machine:get_history(?NS, Ref), + unmarshal(map_history_error(History)). + +get_history(Ref, AfterID, Limit) -> + History = hg_machine:get_history(?NS, Ref, AfterID, Limit), + unmarshal(map_history_error(History)). + +map_history_error({ok, Result}) -> + Result; +map_history_error({error, notfound}) -> + throw({error, notfound}). + +map_error({ok, Response}) -> + Response; +map_error({error, Reason}) -> + error(Reason). + +-include("cashreg_events.hrl"). + +%% hg_machine callbacks + +-spec namespace() -> + hg_machine:ns(). +namespace() -> + ?NS. + +-spec init(receipt_id(), [receipt_params() | adapter()]) -> + hg_machine:result(). +init(_ReceiptID, [ReceiptParams, Adapter]) -> + Changes = [?cashreg_receipt_created(ReceiptParams, Adapter)], + Action = hg_machine_action:instant(), + Result = #{ + changes => Changes, + action => Action + }, + handle_result(Result). + +-spec process_signal(hg_machine:signal(), hg_machine:history(), hg_machine:auxst()) -> + hg_machine:result(). +process_signal(Signal, History, _AuxSt) -> + handle_result(handle_signal(Signal, collapse_history(unmarshal(History)))). + +handle_signal(timeout, St) -> + process_timeout(St). + +process_timeout(#st{session = #{status := Status}} = St) -> + Action = hg_machine_action:new(), + case Status of + undefined -> + process(Action, St); + suspended -> + process_callback_timeout(Action, St) + end. + +process( + Action, + #st{ + receipt_params = ReceiptParams, + adapter = Adapter, + session = Session + } = St +) -> + AdapterState = maps:get(adapter_state, Session, undefined), + ReceiptAdapterResult = register_receipt(ReceiptParams, Adapter, AdapterState), + Result = handle_adapter_result(ReceiptAdapterResult, Action), + finish_processing(Result, St). + +register_receipt(ReceiptParams, Adapter, AdapterState) -> + ReceiptContext = construct_receipt_context(ReceiptParams, Adapter), + Session = construct_session(AdapterState), + {ok, ReceiptAdapterResult} = hg_cashreg_provider:register_receipt(ReceiptContext, Session, Adapter), + ReceiptAdapterResult. + +process_callback_timeout(Action, St) -> + Result = handle_adapter_callback_timeout(Action), + finish_processing(Result, St). + +handle_adapter_callback_timeout(Action) -> + Changes = [ + ?cashreg_receipt_session_finished(?cashreg_receipt_session_failed({ + receipt_registration_failed, #cashreg_main_ReceiptRegistrationFailed{ + % TODO temporary solution with code 0, should be fixed. + reason = #cashreg_main_ExternalFailure{code = <<"0">>} + } + })) + ], + make_adapter_result(Changes, Action). + +make_adapter_result(Changes, Action) -> + make_adapter_result(Changes, Action, undefined). + +make_adapter_result(Changes, Action, Receipt) -> + {wrap_session_events(Changes), Action, Receipt}. + +wrap_session_events(SessionEvents) -> + [?cashreg_receipt_session_changed(Ev) || Ev <- SessionEvents]. + +-spec process_call({callback, _}, hg_machine:history(), hg_machine:auxst()) -> + {hg_machine:response(), hg_machine:result()}. +process_call(Call, History, _AuxSt) -> + St = collapse_history(unmarshal(History)), + try handle_result(handle_call(Call, St)) catch + throw:Exception -> + {{exception, Exception}, #{}} + end. + +handle_call({callback, Callback}, St) -> + dispatch_callback(Callback, St). + +dispatch_callback( + {provider, Callback}, + #st{ + receipt_params = ReceiptParams, + adapter = Adapter, + session = #{status := suspended, adapter_state := AdapterState} + } = St +) -> + Action = hg_machine_action:new(), + ReceiptContext = construct_receipt_context(ReceiptParams, Adapter), + {ok, CallbackResult} = hg_cashreg_provider:handle_receipt_callback( + Callback, + ReceiptContext, + construct_session(AdapterState), + Adapter + ), + {Response, Result} = handle_callback_result(CallbackResult, Action), + maps:merge(#{response => Response}, finish_processing(Result, St)); +dispatch_callback(_Callback, _St) -> + throw(invalid_callback). + +handle_result(Params) -> + Result = handle_result_changes(Params, handle_result_action(Params, #{})), + case maps:find(response, Params) of + {ok, Response} -> + {{ok, Response}, Result}; + error -> + Result + end. + +handle_result_changes(#{changes := Changes = [_ | _]}, Acc) -> + Acc#{events => [marshal(Changes)]}; +handle_result_changes(#{}, Acc) -> + Acc. + +handle_result_action(#{action := Action}, Acc) -> + Acc#{action => Action}. +% handle_result_action(#{}, Acc) -> +% Acc. + +collapse_history(History) -> + lists:foldl( + fun ({_ID, _, Events}, St0) -> + lists:foldl(fun apply_change/2, St0, Events) + end, + #st{}, + History + ). + +apply_changes(Changes, St) -> + lists:foldl(fun apply_change/2, St, Changes). + +apply_change(Event, undefined) -> + apply_change(Event, #st{}); + +apply_change(?cashreg_receipt_created(ReceiptParams, Adapter), St) -> + St#st{ + receipt_params = ReceiptParams, + adapter = Adapter, + session = #{status => undefined}, + receipt_status = {created, #cashreg_main_ReceiptCreated{}} + }; +apply_change(?cashreg_receipt_registered(Receipt), St) -> + St#st{ + receipt_status = {registered, #cashreg_main_ReceiptRegistered{ + receipt_reg_entry = Receipt + }} + }; +apply_change(?cashreg_receipt_failed(Failure), St) -> + St#st{ + receipt_status = {failed, #cashreg_main_ReceiptFailed{ + reason = Failure + }} + }; + +apply_change(?cashreg_receipt_session_changed(Event), #st{session = Session0} = St) -> + Session1 = merge_session_change(Event, Session0), + St#st{session = Session1}. + +merge_session_change(?cashreg_receipt_session_started(), _) -> + #{status => undefined}; +merge_session_change(?cashreg_receipt_session_finished(Result), Session) -> + Session#{status := finished, result => Result}; +merge_session_change(?cashreg_receipt_session_suspended(_Tag), Session) -> + Session#{status := suspended}; +merge_session_change(?cashreg_receipt_adapter_st_changed(AdapterState), Session) -> + Session#{adapter_state => AdapterState}. + +-spec process_callback(tag(), {provider, callback()}) -> + {ok, callback_response()} | {error, invalid_callback | notfound | failed} | no_return(). + +process_callback(Tag, Callback) -> + case hg_machine:call(?NS, {tag, Tag}, {callback, Callback}) of + {ok, {ok, _} = Ok} -> + Ok; + {ok, {exception, invalid_callback}} -> + {error, invalid_callback}; + {error, _} = Error -> + Error + end. + +update_adapter_state(undefined) -> + []; +update_adapter_state(AdapterState) -> + [?cashreg_receipt_adapter_st_changed(AdapterState)]. + +handle_adapter_intent(#'cashreg_adptprv_FinishIntent'{status = {success, _}}, Action) -> + Events = [?cashreg_receipt_session_finished(?cashreg_receipt_session_succeeded())], + {Events, Action}; +handle_adapter_intent( + #'cashreg_adptprv_FinishIntent'{status = {failure, #cashreg_adptprv_Failure{error = Error}}}, + Action +) -> + Events = [?cashreg_receipt_session_finished(?cashreg_receipt_session_failed(Error))], + {Events, Action}; +handle_adapter_intent(#'cashreg_adptprv_SleepIntent'{timer = Timer}, Action0) -> + Action = hg_machine_action:set_timer(Timer, Action0), + Events = [], + {Events, Action}; +handle_adapter_intent(#'cashreg_adptprv_SuspendIntent'{tag = Tag, timeout = Timer}, Action0) -> + Action = hg_machine_action:set_timer(Timer, hg_machine_action:set_tag(Tag, Action0)), + Events = [?cashreg_receipt_session_suspended(Tag)], + {Events, Action}. + +handle_callback_result( + #cashreg_adptprv_ReceiptCallbackResult{result = AdapterResult, response = Response}, + Action +) -> + {Response, handle_adapter_callback_result(AdapterResult, hg_machine_action:unset_timer(Action))}. + +handle_adapter_result( + #cashreg_adptprv_ReceiptAdapterResult{ + intent = {_Type, Intent}, + next_state = AdapterState + }, + Action0 +) -> + Changes1 = update_adapter_state(AdapterState), + {Changes2, Action} = handle_adapter_intent(Intent, Action0), + handle_intent(Intent, Changes1 ++ Changes2, Action). + +handle_adapter_callback_result( + #cashreg_adptprv_ReceiptCallbackAdapterResult{ + intent = {_Type, Intent}, + next_state = AdapterState + }, + Action0 +) -> + Changes1 = update_adapter_state(AdapterState), + {Changes2, Action} = handle_adapter_intent(Intent, hg_machine_action:unset_timer(Action0)), + handle_intent(Intent, Changes1 ++ Changes2, Action). + +handle_intent(Intent, Changes, Action) -> + case Intent of + #cashreg_adptprv_FinishIntent{ + status = {'success', #cashreg_adptprv_Success{receipt_reg_entry = ReceiptRegEntry}} + } -> + make_adapter_result(Changes, Action, ReceiptRegEntry); + _ -> + make_adapter_result(Changes, Action) + end. + +finish_processing({Changes, Action, Receipt}, St) -> + #st{session = Session} = apply_changes(Changes, St), + case Session of + #{status := finished, result := ?cashreg_receipt_session_succeeded()} -> + #{ + changes => Changes ++ [?cashreg_receipt_registered(Receipt)], + action => Action + }; + #{status := finished, result := ?cashreg_receipt_session_failed(Failure)} -> + #{ + changes => Changes ++ [?cashreg_receipt_failed(Failure)], + action => Action + }; + #{} -> + #{ + changes => Changes, + action => Action + } + end. + +construct_receipt( + ReceiptID, + Status, + #cashreg_main_ReceiptParams{ + party = Party, + operation = Opeartion, + purchase = Purchase, + payment = Payment, + metadata = Metadata + } +) -> + #cashreg_main_Receipt{ + id = ReceiptID, + status = Status, + party = Party, + operation = Opeartion, + purchase = Purchase, + payment = Payment, + metadata = Metadata + }. + + +construct_receipt_context(ReceiptParams, Adapter) -> + #cashreg_adptprv_ReceiptContext{ + receipt_params = ReceiptParams, + options = Adapter#cashreg_adptprv_Adapter.options + }. + +construct_session(State) -> + #cashreg_adptprv_Session{ + state = State + }. + +%% Marshalling + +marshal(Changes) when is_list(Changes) -> + [marshal(change, Change) || Change <- Changes]. + +%% Changes + +marshal(change, ?cashreg_receipt_created(ReceiptParams, Adapter)) -> + [1, #{ + <<"change">> => <<"created">>, + <<"receipt_params">> => marshal(receipt_params, ReceiptParams), + <<"adapter">> => marshal(adapter, Adapter) + }]; +marshal(change, ?cashreg_receipt_registered(ReceiptRegEntry)) -> + [1, #{ + <<"change">> => <<"registered">>, + <<"receipt_reg_entry">> => marshal(receipt_reg_entry, ReceiptRegEntry) + }]; +marshal(change, ?cashreg_receipt_failed(Failure)) -> + [1, #{ + <<"change">> => <<"failed">>, + <<"failure">> => marshal(failure, Failure) + }]; +marshal(change, ?cashreg_receipt_session_changed(Payload)) -> + [1, #{ + <<"change">> => <<"session_changed">>, + <<"payload">> => marshal(session_change, Payload) + }]; + +marshal(receipt_params, #cashreg_main_ReceiptParams{} = ReceiptParams) -> + genlib_map:compact(#{ + <<"party">> => marshal(party, ReceiptParams#cashreg_main_ReceiptParams.party), + <<"operation">> => marshal(operation, ReceiptParams#cashreg_main_ReceiptParams.operation), + <<"purchase">> => marshal(purchase, ReceiptParams#cashreg_main_ReceiptParams.purchase), + <<"payment">> => marshal(payment, ReceiptParams#cashreg_main_ReceiptParams.payment), + <<"metadata">> => marshal(msgpack_value, ReceiptParams#cashreg_main_ReceiptParams.metadata) + }); + +marshal(party, #cashreg_main_Party{} = Party) -> + genlib_map:compact(#{ + <<"reg_name">> => marshal(str, Party#cashreg_main_Party.registered_name), + <<"reg_number">> => marshal(str, Party#cashreg_main_Party.registered_number), + <<"inn">> => marshal(str, Party#cashreg_main_Party.inn), + <<"actual_address">> => marshal(str, Party#cashreg_main_Party.actual_address), + <<"tax_system">> => marshal(tax_system, Party#cashreg_main_Party.tax_system), + <<"shop">> => marshal(shop, Party#cashreg_main_Party.shop) + }); + +marshal(tax_system, osn) -> + <<"osn">>; +marshal(tax_system, usn_income) -> + <<"usn_income">>; +marshal(tax_system, usn_income_outcome) -> + <<"usn_income_outcome">>; +marshal(tax_system, envd) -> + <<"envd">>; +marshal(tax_system, esn) -> + <<"esn">>; +marshal(tax_system, patent) -> + <<"patent">>; + +marshal(shop, #cashreg_main_Shop{} = Shop) -> + genlib_map:compact(#{ + <<"name">> => marshal(str, Shop#cashreg_main_Shop.name), + <<"description">> => marshal(str, Shop#cashreg_main_Shop.description), + <<"location">> => marshal(location, Shop#cashreg_main_Shop.location) + }); + +marshal(location, {url, Url}) -> + [<<"url">>, marshal(str, Url)]; + +marshal(operation, sell) -> + <<"sell">>; +marshal(operation, sell_refund) -> + <<"sell_refund">>; + +marshal(purchase, #cashreg_main_Purchase{lines = Lines}) -> + [marshal(purchase_line, Line) || Line <- Lines]; + +marshal(purchase_line, #cashreg_main_PurchaseLine{} = Line) -> + genlib_map:compact(#{ + <<"product">> => marshal(str, Line#cashreg_main_PurchaseLine.product), + <<"quantity">> => marshal(int, Line#cashreg_main_PurchaseLine.quantity), + <<"price">> => marshal(cash, Line#cashreg_main_PurchaseLine.price), + <<"tax">> => marshal(tax, Line#cashreg_main_PurchaseLine.tax) + }); + +marshal(cash, #cashreg_main_Cash{} = Cash) -> + [1, [ + marshal(int, Cash#cashreg_main_Cash.amount), + marshal(currency, Cash#cashreg_main_Cash.currency) + ]]; + +marshal(currency, #cashreg_main_Currency{} = Currency) -> + #{ + <<"name">> => marshal(str, Currency#cashreg_main_Currency.name), + <<"symbolic_code">> => marshal(str, Currency#cashreg_main_Currency.symbolic_code), + <<"numeric_code">> => marshal(int, Currency#cashreg_main_Currency.numeric_code), + <<"exponent">> => marshal(int, Currency#cashreg_main_Currency.exponent) + }; + +marshal(tax, {vat, VAT}) -> + [<<"vat">>, marshal(vat, VAT)]; + +marshal(vat, vat0) -> + <<"vat0">>; +marshal(vat, vat10) -> + <<"vat10">>; +marshal(vat, vat18) -> + <<"vat18">>; +marshal(vat, vat110) -> + <<"vat110">>; +marshal(vat, vat118) -> + <<"vat118">>; + +marshal(payment, #cashreg_main_Payment{} = Payment) -> + #{ + <<"payment_method">> => marshal(payment_method, Payment#cashreg_main_Payment.payment_method), + <<"cash">> => marshal(cash, Payment#cashreg_main_Payment.cash) + }; + +marshal(payment_method, bank_card) -> + <<"bank_card">>; + +marshal(msgpack_value, undefined) -> + undefined; +marshal(msgpack_value, MsgpackValue) -> + hg_cashreg_msgpack_marshalling:unmarshal(MsgpackValue); + +marshal(adapter, #cashreg_adptprv_Adapter{} = Adapter) -> + #{ + <<"url">> => marshal(str, Adapter#cashreg_adptprv_Adapter.url), + <<"options">> => marshal(map_str, Adapter#cashreg_adptprv_Adapter.options) + }; + +marshal(receipt_reg_entry, #cashreg_main_ReceiptRegistrationEntry{} = ReceiptRegEntry) -> + genlib_map:compact(#{ + <<"id">> => marshal(str, ReceiptRegEntry#cashreg_main_ReceiptRegistrationEntry.id), + <<"metadata">> => marshal(msgpack_value, ReceiptRegEntry#cashreg_main_ReceiptRegistrationEntry.metadata) + }); + +marshal(failure, {receipt_registration_failed, #cashreg_main_ReceiptRegistrationFailed{reason = ExternalFailure}}) -> + [1, [<<"registration_failed">>, genlib_map:compact(#{ + <<"code">> => marshal(str, ExternalFailure#cashreg_main_ExternalFailure.code), + <<"description">> => marshal(str, ExternalFailure#cashreg_main_ExternalFailure.description), + <<"metadata">> => marshal(msgpack_value, ExternalFailure#cashreg_main_ExternalFailure.metadata) + })]]; + +marshal(session_change, ?cashreg_receipt_session_started()) -> + [1, <<"started">>]; +marshal(session_change, ?cashreg_receipt_session_finished(Result)) -> + [1, [ + <<"finished">>, + marshal(session_status, Result) + ]]; +marshal(session_change, ?cashreg_receipt_session_suspended(Tag)) -> + [1, [ + <<"suspended">>, + marshal(str, Tag) + ]]; +marshal(session_change, ?cashreg_receipt_adapter_st_changed(AdapterSt)) -> + [1, [ + <<"changed">>, + marshal(msgpack_value, AdapterSt) + ]]; + +marshal(session_status, ?cashreg_receipt_session_succeeded()) -> + <<"succeeded">>; +marshal(session_status, ?cashreg_receipt_session_failed(Failure)) -> + [ + <<"failed">>, + marshal(failure, Failure) + ]; + +marshal(_, Other) -> + Other. + +%% Unmarshalling + +unmarshal(Events) when is_list(Events) -> + [unmarshal(Event) || Event <- Events]; + +unmarshal({ID, Dt, Payload}) -> + {ID, Dt, unmarshal({list, changes}, Payload)}. + +unmarshal({list, changes}, Changes) when is_list(Changes) -> + [unmarshal(change, Change) || Change <- Changes]; + +unmarshal(change, [1, #{ + <<"change">> := <<"created">>, + <<"receipt_params">> := ReceiptParams, + <<"adapter">> := Adapter +}]) -> + ?cashreg_receipt_created( + unmarshal(receipt_params, ReceiptParams), + unmarshal(adapter, Adapter) + ); +unmarshal(change, [1, #{ + <<"change">> := <<"registered">>, + <<"receipt_reg_entry">> := ReceiptRegEntry +}]) -> + ?cashreg_receipt_registered( + unmarshal(receipt_reg_entry, ReceiptRegEntry) + ); +unmarshal(change, [1, #{ + <<"change">> := <<"failed">>, + <<"failure">> := Failure +}]) -> + ?cashreg_receipt_failed( + unmarshal(failure, Failure) + ); +unmarshal(change, [1, #{ + <<"change">> := <<"session_changed">>, + <<"payload">> := Payload +}]) -> + ?cashreg_receipt_session_changed( + unmarshal(session_change, Payload) + ); + +unmarshal(receipt_params, #{ + <<"party">> := Party, + <<"operation">> := Operation, + <<"purchase">> := Purchase, + <<"payment">> := Payment +} = RP) -> + Metadata = genlib_map:get(<<"metadata">>, RP), + #cashreg_main_ReceiptParams{ + party = unmarshal(party, Party), + operation = unmarshal(operation, Operation), + purchase = unmarshal(purchase, Purchase), + payment = unmarshal(payment, Payment), + metadata = unmarshal(msgpack_value, Metadata) + }; + +unmarshal(party, #{ + <<"reg_name">> := RegName, + <<"reg_number">> := RegNumber, + <<"inn">> := Inn, + <<"actual_address">> := ActualAddress, + <<"shop">> := Shop +} = P) -> + TaxSystem = genlib_map:get(<<"tax_system">>, P), + #cashreg_main_Party{ + registered_name = unmarshal(str, RegName), + registered_number = unmarshal(str, RegNumber), + inn = unmarshal(str, Inn), + actual_address = unmarshal(str, ActualAddress), + tax_system = unmarshal(tax_system, TaxSystem), + shop = unmarshal(shop, Shop) + }; + +unmarshal(tax_system, <<"osn">>) -> + osn; +unmarshal(tax_system, <<"usn_income">>) -> + usn_income; +unmarshal(tax_system, <<"usn_income_outcome">>) -> + usn_income_outcome; +unmarshal(tax_system, <<"envd">>) -> + envd; +unmarshal(tax_system, <<"esn">>) -> + esn; +unmarshal(tax_system, <<"patent">>) -> + patent; + +unmarshal(shop, #{ + <<"name">> := Name, + <<"location">> := Location +} = S) -> + Description = genlib_map:get(<<"description">>, S), + #cashreg_main_Shop{ + name = unmarshal(str, Name), + description = unmarshal(str, Description), + location = unmarshal(location, Location) + }; + +unmarshal(location, [<<"url">>, Url]) -> + {url, unmarshal(str, Url)}; + +unmarshal(operation, <<"sell">>) -> + sell; +unmarshal(operation, <<"sell_refund">>) -> + sell_refund; + +unmarshal(purchase, Lines) when is_list(Lines) -> + #cashreg_main_Purchase{lines = [unmarshal(purchase_line, Line) || Line <- Lines]}; + +unmarshal(purchase_line, #{ + <<"product">> := Product, + <<"quantity">> := Quantity, + <<"price">> := Price +} = PL) -> + Tax = genlib_map:get(<<"tax">>, PL), + #cashreg_main_PurchaseLine{ + product = unmarshal(str, Product), + quantity = unmarshal(int, Quantity), + price = unmarshal(cash, Price), + tax = unmarshal(tax, Tax) + }; + +unmarshal(cash, [1, [Amount, Currency]]) -> + #cashreg_main_Cash{ + amount = unmarshal(int, Amount), + currency = unmarshal(currency, Currency) + }; + +unmarshal(currency, #{ + <<"name">> := Name, + <<"symbolic_code">> := SymbolicCode, + <<"numeric_code">> := NumericCode, + <<"exponent">> := Exponent +}) -> + #cashreg_main_Currency{ + name = unmarshal(str, Name), + symbolic_code = unmarshal(str, SymbolicCode), + numeric_code = unmarshal(int, NumericCode), + exponent = unmarshal(int, Exponent) + }; + +unmarshal(tax, [<<"vat">>, VAT]) -> + {vat, unmarshal(vat, VAT)}; + +unmarshal(vat, <<"vat0">>) -> + vat0; +unmarshal(vat, <<"vat10">>) -> + vat10; +unmarshal(vat, <<"vat18">>) -> + vat18; +unmarshal(vat, <<"vat110">>) -> + vat110; +unmarshal(vat, <<"vat118">>) -> + vat118; + +unmarshal(payment, #{ + <<"payment_method">> := PaymentMethod, + <<"cash">> := Cash +}) -> + #cashreg_main_Payment{ + payment_method = unmarshal(payment_method, PaymentMethod), + cash = unmarshal(cash, Cash) + }; + +unmarshal(payment_method, <<"bank_card">>) -> + bank_card; + +unmarshal(msgpack_value, undefined) -> + undefined; +unmarshal(msgpack_value, MsgpackValue) -> + hg_cashreg_msgpack_marshalling:marshal(MsgpackValue); + +unmarshal(adapter, #{ + <<"url">> := Url, + <<"options">> := Options +}) -> + #cashreg_adptprv_Adapter{ + url = unmarshal(str, Url), + options = unmarshal(map_str, Options) + }; + +unmarshal(receipt_reg_entry, #{<<"id">> := Id} = RE) -> + Metadata = genlib_map:get(<<"metadata">>, RE), + #cashreg_main_ReceiptRegistrationEntry{ + id = unmarshal(str, Id), + metadata = unmarshal(msgpack_value, Metadata) + }; + +unmarshal(failure, [1, [<<"registration_failed">>, #{<<"code">> := Code} = RF]]) -> + Description = genlib_map:get(<<"description">>, RF), + Metadata = genlib_map:get(<<"metadata">>, RF), + {receipt_registration_failed, #cashreg_main_ReceiptRegistrationFailed{ + reason = #cashreg_main_ExternalFailure{ + code = marshal(str, Code), + description = marshal(str, Description), + metadata = marshal(msgpack_value, Metadata) + } + }}; + +unmarshal(session_change, [1, <<"started">>]) -> + ?cashreg_receipt_session_started(); +unmarshal(session_change, [1, [<<"finished">>, Result]]) -> + ?cashreg_receipt_session_finished(unmarshal(session_status, Result)); +unmarshal(session_change, [1, [<<"suspended">>, Tag]]) -> + ?cashreg_receipt_session_suspended(unmarshal(str, Tag)); +unmarshal(session_change, [1, [<<"changed">>, AdapterSt]]) -> + ?cashreg_receipt_adapter_st_changed(unmarshal(msgpack_value, AdapterSt)); + +unmarshal(session_status, <<"succeeded">>) -> + ?cashreg_receipt_session_succeeded(); +unmarshal(session_status, [<<"failed">>, Failure]) -> + ?cashreg_receipt_session_failed(unmarshal(failure, Failure)); + +unmarshal(_, Other) -> + Other. \ No newline at end of file diff --git a/apps/hellgate/src/hg_cashreg_msgpack_marshalling.erl b/apps/hellgate/src/hg_cashreg_msgpack_marshalling.erl new file mode 100644 index 000000000..d83a8a03a --- /dev/null +++ b/apps/hellgate/src/hg_cashreg_msgpack_marshalling.erl @@ -0,0 +1,56 @@ +-module(hg_cashreg_msgpack_marshalling). +-include_lib("cashreg_proto/include/cashreg_proto_msgpack_thrift.hrl"). + +%% API +-export([marshal/1]). +-export([unmarshal/1]). + +-export_type([value/0]). + +-type value() :: term(). + +%% + +-spec marshal(value()) -> + cashreg_proto_msgpack_thrift:'Value'(). +marshal(undefined) -> + {nl, #cashreg_msgpack_Nil{}}; +marshal(Boolean) when is_boolean(Boolean) -> + {b, Boolean}; +marshal(Integer) when is_integer(Integer) -> + {i, Integer}; +marshal(Float) when is_float(Float) -> + {flt, Float}; +marshal(String) when is_binary(String) -> + {str, String}; +marshal({bin, Binary}) -> + {bin, Binary}; +marshal(Object) when is_map(Object) -> + {obj, maps:fold( + fun(K, V, Acc) -> + maps:put(marshal(K), marshal(V), Acc) + end, + #{}, + Object + )}; +marshal(Array) when is_list(Array) -> + {arr, lists:map(fun marshal/1, Array)}. + +-spec unmarshal(cashreg_proto_msgpack_thrift:'Value'()) -> + value(). +unmarshal({nl, #cashreg_msgpack_Nil{}}) -> + undefined; +unmarshal({b, Boolean}) -> + Boolean; +unmarshal({i, Integer}) -> + Integer; +unmarshal({flt, Float}) -> + Float; +unmarshal({str, String}) -> + String; +unmarshal({bin, Binary}) -> + {bin, Binary}; +unmarshal({obj, Object}) -> + maps:fold(fun(K, V, Acc) -> maps:put(unmarshal(K), unmarshal(V), Acc) end, #{}, Object); +unmarshal({arr, Array}) -> + lists:map(fun unmarshal/1, Array). \ No newline at end of file diff --git a/apps/hellgate/src/hg_cashreg_provider.erl b/apps/hellgate/src/hg_cashreg_provider.erl new file mode 100644 index 000000000..5f78f1369 --- /dev/null +++ b/apps/hellgate/src/hg_cashreg_provider.erl @@ -0,0 +1,32 @@ +-module(hg_cashreg_provider). +-include_lib("cashreg_proto/include/cashreg_proto_adapter_provider_thrift.hrl"). + +-export([register_receipt/3]). +-export([handle_receipt_callback/4]). + +-type receipt_context() :: cashreg_proto_adapter_provider_thrift:'ReceiptContext'(). +-type session() :: cashreg_proto_adapter_provider_thrift:'Session'(). +-type adapter() :: cashreg_proto_adapter_provider_thrift:'Adapter'(). +-type callback() :: cashreg_proto_adapter_provider_thrift:'Callback'(). + +-spec register_receipt(receipt_context(), session(), adapter()) -> + term(). +register_receipt(ReceiptContext, Session, Adapter) -> + issue_call('RegisterReceipt', [ReceiptContext, Session], Adapter). + +-spec handle_receipt_callback(callback(), receipt_context(), session(), adapter()) -> + term(). +handle_receipt_callback(Callback, ReceiptContext, Session, Adapter) -> + issue_call('HandleReceiptCallback', [Callback, ReceiptContext, Session], Adapter). + +issue_call(Func, Args, Adapter) -> + hg_woody_wrapper:call(cashreg_provider, Func, Args, construct_call_options(Adapter)). + +construct_call_options(#cashreg_adptprv_Adapter{url = URL}) -> + Opts = case genlib_app:env(hellgate, adapter_opts, #{}) of + #{transport_opts := TransportOpts = #{}} -> + #{transport_opts => maps:to_list(maps:with([connect_timeout, recv_timeout], TransportOpts))}; + #{} -> + #{} + end, + Opts#{url => URL}. diff --git a/apps/hellgate/src/hg_claim.erl b/apps/hellgate/src/hg_claim.erl index 7f5731e25..ab6daec44 100644 --- a/apps/hellgate/src/hg_claim.erl +++ b/apps/hellgate/src/hg_claim.erl @@ -192,6 +192,8 @@ is_shop_modification_need_acceptance({proxy_modification, _}) -> false; is_shop_modification_need_acceptance({shop_account_creation, _}) -> false; +is_shop_modification_need_acceptance(?cashreg_modification(_)) -> + false; is_shop_modification_need_acceptance(_) -> true. @@ -301,7 +303,9 @@ make_shop_modification_effect(_, ?proxy_modification(Proxy), _) -> make_shop_modification_effect(_, {location_modification, Location}, _) -> {location_changed, Location}; make_shop_modification_effect(_, {shop_account_creation, Params}, _) -> - {account_created, create_shop_account(Params)}. + {account_created, create_shop_account(Params)}; +make_shop_modification_effect(_, ?cashreg_modification(CashRegister), _) -> + {cash_register_changed, #payproc_ShopCashRegisterChanged{cash_register = CashRegister}}. create_shop_account(#payproc_ShopAccountParams{currency = Currency}) -> create_shop_account(Currency); @@ -458,7 +462,9 @@ update_shop({location_changed, Location}, Shop) -> update_shop({proxy_changed, #payproc_ShopProxyChanged{proxy = Proxy}}, Shop) -> Shop#domain_Shop{proxy = Proxy}; update_shop({account_created, Account}, Shop) -> - Shop#domain_Shop{account = Account}. + Shop#domain_Shop{account = Account}; +update_shop({cash_register_changed, #payproc_ShopCashRegisterChanged{cash_register = CashRegister}}, Shop) -> + Shop#domain_Shop{cash_register = CashRegister}. -spec raise_invalid_changeset(dmsl_payment_processing_thrift:'InvalidChangesetReason'()) -> no_return(). diff --git a/apps/hellgate/src/hg_invoice_payment.erl b/apps/hellgate/src/hg_invoice_payment.erl index b2f04717b..d0c5d108a 100644 --- a/apps/hellgate/src/hg_invoice_payment.erl +++ b/apps/hellgate/src/hg_invoice_payment.erl @@ -68,7 +68,10 @@ %% --type activity() :: undefined | payment | {refund, refund_id()}. +-define(SYNC_INTERVAL, 1). +-define(RECEIPT_EVENTS_LIMIT, 10). + +-type activity() :: undefined | payment | {refund, refund_id()} | {receipt, receipt_id()}. -record(st, { activity :: activity(), @@ -81,6 +84,7 @@ sessions = #{} :: #{target() => session()}, refunds = #{} :: #{refund_id() => refund_state()}, adjustments = [] :: [adjustment()], + receipt :: undefined | receipt(), opts :: undefined | opts() }). @@ -92,6 +96,13 @@ -type refund_state() :: #refund_st{}. +-record(receipt, { + status :: undefined | created | registered | {failed, failure()}, + last_event_id :: integer() +}). + +-type receipt() :: #receipt{}. + -type st() :: #st{}. -export_type([st/0]). @@ -111,6 +122,8 @@ -type route() :: dmsl_domain_thrift:'PaymentRoute'(). -type cash_flow() :: dmsl_domain_thrift:'FinalCashFlow'(). -type trx_info() :: dmsl_domain_thrift:'TransactionInfo'(). +-type receipt_id() :: dmsl_domain_thrift:'InvoicePaymentReceiptID'(). +-type failure() :: dmsl_domain_thrift:'OperationFailure'(). -type session_result() :: dmsl_payment_processing_thrift:'SessionResult'(). -type proxy_state() :: dmsl_proxy_thrift:'ProxyState'(). -type tag() :: dmsl_proxy_thrift:'CallbackTag'(). @@ -829,17 +842,12 @@ process_signal(timeout, St, Options) -> ). process_timeout(St) -> - Action = hg_machine_action:new(), - case get_active_session(St) of - Session when Session /= undefined -> - case get_session_status(Session) of - active -> - process(Action, St); - suspended -> - process_callback_timeout(Action, St) - end; - undefined -> - process_finished_session(St) + case get_activity(St) of + {receipt, _} -> + Status = get_receipt_status(St), + process_timeout_receipt(Status, St); + _ -> + process_timeout_payment(St) end. -spec process_call({callback, tag(), _}, st(), opts()) -> @@ -868,6 +876,37 @@ process_callback(Tag, Payload, Action, Session, St) when Session /= undefined -> process_callback(_Tag, _Payload, _Action, undefined, _St) -> throw(invalid_callback). +process_timeout_payment(St) -> + Action = hg_machine_action:new(), + case get_active_session(St) of + Session when Session /= undefined -> + case get_session_status(Session) of + active -> + process(Action, St); + suspended -> + process_callback_timeout(Action, St) + end; + undefined -> + process_finished_session(St) + end. + +process_timeout_receipt(created, St0) -> + LastEventID = get_receipt_last_event_id(St0), + ID = get_receipt_id(St0), + Changes = get_receipt_changes(ID, LastEventID), + St1 = collapse_changes(Changes, St0), + Result = case get_receipt_status(St1) of + created -> + {[], hg_machine_action:set_timeout(?SYNC_INTERVAL)}; + registered -> + {Changes, hg_machine_action:instant()}; + {failed, _} -> + {Changes, hg_machine_action:instant()} + end, + {done, Result}; +process_timeout_receipt(Status, _) -> + finish_receipt_registration(Status). + process_callback_timeout(Action, St) -> Session = get_active_session(St), Result = handle_proxy_callback_timeout(Action, Session), @@ -880,20 +919,50 @@ process(Action, St) -> finish_processing(Result, St). process_finished_session(St) -> - Target = case get_payment_flow(get_payment(St)) of + case get_payment_flow(get_payment(St)) of ?invoice_payment_flow_instant() -> - ?captured(); + start_receipt_registration(St); ?invoice_payment_flow_hold(OnHoldExpiration, _) -> case OnHoldExpiration of cancel -> - ?cancelled(); + {ok, Result} = start_session(?cancelled()), + {done, Result}; capture -> - ?captured() + start_receipt_registration(St) end + end. + +start_receipt_registration(St) -> + Shop = get_shop(get_opts(St)), + case Shop#domain_Shop.cash_register of + undefined -> + {ok, Result} = start_session(?captured()), + {done, Result}; + _CashRegister -> + ReceiptID = register_receipt(St), + Changes = get_receipt_changes(ReceiptID, undefined), + Action = hg_machine_action:set_timeout(?SYNC_INTERVAL), + {done, {Changes, Action}} + end. + +finish_receipt_registration(Status) -> + Target = case Status of + registered -> + ?captured(); + {failed, _} -> + ?cancelled() end, {ok, Result} = start_session(Target), {done, Result}. +register_receipt(St) -> + Opts = get_opts(St), + Party = get_party(Opts), + Invoice = get_invoice(Opts), + Payment = get_payment(St), + Revision = hg_domain:head(), + hg_payment_cashreg:register_receipt(Party, Invoice, Payment, Revision). + handle_callback(Payload, Action, St) -> ProxyContext = construct_proxy_context(St), {ok, CallbackResult} = issue_callback_call(Payload, ProxyContext, St), @@ -917,7 +986,7 @@ finish_processing(payment, {Events, Action}, St) -> undefined end, NewAction = get_action(Target, Action, St), - {done, {Events ++ [?payment_status_changed(Target)], NewAction}}; + {done, {Events ++ get_changes(Target, St), NewAction}}; #{status := finished, result := ?session_failed(Failure)} -> % TODO is it always rollback? _AffectedAccounts = rollback_payment_cashflow(St), @@ -948,6 +1017,18 @@ finish_processing({refund, ID}, {Events, Action}, St) -> {next, {Events1, Action}} end. +get_changes(?captured() = Target, _) -> + [?payment_status_changed(Target)]; +get_changes(?processed() = Target, _) -> + [?payment_status_changed(Target)]; +get_changes(?cancelled() = Target, St) -> + case get_receipt_status(St) of + {failed, Failure} -> + [?payment_status_changed(?failed(Failure))]; + undefined -> + [?payment_status_changed(Target)] + end. + get_action({processed, _}, Action, St) -> case get_payment_flow(get_payment(St)) of ?invoice_payment_flow_instant() -> @@ -1263,8 +1344,8 @@ throw_invalid_request(Why) -> -spec merge_change(change(), st() | undefined) -> st(). -merge_change(Event, undefined) -> - merge_change(Event, #st{}); +merge_change(Change, undefined) -> + merge_change(Change, #st{}); merge_change(?payment_started(Payment, RiskScore, Route, Cashflow), St) -> St#st{ @@ -1302,9 +1383,12 @@ merge_change(?adjustment_ev(ID, Event), St) -> _ -> St1 end; +merge_change(?receipt_ev(ID, Event, EventID), St) -> + Receipt = merge_receipt_change(Event, #receipt{last_event_id = EventID}), + St#st{receipt = Receipt, activity = {receipt, ID}}; merge_change(?session_ev(Target, ?session_started()), St) -> % FIXME why the hell dedicated handling - set_session(Target, create_session(Target, get_trx(St)), St#st{target = Target}); + set_session(Target, create_session(Target, get_trx(St)), St#st{activity = payment, target = Target}); merge_change(?session_ev(Target, Event), St) -> Session = merge_session_change(Event, get_session(Target, St)), St1 = set_session(Target, Session, St), @@ -1334,6 +1418,35 @@ merge_adjustment_change(?adjustment_created(Adjustment), undefined) -> merge_adjustment_change(?adjustment_status_changed(Status), Adjustment) -> Adjustment#domain_InvoicePaymentAdjustment{status = Status}. +merge_receipt_change(?receipt_created(), Receipt) -> + Receipt#receipt{status = created}; +merge_receipt_change(?receipt_registered(), Receipt) -> + Receipt#receipt{status = registered}; +merge_receipt_change(?receipt_failed(Failure), Receipt) -> + Receipt#receipt{status = {failed, Failure}}. + +get_receipt_changes(ReceiptID, LastEventID) -> + EventRange = construct_event_range(LastEventID), + hg_payment_cashreg:get_changes(ReceiptID, EventRange). + +get_receipt_status(#st{receipt = undefined}) -> + undefined; +get_receipt_status(#st{receipt = Receipt}) -> + Receipt#receipt.status. + +get_receipt_last_event_id(#st{receipt = undefined}) -> + undefined; +get_receipt_last_event_id(#st{receipt = Receipt}) -> + Receipt#receipt.last_event_id. + +get_receipt_id(#st{activity = {receipt, ID}}) -> + ID; +get_receipt_id(#st{activity = _}) -> + undefined. + +construct_event_range(LastEventID) -> + #payproc_EventRange{'after' = LastEventID, limit = ?RECEIPT_EVENTS_LIMIT}. + get_cashflow(#st{cash_flow = FinalCashflow}) -> FinalCashflow. @@ -1633,6 +1746,13 @@ marshal(change, ?refund_ev(RefundID, Payload)) -> <<"id">> => marshal(str, RefundID), <<"payload">> => marshal(refund_change, Payload) }]; +marshal(change, ?receipt_ev(ReceiptID, Payload, EventID)) -> + [2, #{ + <<"change">> => <<"receipt">>, + <<"id">> => marshal(str, ReceiptID), + <<"payload">> => marshal(receipt_change, Payload), + <<"event_id">> => marshal(int, EventID) + }]; %% Payment @@ -1729,6 +1849,15 @@ marshal(refund_change, ?refund_status_changed(Status)) -> marshal(refund_change, ?session_ev(_Target, Payload)) -> [2, [<<"session">>, marshal(session_change, Payload)]]; +%% Receipt change + +marshal(receipt_change, ?receipt_created()) -> + [1, <<"created">>]; +marshal(receipt_change, ?receipt_registered()) -> + [1, <<"registered">>]; +marshal(receipt_change, ?receipt_failed(Failure)) -> + [1, [<<"failed">>, marshal(failure, Failure)]]; + %% Adjustment marshal(adjustment, #domain_InvoicePaymentAdjustment{} = Adjustment) -> @@ -1906,6 +2035,13 @@ unmarshal(change, [2, #{ <<"payload">> := Payload }]) -> ?refund_ev(unmarshal(str, RefundID), unmarshal(refund_change, Payload)); +unmarshal(change, [2, #{ + <<"change">> := <<"receipt">>, + <<"id">> := ReceiptID, + <<"payload">> := Payload, + <<"event_id">> := EventID +}]) -> + ?receipt_ev(unmarshal(str, ReceiptID), unmarshal(receipt_change, Payload), unmarshal(int, EventID)); unmarshal(change, [1, ?legacy_payment_started(Payment, RiskScore, Route, Cashflow)]) -> ?payment_started( @@ -2072,6 +2208,16 @@ unmarshal(refund_change, [2, [<<"status">>, Status]]) -> unmarshal(refund_change, [2, [<<"session">>, Payload]]) -> ?session_ev(?refunded(), unmarshal(session_change, Payload)); + +%% Receipt change + +unmarshal(receipt_change, [1, <<"created">>]) -> + ?receipt_created(); +unmarshal(receipt_change, [1, <<"registered">>]) -> + ?receipt_registered(); +unmarshal(receipt_change, [1, [<<"failed">>, Failure]]) -> + ?receipt_failed(unmarshal(failure, Failure)); + %% Adjustment unmarshal(adjustment, #{ diff --git a/apps/hellgate/src/hg_payment_cashreg.erl b/apps/hellgate/src/hg_payment_cashreg.erl new file mode 100644 index 000000000..e7fd19aff --- /dev/null +++ b/apps/hellgate/src/hg_payment_cashreg.erl @@ -0,0 +1,229 @@ +-module(hg_payment_cashreg). + +-include_lib("include/cashreg_events.hrl"). +-include_lib("include/payment_events.hrl"). +-include_lib("dmsl/include/dmsl_domain_thrift.hrl"). +-include_lib("dmsl/include/dmsl_payment_processing_thrift.hrl"). +-include_lib("cashreg_proto/include/cashreg_proto_main_thrift.hrl"). +-include_lib("cashreg_proto/include/cashreg_proto_adapter_provider_thrift.hrl"). +-include_lib("cashreg_proto/include/cashreg_proto_processing_thrift.hrl"). + +-export([register_receipt/4]). +-export([get_changes/2]). + +-type party() :: dmsl_domain_thrift:'Party'(). +-type invoice() :: dmsl_domain_thrift:'Invoice'(). +-type payment() :: dmsl_domain_thrift:'InvoicePayment'(). +-type change() :: dmsl_payment_processing_thrift:'InvoicePaymentReceiptChange'(). +-type event_range() :: dmsl_payment_processing_thrift:'EventRange'(). +-type receipt_id() :: cashreg_proto_main_thrift:'ReceiptID'(). +-type revision() :: pos_integer(). + +-spec register_receipt(party(), invoice(), payment(), revision()) -> + receipt_id(). +register_receipt(Party, Invoice, Payment, Revision) -> + ReceiptParams = construct_receipt_params(Party, Invoice, Payment, Revision), + Adapter = construct_adapter(Party, Invoice, Revision), + create_receipt(ReceiptParams, Adapter). + +-spec get_changes(receipt_id(), event_range()) -> + [change()]. +get_changes(ReceiptID, EventRange) -> + CashregEventRange = construct_event_range(EventRange), + Events = get_receipt_events(ReceiptID, CashregEventRange), + construct_payment_changes(Events). + +construct_receipt_params(Party, Invoice, Payment, Revision) -> + #cashreg_main_ReceiptParams{ + party = construct_party(Party, Invoice), + operation = construct_operation(Payment), + purchase = construct_purchase(Invoice, Revision), + payment = construct_payment(Payment, Revision) + }. + +construct_party(Party, Invoice) -> + Shop = hg_party:get_shop(Invoice#domain_Invoice.shop_id, Party), + Contract = hg_party:get_contract(Shop#domain_Shop.contract_id, Party), + RussianLegalEntity = get_russian_legal_entity(Contract), + #cashreg_main_Party{ + registered_name = RussianLegalEntity#domain_RussianLegalEntity.registered_name, + registered_number = RussianLegalEntity#domain_RussianLegalEntity.registered_number, + inn = RussianLegalEntity#domain_RussianLegalEntity.inn, + actual_address = RussianLegalEntity#domain_RussianLegalEntity.actual_address, + tax_system = get_tax_system(Shop#domain_Shop.cash_register), + shop = construct_shop(Shop) + }. + +get_tax_system(undefined) -> + undefined; +get_tax_system(#domain_ShopCashRegister{tax_system = TaxSystem}) -> + TaxSystem. + +get_russian_legal_entity( + #domain_Contract{ + contractor = {legal_entity, {russian_legal_entity, RussianLegalEntity}} + } +) -> + RussianLegalEntity. + +construct_shop(#domain_Shop{ + details = #domain_ShopDetails{name = Name, description = Description}, + location = Location +}) -> + #cashreg_main_Shop{ + name = Name, + description = Description, + location = Location + }. + +construct_operation(_) -> + sell. + +construct_purchase(#domain_Invoice{ + details = #domain_InvoiceDetails{ + cart = #domain_InvoiceCart{ + lines = Lines + } + } +}, Revision) -> + #cashreg_main_Purchase{ + lines = [construct_purchase_line(Line, Revision) || Line <- Lines] + }. + +construct_purchase_line(#domain_InvoiceLine{ + product = Product, + quantity = Quantity, + price = Price, + tax = Tax +}, Revision) -> + #cashreg_main_PurchaseLine{ + product = Product, + quantity = Quantity, + price = construct_cash(Price, Revision), + tax = Tax + }. + +construct_cash(#domain_Cash{ + amount = Amount, + currency = CurrencyRef +}, Revision) -> + Currency = hg_domain:get(Revision, {currency, CurrencyRef}), + #cashreg_main_Cash{ + amount = Amount, + currency = construct_currency(Currency) + }. + +construct_currency(#domain_Currency{ + name = Name, + symbolic_code = CurrencySymbolicCode, + numeric_code = NumericCode, + exponent = Exponent +}) -> + #cashreg_main_Currency{ + name = Name, + symbolic_code = CurrencySymbolicCode, + numeric_code = NumericCode, + exponent = Exponent + }. + +construct_payment(#domain_InvoicePayment{ + payer = Payer, + cost = Cash +}, Revision) -> + #cashreg_main_Payment{ + payment_method = construct_payment_method(Payer), + cash = construct_cash(Cash, Revision) + }. + +construct_payment_method( + {payment_resource, #domain_PaymentResourcePayer{ + resource = #domain_DisposablePaymentResource{ + payment_tool = {bank_card, _} + } + }} +) -> + bank_card; +construct_payment_method( + {customer, #domain_CustomerPayer{ + payment_tool = {bank_card, _} + }} +) -> + bank_card. + +construct_event_range(#payproc_EventRange{ + 'after' = LastEventID, + limit = Limit +}) -> + #cashreg_proc_EventRange{ + 'after' = LastEventID, + limit = Limit + }. + +construct_payment_changes(ReceiptEvents) -> + lists:flatmap( + fun(#cashreg_proc_ReceiptEvent{id = ID, source = ReceiptID, payload = Changes}) -> + lists:foldl( + fun(Change, Acc) -> + Acc ++ construct_payment_change(ID, ReceiptID, Change) + end, + [], + Changes + ) + end, + ReceiptEvents + ). + +construct_payment_change(EventID, ReceiptID, ?cashreg_receipt_created(_, _)) -> + [?receipt_ev(ReceiptID, ?receipt_created(), EventID)]; +construct_payment_change(EventID, ReceiptID, ?cashreg_receipt_registered(_)) -> + [?receipt_ev(ReceiptID, ?receipt_registered(), EventID)]; +construct_payment_change(EventID, ReceiptID, ?cashreg_receipt_failed(Error)) -> + Failure = make_external_failure(Error), + [?receipt_ev(ReceiptID, ?receipt_failed(Failure), EventID)]; +construct_payment_change(_, _, ?cashreg_receipt_session_changed(_)) -> + []. + +make_external_failure({receipt_registration_failed, #cashreg_main_ReceiptRegistrationFailed{ + reason = #cashreg_main_ExternalFailure{description = Description} +}}) -> + Code = <<"receipt_registration_failed">>, + {external_failure, #domain_ExternalFailure{code = Code, description = Description}}. + +create_receipt(ReceiptParams, Adapter) -> + case issue_receipt_call('CreateReceipt', [ReceiptParams, Adapter]) of + {ok, Receipt} -> + Receipt#cashreg_main_Receipt.id; + Error -> + Error + end. + +get_receipt_events(ReceiptID, EventRange) -> + case issue_receipt_call('GetReceiptEvents', [ReceiptID, EventRange]) of + {ok, Events} -> + Events; + Error -> + Error + end. + +issue_receipt_call(Function, Args) -> + hg_woody_wrapper:call(cashreg, Function, Args). + +construct_adapter(Party, Invoice, Revision) -> + Shop = hg_party:get_shop(Invoice#domain_Invoice.shop_id, Party), + construct_adapter(Shop, Revision). + +construct_adapter(#domain_Shop{ + cash_register = #domain_ShopCashRegister{ + ref = CashRegisterRef, + options = CashRegOptions + } +}, Revision) -> + CashRegister = hg_domain:get(Revision, {cash_register, CashRegisterRef}), + Proxy = CashRegister#domain_CashRegister.proxy, + ProxyDef = hg_domain:get(Revision, {proxy, Proxy#domain_Proxy.ref}), + URL = ProxyDef#domain_ProxyDefinition.url, + ProxyOptions = ProxyDef#domain_ProxyDefinition.options, + #cashreg_adptprv_Adapter{ + url = URL, + options = maps:merge(ProxyOptions, CashRegOptions) + }. \ No newline at end of file diff --git a/apps/hellgate/test/hg_cashreg_tests_SUITE.erl b/apps/hellgate/test/hg_cashreg_tests_SUITE.erl new file mode 100644 index 000000000..42f32ae6c --- /dev/null +++ b/apps/hellgate/test/hg_cashreg_tests_SUITE.erl @@ -0,0 +1,945 @@ +-module(hg_cashreg_tests_SUITE). + +-include("hg_ct_domain.hrl"). +-include_lib("common_test/include/ct.hrl"). +-include_lib("dmsl/include/dmsl_payment_processing_thrift.hrl"). + +-export([all/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). +-export([init_per_testcase/2]). +-export([end_per_testcase/2]). + +-export([receipt_registration_success/1]). +-export([receipt_registration_failed/1]). +-export([receipt_registration_success_suspend/1]). + +%% + +-behaviour(supervisor). +-export([init/1]). + +-spec init([]) -> + {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. + +init([]) -> + {ok, {#{strategy => one_for_all, intensity => 1, period => 1}, []}}. + + +%% tests descriptions + +-type config() :: hg_ct_helper:config(). +-type test_case_name() :: hg_ct_helper:test_case_name(). +-type group_name() :: hg_ct_helper:group_name(). +-type test_return() :: _ | no_return(). + +cfg(Key, C) -> + hg_ct_helper:cfg(Key, C). + +-spec all() -> [test_case_name() | {group, group_name()}]. + +all() -> + [ + receipt_registration_success, + receipt_registration_failed, + receipt_registration_success_suspend + ]. + +-spec init_per_suite(config()) -> config(). + +init_per_suite(C) -> + CowboySpec1 = hg_dummy_provider:get_http_cowboy_spec(), + CowboySpec2 = hg_dummy_cashreg_provider:get_http_cowboy_spec(), + {Apps, Ret} = hg_ct_helper:start_apps( + [lager, woody, scoper, dmt_client, hellgate, {cowboy, CowboySpec1}, {cowboy, CowboySpec2}] + ), + ok = hg_domain:insert(construct_domain_fixture()), + RootUrl = maps:get(hellgate_root_url, Ret), + PartyID = hg_utils:unique_id(), + PartyClient = hg_client_party:start(PartyID, hg_ct_helper:create_client(RootUrl, PartyID)), + CustomerClient = hg_client_customer:start(hg_ct_helper:create_client(RootUrl, PartyID)), + ShopID = hg_ct_helper:create_party_and_shop(PartyClient), + {ok, SupPid} = supervisor:start_link(?MODULE, []), + _ = unlink(SupPid), + NewC = [ + {party_id, PartyID}, + {party_client, PartyClient}, + {customer_client, CustomerClient}, + {shop_id, ShopID}, + {root_url, RootUrl}, + {apps, Apps}, + {test_sup, SupPid} + | C + ], + ok = start_proxies([ + {hg_dummy_provider, 1, NewC}, + {hg_dummy_inspector, 2, NewC}, + {hg_dummy_cashreg_provider, 3, NewC} + ]), + NewC. + +-spec end_per_suite(config()) -> _. + +end_per_suite(C) -> + ok = hg_domain:cleanup(), + [application:stop(App) || App <- cfg(apps, C)], + exit(cfg(test_sup, C), shutdown). + +%% tests +-include("invoice_events.hrl"). +-include("payment_events.hrl"). + +-define(invoice(ID), #domain_Invoice{id = ID}). +-define(payment(ID), #domain_InvoicePayment{id = ID}). +-define(invoice_state(Invoice), #payproc_Invoice{invoice = Invoice}). +-define(invoice_state(Invoice, Payments), #payproc_Invoice{invoice = Invoice, payments = Payments}). +-define(payment_state(Payment), #payproc_InvoicePayment{payment = Payment}). +-define(invoice_w_status(Status), #domain_Invoice{status = Status}). +-define(payment_w_status(Status), #domain_InvoicePayment{status = Status}). +-define(payment_w_status(ID, Status), #domain_InvoicePayment{id = ID, status = Status}). +-define(trx_info(ID), #domain_TransactionInfo{id = ID}). + +-spec init_per_testcase(test_case_name(), config()) -> config(). + +init_per_testcase(_Name, C) -> + init_per_testcase(C). + +init_per_testcase(C) -> + ApiClient = hg_ct_helper:create_client(cfg(root_url, C), cfg(party_id, C)), + Client = hg_client_invoicing:start_link(ApiClient), + ClientTpl = hg_client_invoice_templating:start_link(ApiClient), + [{client, Client}, {client_tpl, ClientTpl} | C]. + +-spec end_per_testcase(test_case_name(), config()) -> config(). + +end_per_testcase(_Name, C) -> + _ = case cfg(original_domain_revision, C) of + Revision when is_integer(Revision) -> + ok = hg_domain:reset(Revision); + undefined -> + ok + end. + +-spec receipt_registration_success(config()) -> test_return(). + +receipt_registration_success(C) -> + Client = cfg(client, C), + PartyClient = cfg(party_client, C), + PartyID = cfg(party_id, C), + ShopCashRegister = #domain_ShopCashRegister{ + ref = ?cashreg(1), + tax_system = osn, + options = #{<<"adapter_state">> => <<"sleeping">>} + }, + ShopID = hg_ct_helper:create_battle_ready_shop_with_cashreg(?cat(2), ?tmpl(2), PartyClient, ShopCashRegister), + InvoiceParams = make_invoice_params(PartyID, ShopID), + InvoiceID = create_invoice(InvoiceParams, Client), + + [?invoice_created(?invoice_w_status(?invoice_unpaid()))] = next_event(InvoiceID, Client), + PaymentParams = make_payment_params(), + PaymentID = process_payment(InvoiceID, PaymentParams, Client), + PaymentID = await_receipt_created(InvoiceID, PaymentID, Client), + PaymentID = await_receipt_registered(InvoiceID, PaymentID, Client), + PaymentID = await_payment_capture(InvoiceID, PaymentID, Client), + ?invoice_state( + ?invoice_w_status(?invoice_paid()), + [?payment_state(?payment_w_status(PaymentID, ?captured()))] + ) = hg_client_invoicing:get(InvoiceID, Client). + +-spec receipt_registration_failed(config()) -> test_return(). + +receipt_registration_failed(C) -> + Client = cfg(client, C), + PartyClient = cfg(party_client, C), + PartyID = cfg(party_id, C), + ShopCashRegister = #domain_ShopCashRegister{ + ref = ?cashreg(1), + tax_system = osn, + options = #{<<"adapter_state">> => <<"finishing_failure">>} + }, + ShopID = hg_ct_helper:create_battle_ready_shop_with_cashreg(?cat(2), ?tmpl(2), PartyClient, ShopCashRegister), + InvoiceParams = make_invoice_params(PartyID, ShopID), + InvoiceID = create_invoice(InvoiceParams, Client), + + [?invoice_created(?invoice_w_status(?invoice_unpaid()))] = next_event(InvoiceID, Client), + PaymentParams = make_payment_params(), + PaymentID = process_payment(InvoiceID, PaymentParams, Client), + PaymentID = await_receipt_created(InvoiceID, PaymentID, Client), + PaymentID = await_receipt_failed(InvoiceID, PaymentID, Client), + [ + ?payment_ev(PaymentID, ?session_ev(?cancelled_with_reason(Reason), ?session_started())) + ] = next_event(InvoiceID, Client), + [ + ?payment_ev(PaymentID, ?session_ev(?cancelled_with_reason(Reason), ?session_finished(?session_succeeded()))), + ?payment_ev(PaymentID, ?payment_status_changed(?failed(_))) + ] = next_event(InvoiceID, Client). + +-spec receipt_registration_success_suspend(config()) -> test_return(). + +receipt_registration_success_suspend(C) -> + Client = cfg(client, C), + PartyClient = cfg(party_client, C), + PartyID = cfg(party_id, C), + Tag = hg_utils:unique_id(), + ShopCashRegister = #domain_ShopCashRegister{ + ref = ?cashreg(1), + tax_system = osn, + options = #{<<"adapter_state">> => <<"suspending">>, <<"tag">> => Tag, <<"callback">> => <<"ok">>} + }, + ShopID = hg_ct_helper:create_battle_ready_shop_with_cashreg(?cat(2), ?tmpl(2), PartyClient, ShopCashRegister), + InvoiceParams = make_invoice_params(PartyID, ShopID), + InvoiceID = create_invoice(InvoiceParams, Client), + + [?invoice_created(?invoice_w_status(?invoice_unpaid()))] = next_event(InvoiceID, Client), + PaymentParams = make_payment_params(), + PaymentID = process_payment(InvoiceID, PaymentParams, Client), + PaymentID = await_receipt_created(InvoiceID, PaymentID, Client), + timer:sleep(1000), + _ = assert_success_post_request({hg_dummy_cashreg_provider:get_callback_url(), #{<<"tag">> => Tag}}), + PaymentID = await_receipt_registered(InvoiceID, PaymentID, Client), + PaymentID = await_payment_capture(InvoiceID, PaymentID, Client), + ?invoice_state( + ?invoice_w_status(?invoice_paid()), + [?payment_state(?payment_w_status(PaymentID, ?captured()))] + ) = hg_client_invoicing:get(InvoiceID, Client). +%% + +next_event(InvoiceID, Client) -> + next_event(InvoiceID, 5000, Client). + +next_event(InvoiceID, Timeout, Client) -> + case hg_client_invoicing:pull_event(InvoiceID, Timeout, Client) of + {ok, ?invoice_ev(Changes)} -> + case filter_changes(Changes) of + L when length(L) > 0 -> + L; + [] -> + next_event(InvoiceID, Timeout, Client) + end; + Result -> + Result + end. + +filter_changes(Changes) -> + lists:filtermap(fun filter_change/1, Changes). + +filter_change(?payment_ev(_, C)) -> + filter_change(C); +filter_change(?refund_ev(_, C)) -> + filter_change(C); +filter_change(?session_ev(_, ?proxy_st_changed(_))) -> + false; +filter_change(?session_ev(_, ?session_suspended(_))) -> + false; +filter_change(?session_ev(_, ?session_activated())) -> + false; +filter_change(_) -> + true. + +%% + +start_service_handler(Module, C, HandlerOpts) -> + start_service_handler(Module, Module, C, HandlerOpts). + +start_service_handler(Name, Module, C, HandlerOpts) -> + IP = "127.0.0.1", + Port = get_random_port(), + Opts = maps:merge(HandlerOpts, #{hellgate_root_url => cfg(root_url, C)}), + ChildSpec = hg_test_proxy:get_child_spec(Name, Module, IP, Port, Opts), + {ok, _} = supervisor:start_child(cfg(test_sup, C), ChildSpec), + hg_test_proxy:get_url(Module, IP, Port). + +start_proxies(Proxies) -> + setup_proxies(lists:map( + fun + Mapper({Module, ProxyID, Context}) -> + Mapper({Module, ProxyID, #{}, Context}); + Mapper({Module, ProxyID, ProxyOpts, Context}) -> + construct_proxy(ProxyID, start_service_handler(Module, Context, #{}), ProxyOpts) + end, + Proxies + )). + +setup_proxies(Proxies) -> + ok = hg_domain:upsert(Proxies). + +get_random_port() -> + rand:uniform(32768) + 32767. + +construct_proxy(ID, Url, Options) -> + {proxy, #domain_ProxyObject{ + ref = ?prx(ID), + data = #domain_ProxyDefinition{ + name = Url, + description = Url, + url = Url, + options = Options + } + }}. + +%% + +make_invoice_params(PartyID, ShopID) -> + #payproc_InvoiceParams{ + party_id = PartyID, + shop_id = ShopID, + details = #domain_InvoiceDetails{ + product = <<"Some product">>, + cart = #domain_InvoiceCart{ + lines = [ + #domain_InvoiceLine{ + product = <<"Some product">>, + quantity = 10, + price = hg_ct_helper:make_cash(420000, <<"RUB">>), + tax = {vat, vat0}, + metadata = #{} + } + ] + } + }, + due = hg_datetime:format_ts(make_due_date(10)), + cost = hg_ct_helper:make_cash(42000, <<"RUB">>), + context = hg_ct_helper:make_invoice_context() + }. + +make_payment_params() -> + make_payment_params(instant). + +make_payment_params(FlowType) -> + {PaymentTool, Session} = hg_ct_helper:make_simple_payment_tool(), + make_payment_params(PaymentTool, Session, FlowType). + +make_payment_params(PaymentTool, Session, FlowType) -> + Flow = case FlowType of + instant -> + {instant, #payproc_InvoicePaymentParamsFlowInstant{}}; + {hold, OnHoldExpiration} -> + {hold, #payproc_InvoicePaymentParamsFlowHold{on_hold_expiration = OnHoldExpiration}} + end, + #payproc_InvoicePaymentParams{ + payer = {payment_resource, #payproc_PaymentResourcePayerParams{ + resource = #domain_DisposablePaymentResource{ + payment_tool = PaymentTool, + payment_session_id = Session, + client_info = #domain_ClientInfo{} + }, + contact_info = #domain_ContactInfo{} + }}, + flow = Flow + }. + +make_due_date(LifetimeSeconds) -> + genlib_time:unow() + LifetimeSeconds. + +create_invoice(InvoiceParams, Client) -> + ?invoice_state(?invoice(InvoiceID)) = hg_client_invoicing:create(InvoiceParams, Client), + InvoiceID. + +start_payment(InvoiceID, PaymentParams, Client) -> + ?payment_state(?payment(PaymentID)) = hg_client_invoicing:start_payment(InvoiceID, PaymentParams, Client), + [ + ?payment_ev(PaymentID, ?payment_started(?payment_w_status(?pending()))), + ?payment_ev(PaymentID, ?session_ev(?processed(), ?session_started())) + ] = next_event(InvoiceID, Client), + PaymentID. + +process_payment(InvoiceID, PaymentParams, Client) -> + PaymentID = start_payment(InvoiceID, PaymentParams, Client), + PaymentID = await_payment_process_finish(InvoiceID, PaymentID, Client). + +await_payment_process_finish(InvoiceID, PaymentID, Client) -> + [ + ?payment_ev(PaymentID, ?session_ev(?processed(), ?trx_bound(?trx_info(_)))), + ?payment_ev(PaymentID, ?session_ev(?processed(), ?session_finished(?session_succeeded()))), + ?payment_ev(PaymentID, ?payment_status_changed(?processed())) + ] = next_event(InvoiceID, Client), + PaymentID. + +await_receipt_created(InvoiceID, PaymentID, Client) -> + [ + ?payment_ev(PaymentID, ?receipt_ev(_, ?receipt_created(), _)) + ] = next_event(InvoiceID, Client), + PaymentID. + +await_receipt_registered(InvoiceID, PaymentID, Client) -> + [ + ?payment_ev(PaymentID, ?receipt_ev(_, ?receipt_registered(), _)) + ] = next_event(InvoiceID, Client), + PaymentID. + +await_receipt_failed(InvoiceID, PaymentID, Client) -> + [ + ?payment_ev(PaymentID, ?receipt_ev(_, ?receipt_failed(_), _)) + ] = next_event(InvoiceID, Client), + PaymentID. + +await_payment_capture(InvoiceID, PaymentID, Client) -> + await_payment_capture(InvoiceID, PaymentID, undefined, Client). + +await_payment_capture(InvoiceID, PaymentID, Reason, Client) -> + [ + ?payment_ev(PaymentID, ?session_ev(?captured_with_reason(Reason), ?session_started())) + ] = next_event(InvoiceID, Client), + await_payment_capture_finish(InvoiceID, PaymentID, Reason, Client). + +await_payment_capture_finish(InvoiceID, PaymentID, Reason, Client) -> + [ + ?payment_ev(PaymentID, ?session_ev(?captured_with_reason(Reason), ?session_finished(?session_succeeded()))), + ?payment_ev(PaymentID, ?payment_status_changed(?captured_with_reason(Reason))), + ?invoice_status_changed(?invoice_paid()) + ] = next_event(InvoiceID, Client), + PaymentID. + +assert_success_post_request(Req) -> + {ok, 200, _RespHeaders, _ClientRef} = post_request(Req). + +% assert_invalid_post_request(Req) -> +% {ok, 400, _RespHeaders, _ClientRef} = post_request(Req). + +post_request({URL, Form}) -> + Method = post, + Headers = [], + Body = {form, maps:to_list(Form)}, + hackney:request(Method, URL, Headers, Body). + +construct_domain_fixture() -> + TestTermSet = #domain_TermSet{ + payments = #domain_PaymentsServiceTerms{ + currencies = {value, ?ordset([ + ?cur(<<"RUB">>) + ])}, + categories = {value, ?ordset([ + ?cat(1) + ])}, + payment_methods = {decisions, [ + #domain_PaymentMethodDecision{ + if_ = ?partycond(<<"DEPRIVED ONE">>, {shop_is, <<"TESTSHOP">>}), + then_ = {value, ordsets:new()} + }, + #domain_PaymentMethodDecision{ + if_ = {constant, true}, + then_ = {value, ?ordset([ + ?pmt(bank_card, visa), + ?pmt(bank_card, mastercard), + ?pmt(payment_terminal, euroset) + ])} + } + ]}, + cash_limit = {decisions, [ + #domain_CashLimitDecision{ + if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, + then_ = {value, ?cashrng( + {inclusive, ?cash( 1000, <<"RUB">>)}, + {exclusive, ?cash(420000000, <<"RUB">>)} + )} + } + ]}, + fees = {decisions, [ + #domain_CashFlowDecision{ + if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, + then_ = {value, [ + ?cfpost( + {merchant, settlement}, + {system, settlement}, + ?share(45, 1000, payment_amount) + ) + ]} + } + ]}, + holds = #domain_PaymentHoldsServiceTerms{ + payment_methods = {value, ?ordset([ + ?pmt(bank_card, visa), + ?pmt(bank_card, mastercard) + ])}, + lifetime = {decisions, [ + #domain_HoldLifetimeDecision{ + if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, + then_ = {value, #domain_HoldLifetime{seconds = 3}} + } + ]} + }, + refunds = #domain_PaymentRefundsServiceTerms{ + payment_methods = {value, ?ordset([ + ?pmt(bank_card, visa), + ?pmt(bank_card, mastercard) + ])}, + fees = {value, [ + ?cfpost( + {merchant, settlement}, + {system, settlement}, + ?fixed(100, <<"RUB">>) + ) + ]} + } + }, + recurrent_paytools = #domain_RecurrentPaytoolsServiceTerms{ + payment_methods = {value, ordsets:from_list([ + ?pmt(bank_card, visa), + ?pmt(bank_card, mastercard) + ])} + } + }, + DefaultTermSet = #domain_TermSet{ + payments = #domain_PaymentsServiceTerms{ + currencies = {value, ?ordset([ + ?cur(<<"RUB">>), + ?cur(<<"USD">>) + ])}, + categories = {value, ?ordset([ + ?cat(2), + ?cat(3) + ])}, + payment_methods = {value, ?ordset([ + ?pmt(bank_card, visa), + ?pmt(bank_card, mastercard) + ])}, + cash_limit = {decisions, [ + #domain_CashLimitDecision{ + if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, + then_ = {value, ?cashrng( + {inclusive, ?cash( 1000, <<"RUB">>)}, + {exclusive, ?cash( 4200000, <<"RUB">>)} + )} + }, + #domain_CashLimitDecision{ + if_ = {condition, {currency_is, ?cur(<<"USD">>)}}, + then_ = {value, ?cashrng( + {inclusive, ?cash( 200, <<"USD">>)}, + {exclusive, ?cash( 313370, <<"USD">>)} + )} + } + ]}, + fees = {decisions, [ + #domain_CashFlowDecision{ + if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, + then_ = {value, [ + ?cfpost( + {merchant, settlement}, + {system, settlement}, + ?share(45, 1000, payment_amount) + ) + ]} + }, + #domain_CashFlowDecision{ + if_ = {condition, {currency_is, ?cur(<<"USD">>)}}, + then_ = {value, [ + ?cfpost( + {merchant, settlement}, + {system, settlement}, + ?share(65, 1000, payment_amount) + ) + ]} + } + ]}, + holds = #domain_PaymentHoldsServiceTerms{ + payment_methods = {value, ?ordset([ + ?pmt(bank_card, visa), + ?pmt(bank_card, mastercard) + ])}, + lifetime = {decisions, [ + #domain_HoldLifetimeDecision{ + if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, + then_ = {value, #domain_HoldLifetime{seconds = 3}} + } + ]} + }, + refunds = #domain_PaymentRefundsServiceTerms{ + payment_methods = {value, ?ordset([ + ?pmt(bank_card, visa), + ?pmt(bank_card, mastercard) + ])}, + fees = {value, [ + ]} + } + } + }, + [ + hg_ct_fixture:construct_currency(?cur(<<"RUB">>)), + hg_ct_fixture:construct_currency(?cur(<<"USD">>)), + + hg_ct_fixture:construct_category(?cat(1), <<"Test category">>, test), + hg_ct_fixture:construct_category(?cat(2), <<"Generic Store">>, live), + hg_ct_fixture:construct_category(?cat(3), <<"Guns & Booze">>, live), + + hg_ct_fixture:construct_payment_method(?pmt(bank_card, visa)), + hg_ct_fixture:construct_payment_method(?pmt(bank_card, mastercard)), + hg_ct_fixture:construct_payment_method(?pmt(payment_terminal, euroset)), + + hg_ct_fixture:construct_proxy(?prx(1), <<"Dummy proxy">>), + hg_ct_fixture:construct_proxy(?prx(2), <<"Inspector proxy">>), + hg_ct_fixture:construct_proxy(?prx(3), <<"Cashreg proxy">>), + + hg_ct_fixture:construct_inspector(?insp(1), <<"Rejector">>, ?prx(2), #{<<"risk_score">> => <<"low">>}), + hg_ct_fixture:construct_inspector(?insp(2), <<"Skipper">>, ?prx(2), #{<<"risk_score">> => <<"high">>}), + hg_ct_fixture:construct_inspector(?insp(3), <<"Fatalist">>, ?prx(2), #{<<"risk_score">> => <<"fatal">>}), + + hg_ct_fixture:construct_contract_template(?tmpl(1), ?trms(1)), + hg_ct_fixture:construct_contract_template(?tmpl(2), ?trms(2)), + + hg_ct_fixture:construct_system_account_set(?sas(1)), + hg_ct_fixture:construct_external_account_set(?eas(1)), + hg_ct_fixture:construct_external_account_set(?eas(2), <<"Assist">>, ?cur(<<"RUB">>)), + + hg_ct_fixture:construct_cashreg(?cashreg(1), <<"Test cash register">>, #domain_Proxy{ + ref = ?prx(3), + additional = #{ + <<"override">> => <<"drovider">> + } + }), + + {globals, #domain_GlobalsObject{ + ref = #domain_GlobalsRef{}, + data = #domain_Globals{ + party_prototype = #domain_PartyPrototypeRef{id = 42}, + providers = {value, ?ordset([ + ?prv(1), + ?prv(2), + ?prv(3) + ])}, + system_account_set = {value, ?sas(1)}, + external_account_set = {decisions, [ + #domain_ExternalAccountSetDecision{ + if_ = {condition, {party, #domain_PartyCondition{ + id = <<"LGBT">> + }}}, + then_ = {value, ?eas(2)} + }, + #domain_ExternalAccountSetDecision{ + if_ = {constant, true}, + then_ = {value, ?eas(1)} + } + ]}, + default_contract_template = ?tmpl(2), + inspector = {decisions, [ + #domain_InspectorDecision{ + if_ = {condition, {currency_is, ?cur(<<"RUB">>)}}, + then_ = {decisions, [ + #domain_InspectorDecision{ + if_ = {condition, {category_is, ?cat(3)}}, + then_ = {value, ?insp(2)} + }, + #domain_InspectorDecision{ + if_ = {condition, {cost_in, ?cashrng( + {inclusive, ?cash( 0, <<"RUB">>)}, + {exclusive, ?cash( 500000, <<"RUB">>)} + )}}, + then_ = {value, ?insp(1)} + }, + #domain_InspectorDecision{ + if_ = {condition, {cost_in, ?cashrng( + {inclusive, ?cash( 500000, <<"RUB">>)}, + {exclusive, ?cash(100000000, <<"RUB">>)} + )}}, + then_ = {value, ?insp(2)} + }, + #domain_InspectorDecision{ + if_ = {condition, {cost_in, ?cashrng( + {inclusive, ?cash( 100000000, <<"RUB">>)}, + {exclusive, ?cash(1000000000, <<"RUB">>)} + )}}, + then_ = {value, ?insp(3)} + } + ]} + } + ]} + } + }}, + {party_prototype, #domain_PartyPrototypeObject{ + ref = #domain_PartyPrototypeRef{id = 42}, + data = #domain_PartyPrototype{ + shop = #domain_ShopPrototype{ + shop_id = <<"TESTSHOP">>, + category = ?cat(1), + currency = ?cur(<<"RUB">>), + details = #domain_ShopDetails{ + name = <<"SUPER DEFAULT SHOP">> + }, + location = {url, <<"">>} + }, + contract = #domain_ContractPrototype{ + contract_id = <<"TESTCONTRACT">>, + test_contract_template = ?tmpl(1), + payout_tool = #domain_PayoutToolPrototype{ + payout_tool_id = <<"TESTPAYOUTTOOL">>, + payout_tool_info = {bank_account, #domain_BankAccount{ + account = <<"">>, + bank_name = <<"">>, + bank_post_account = <<"">>, + bank_bik = <<"">> + }}, + payout_tool_currency = ?cur(<<"RUB">>) + } + } + } + }}, + {term_set_hierarchy, #domain_TermSetHierarchyObject{ + ref = ?trms(1), + data = #domain_TermSetHierarchy{ + term_sets = [#domain_TimedTermSet{ + action_time = #'TimestampInterval'{}, + terms = TestTermSet + }] + } + }}, + {term_set_hierarchy, #domain_TermSetHierarchyObject{ + ref = ?trms(2), + data = #domain_TermSetHierarchy{ + term_sets = [#domain_TimedTermSet{ + action_time = #'TimestampInterval'{}, + terms = DefaultTermSet + }] + } + }}, + {provider, #domain_ProviderObject{ + ref = ?prv(1), + data = #domain_Provider{ + name = <<"Brovider">>, + description = <<"A provider but bro">>, + terminal = {value, ?ordset([ + ?trm(1) + ])}, + proxy = #domain_Proxy{ + ref = ?prx(1), + additional = #{ + <<"override">> => <<"brovider">> + } + }, + abs_account = <<"1234567890">>, + accounts = hg_ct_fixture:construct_provider_account_set([?cur(<<"RUB">>)]), + payment_terms = #domain_PaymentsProvisionTerms{ + currencies = {value, ?ordset([ + ?cur(<<"RUB">>) + ])}, + categories = {value, ?ordset([ + ?cat(1) + ])}, + payment_methods = {value, ?ordset([ + ?pmt(bank_card, visa), + ?pmt(bank_card, mastercard) + ])}, + cash_limit = {value, ?cashrng( + {inclusive, ?cash( 1000, <<"RUB">>)}, + {exclusive, ?cash(1000000000, <<"RUB">>)} + )}, + cash_flow = {decisions, [ + #domain_CashFlowDecision{ + if_ = {condition, {payment_tool, {bank_card, {payment_system_is, visa}}}}, + then_ = {value, [ + ?cfpost( + {provider, settlement}, + {merchant, settlement}, + ?share(1, 1, payment_amount) + ), + ?cfpost( + {system, settlement}, + {provider, settlement}, + ?share(18, 1000, payment_amount) + ) + ]} + }, + #domain_CashFlowDecision{ + if_ = {condition, {payment_tool, {bank_card, {payment_system_is, mastercard}}}}, + then_ = {value, [ + ?cfpost( + {provider, settlement}, + {merchant, settlement}, + ?share(1, 1, payment_amount) + ), + ?cfpost( + {system, settlement}, + {provider, settlement}, + ?share(19, 1000, payment_amount) + ) + ]} + } + ]}, + holds = #domain_PaymentHoldsProvisionTerms{ + lifetime = {decisions, [ + #domain_HoldLifetimeDecision{ + if_ = {condition, {payment_tool, {bank_card, {payment_system_is, visa}}}}, + then_ = {value, ?hold_lifetime(5)} + } + ]} + }, + refunds = #domain_PaymentRefundsProvisionTerms{ + cash_flow = {value, [ + ?cfpost( + {merchant, settlement}, + {provider, settlement}, + ?share(1, 1, payment_amount) + ) + ]} + } + }, + recurrent_paytool_terms = #domain_RecurrentPaytoolsProvisionTerms{ + categories = {value, ?ordset([?cat(1)])}, + payment_methods = {value, ?ordset([ + ?pmt(bank_card, visa), + ?pmt(bank_card, mastercard) + ])}, + cash_value = {value, ?cash(1000, <<"RUB">>)} + } + } + }}, + {terminal, #domain_TerminalObject{ + ref = ?trm(1), + data = #domain_Terminal{ + name = <<"Brominal 1">>, + description = <<"Brominal 1">>, + risk_coverage = high + } + }}, + {provider, #domain_ProviderObject{ + ref = ?prv(2), + data = #domain_Provider{ + name = <<"Drovider">>, + description = <<"I'm out of ideas of what to write here">>, + terminal = {value, [?trm(6), ?trm(7)]}, + proxy = #domain_Proxy{ + ref = ?prx(1), + additional = #{ + <<"override">> => <<"drovider">> + } + }, + abs_account = <<"1234567890">>, + accounts = hg_ct_fixture:construct_provider_account_set([?cur(<<"RUB">>)]), + payment_terms = #domain_PaymentsProvisionTerms{ + currencies = {value, ?ordset([ + ?cur(<<"RUB">>) + ])}, + categories = {value, ?ordset([ + ?cat(2) + ])}, + payment_methods = {value, ?ordset([ + ?pmt(bank_card, visa), + ?pmt(bank_card, mastercard) + ])}, + cash_limit = {value, ?cashrng( + {inclusive, ?cash( 1000, <<"RUB">>)}, + {exclusive, ?cash(10000000, <<"RUB">>)} + )}, + cash_flow = {value, [ + ?cfpost( + {provider, settlement}, + {merchant, settlement}, + ?share(1, 1, payment_amount) + ), + ?cfpost( + {system, settlement}, + {provider, settlement}, + ?share(16, 1000, payment_amount) + ) + ]}, + refunds = #domain_PaymentRefundsProvisionTerms{ + cash_flow = {value, [ + ?cfpost( + {merchant, settlement}, + {provider, settlement}, + ?share(1, 1, payment_amount) + ) + ]} + } + } + } + }}, + {terminal, #domain_TerminalObject{ + ref = ?trm(6), + data = #domain_Terminal{ + name = <<"Drominal 1">>, + description = <<"Drominal 1">>, + risk_coverage = low, + terms = #domain_PaymentsProvisionTerms{ + currencies = {value, ?ordset([ + ?cur(<<"RUB">>) + ])}, + categories = {value, ?ordset([ + ?cat(2) + ])}, + payment_methods = {value, ?ordset([ + ?pmt(bank_card, visa) + ])}, + cash_limit = {value, ?cashrng( + {inclusive, ?cash( 1000, <<"RUB">>)}, + {exclusive, ?cash( 5000000, <<"RUB">>)} + )}, + cash_flow = {value, [ + ?cfpost( + {provider, settlement}, + {merchant, settlement}, + ?share(1, 1, payment_amount) + ), + ?cfpost( + {system, settlement}, + {provider, settlement}, + ?share(16, 1000, payment_amount) + ), + ?cfpost( + {system, settlement}, + {external, outcome}, + ?fixed(20, <<"RUB">>), + <<"Assist fee">> + ) + ]} + } + } + }}, + {terminal, #domain_TerminalObject{ + ref = ?trm(7), + data = #domain_Terminal{ + name = <<"Terminal 7">>, + description = <<"Terminal 7">>, + risk_coverage = high + } + }}, + {provider, #domain_ProviderObject{ + ref = ?prv(3), + data = #domain_Provider{ + name = <<"Crovider">>, + description = <<"Payment terminal provider">>, + terminal = {value, [?trm(10)]}, + proxy = #domain_Proxy{ + ref = ?prx(1), + additional = #{ + <<"override">> => <<"crovider">> + } + }, + abs_account = <<"0987654321">>, + accounts = hg_ct_fixture:construct_provider_account_set([?cur(<<"RUB">>)]), + payment_terms = #domain_PaymentsProvisionTerms{ + currencies = {value, ?ordset([ + ?cur(<<"RUB">>) + ])}, + categories = {value, ?ordset([ + ?cat(1) + ])}, + payment_methods = {value, ?ordset([ + ?pmt(payment_terminal, euroset) + ])}, + cash_limit = {value, ?cashrng( + {inclusive, ?cash( 1000, <<"RUB">>)}, + {exclusive, ?cash(10000000, <<"RUB">>)} + )}, + cash_flow = {value, [ + ?cfpost( + {provider, settlement}, + {merchant, settlement}, + ?share(1, 1, payment_amount) + ), + ?cfpost( + {system, settlement}, + {provider, settlement}, + ?share(21, 1000, payment_amount) + ) + ]} + } + } + }}, + {terminal, #domain_TerminalObject{ + ref = ?trm(10), + data = #domain_Terminal{ + name = <<"Payment Terminal Terminal">>, + description = <<"Euroset">>, + risk_coverage = low + } + }} + ]. diff --git a/apps/hellgate/test/hg_ct_domain.hrl b/apps/hellgate/test/hg_ct_domain.hrl index b8dbe9aaa..dde03506b 100644 --- a/apps/hellgate/test/hg_ct_domain.hrl +++ b/apps/hellgate/test/hg_ct_domain.hrl @@ -18,6 +18,7 @@ -define(eas(ID), #domain_ExternalAccountSetRef{id = ID}). -define(insp(ID), #domain_InspectorRef{id = ID}). -define(partyproto(ID), #domain_PartyPrototypeRef{id = ID}). +-define(cashreg(ID), #domain_CashRegisterRef{id = ID}). -define(cashrng(Lower, Upper), #domain_CashRange{lower = Lower, upper = Upper}). diff --git a/apps/hellgate/test/hg_ct_fixture.erl b/apps/hellgate/test/hg_ct_fixture.erl index 4ba929c75..1d8ed98e5 100644 --- a/apps/hellgate/test/hg_ct_fixture.erl +++ b/apps/hellgate/test/hg_ct_fixture.erl @@ -21,13 +21,15 @@ -export([construct_system_account_set/3]). -export([construct_external_account_set/1]). -export([construct_external_account_set/3]). +-export([construct_cashreg/3]). %% -type name() :: binary(). -type category() :: dmsl_domain_thrift:'CategoryRef'(). -type currency() :: dmsl_domain_thrift:'CurrencyRef'(). --type proxy() :: dmsl_domain_thrift:'ProxyRef'(). +-type proxy_ref() :: dmsl_domain_thrift:'ProxyRef'(). +-type proxy() :: dmsl_domain_thrift:'Proxy'(). -type inspector() :: dmsl_domain_thrift:'InspectorRef'(). -type template() :: dmsl_domain_thrift:'ContractTemplateRef'(). -type terms() :: dmsl_domain_thrift:'TermSetHierarchyRef'(). @@ -90,13 +92,13 @@ construct_payment_method(?pmt(_Type, Name) = Ref) -> } }}. --spec construct_proxy(proxy(), name()) -> +-spec construct_proxy(proxy_ref(), name()) -> {proxy, dmsl_domain_thrift:'ProxyObject'()}. construct_proxy(Ref, Name) -> construct_proxy(Ref, Name, #{}). --spec construct_proxy(proxy(), name(), Opts :: map()) -> +-spec construct_proxy(proxy_ref(), name(), Opts :: map()) -> {proxy, dmsl_domain_thrift:'ProxyObject'()}. construct_proxy(Ref, Name, Opts) -> @@ -110,13 +112,13 @@ construct_proxy(Ref, Name, Opts) -> } }}. --spec construct_inspector(inspector(), name(), proxy()) -> +-spec construct_inspector(inspector(), name(), proxy_ref()) -> {inspector, dmsl_domain_thrift:'InspectorObject'()}. construct_inspector(Ref, Name, ProxyRef) -> construct_inspector(Ref, Name, ProxyRef, #{}). --spec construct_inspector(inspector(), name(), proxy(), Additional :: map()) -> +-spec construct_inspector(inspector(), name(), proxy_ref(), Additional :: map()) -> {inspector, dmsl_domain_thrift:'InspectorObject'()}. construct_inspector(Ref, Name, ProxyRef, Additional) -> @@ -215,3 +217,15 @@ construct_external_account_set(Ref, Name, ?cur(CurrencyCode)) -> } }}. +-spec construct_cashreg(proxy_ref(), name(), proxy()) -> + {cash_register, dmsl_domain_thrift:'CashRegisterObject'()}. + +construct_cashreg(Ref, Name, Proxy) -> + {cash_register, #domain_CashRegisterObject{ + ref = Ref, + data = #domain_CashRegister{ + name = Name, + description = Name, + proxy = Proxy + } + }}. \ No newline at end of file diff --git a/apps/hellgate/test/hg_ct_helper.erl b/apps/hellgate/test/hg_ct_helper.erl index 8e0e8f123..7a291b5b0 100644 --- a/apps/hellgate/test/hg_ct_helper.erl +++ b/apps/hellgate/test/hg_ct_helper.erl @@ -11,6 +11,8 @@ -export([create_party_and_shop/1]). -export([create_battle_ready_shop/3]). +-export([create_battle_ready_shop_with_cashreg/4]). +-export([create_customer_w_binding/1]). -export([get_account/1]). -export([get_first_contract_id/1]). -export([get_first_battle_ready_contract_id/1]). @@ -133,7 +135,8 @@ start_app(hellgate = AppName) -> accounter => <<"http://shumway:8022/accounter">>, party_management => <<"http://hellgate:8022/v1/processing/partymgmt">>, customer_management => <<"http://hellgate:8022/v1/processing/customer_management">>, - recurrent_paytool => <<"http://hellgate:8022/v1/processing/recpaytool">> + recurrent_paytool => <<"http://hellgate:8022/v1/processing/recpaytool">>, + cashreg => <<"http://hellgate:8022/v1/processing/cashreg">> }}, {proxy_opts, #{ transport_opts => #{ @@ -239,6 +242,7 @@ make_user_identity(UserID) -> -type currency() :: dmsl_domain_thrift:'CurrencySymbolicCode'(). -type invoice_tpl_create_params() :: dmsl_payment_processing_thrift:'InvoiceTemplateCreateParams'(). -type invoice_tpl_update_params() :: dmsl_payment_processing_thrift:'InvoiceTemplateUpdateParams'(). +-type shop_cash_register() :: dmsl_domain_thrift:'ShopCashRegister'(). -spec create_party_and_shop(Client :: pid()) -> shop_id(). @@ -295,6 +299,27 @@ create_battle_ready_shop(Category, TemplateRef, Client) -> _Shop = hg_client_party:get_shop(ShopID, Client), ShopID. +-spec create_battle_ready_shop_with_cashreg(category(), contract_tpl(), Client :: pid(), shop_cash_register()) -> + shop_id(). + +create_battle_ready_shop_with_cashreg(Category, TemplateRef, Client, ShopCashRegister) -> + ShopID = create_battle_ready_shop(Category, TemplateRef, Client), + Changeset = [ + {shop_modification, #payproc_ShopModificationUnit{ + id = ShopID, + modification = {cash_register_modification, #payproc_CashRegModification{ + cash_register = ShopCashRegister + }} + }} + ], + #payproc_Claim{} = hg_client_party:create_claim(Changeset, Client), + ShopID. + +-spec create_customer_w_binding(Client :: pid()) -> ok. + +create_customer_w_binding(_Client) -> + ok. + -spec get_first_contract_id(Client :: pid()) -> contract_id(). diff --git a/apps/hellgate/test/hg_dummy_cashreg_provider.erl b/apps/hellgate/test/hg_dummy_cashreg_provider.erl new file mode 100644 index 000000000..1978dbd60 --- /dev/null +++ b/apps/hellgate/test/hg_dummy_cashreg_provider.erl @@ -0,0 +1,185 @@ +-module(hg_dummy_cashreg_provider). +-behaviour(hg_woody_wrapper). + +-export([handle_function/3]). + +-behaviour(hg_test_proxy). + +-export([get_service_spec/0]). +-export([get_http_cowboy_spec/0]). + +-export([get_callback_url/0]). + +%% cowboy http callbacks +-export([init/3]). +-export([handle/2]). +-export([terminate/3]). +%% + +-define(COWBOY_PORT, 9977). + +-define(sleep(To), + {sleep, #'cashreg_adptprv_SleepIntent'{timer = {timeout, To}}}). +-define(suspend(Tag, To), + {suspend, #'cashreg_adptprv_SuspendIntent'{tag = Tag, timeout = {timeout, To}}}). +-define(finish_success(ReceiptRegEntry), + {finish, #'cashreg_adptprv_FinishIntent'{ + status = {success, #'cashreg_adptprv_Success'{receipt_reg_entry = ReceiptRegEntry}} + }}). +-define(finish_failure(ErrorCode), + {finish, #'cashreg_adptprv_FinishIntent'{status = {failure, #'cashreg_adptprv_Failure'{ + error = {receipt_registration_failed, #cashreg_main_ReceiptRegistrationFailed{ + reason = #cashreg_main_ExternalFailure{code = ErrorCode} + }} + }}}}). + +-spec get_service_spec() -> + hg_proto:service_spec(). + +get_service_spec() -> + {"/test/proxy/cashreg_provider/dummy", {cashreg_proto_adapter_provider_thrift, 'ProviderAdapter'}}. + +-spec get_http_cowboy_spec() -> #{}. + +get_http_cowboy_spec() -> + Dispatch = cowboy_router:compile([{'_', [{"/", ?MODULE, []}]}]), + #{ + listener_ref => ?MODULE, + acceptors_count => 10, + transport_opts => [{port, ?COWBOY_PORT}], + proto_opts => [{env, [{dispatch, Dispatch}]}] + }. + +%% + +-include_lib("cashreg_proto/include/cashreg_proto_main_thrift.hrl"). +-include_lib("cashreg_proto/include/cashreg_proto_adapter_provider_thrift.hrl"). +-include_lib("hellgate/include/cashreg_events.hrl"). + +-spec handle_function(woody:func(), woody:args(), hg_woody_wrapper:handler_opts()) -> + term() | no_return(). + +handle_function( + 'RegisterReceipt', + [ + #cashreg_adptprv_ReceiptContext{ + receipt_params = _, + options = Options + }, + #cashreg_adptprv_Session{state = State} + ], + _ +) -> + register_receipt(State, Options); +handle_function( + 'HandleReceiptCallback', + _, + _ +) -> + Receipt = get_default_receipt_result(), + handle_register_callback(Receipt). + +%% + +register_receipt( + undefined, + #{<<"adapter_state">> := <<"sleeping">>} +) -> + receipt_sleep(1, {str, <<"sleeping">>}); +register_receipt( + undefined, + #{<<"adapter_state">> := <<"suspending">>, <<"tag">> := Tag} +) -> + receipt_suspend(Tag, 1, {str, <<"suspending">>}); +register_receipt( + undefined, + #{<<"adapter_state">> := <<"finishing_failure">>} +) -> + receipt_finish(?finish_failure(<<"400">>), undefined); +register_receipt( + {str, <<"sleeping">>}, + _ +) -> + receipt_sleep(1, {str, <<"finishing_success">>}); +register_receipt( + {str, <<"finishing_success">>}, + _ +) -> + Receipt = get_default_receipt_result(), + receipt_finish(?finish_success(Receipt), undefined). + +receipt_finish(Intent, State) -> + #cashreg_adptprv_ReceiptAdapterResult{ + intent = Intent, + next_state = State + }. + +receipt_sleep(Timeout, State) -> + #cashreg_adptprv_ReceiptAdapterResult{ + intent = ?sleep(Timeout), + next_state = State + }. + +receipt_suspend(Tag, Timeout, State) -> + #cashreg_adptprv_ReceiptAdapterResult{ + intent = ?suspend(Tag, Timeout), + next_state = State + }. + +handle_register_callback(Receipt) -> + #cashreg_adptprv_ReceiptCallbackResult{ + response = {str, <<"ok">>}, + result = #cashreg_adptprv_ReceiptCallbackAdapterResult{ + intent = ?finish_success(Receipt), + next_state = undefined + } + }. + +%% + +-spec init(atom(), cowboy_req:req(), list()) -> {ok, cowboy_req:req(), state}. + +init(_Transport, Req, []) -> + {ok, Req, undefined}. + +-spec handle(cowboy_req:req(), state) -> {ok, cowboy_req:req(), state}. + +handle(Req, State) -> + {Method, Req2} = cowboy_req:method(Req), + {ok, Req3} = handle_adapter_callback(Method, Req2), + {ok, Req3, State}. + +-spec terminate(term(), cowboy_req:req(), state) -> ok. + +terminate(_Reason, _Req, _State) -> + ok. + +-spec get_callback_url() -> binary(). + +get_callback_url() -> + genlib:to_binary("http://127.0.0.1:" ++ integer_to_list(?COWBOY_PORT)). + +handle_adapter_callback(<<"POST">>, Req) -> + {ok, Body, Req2} = cowboy_req:body(Req), + Form = maps:from_list(cow_qs:parse_qs(Body)), + Tag = maps:get(<<"tag">>, Form), + Callback = maps:get(<<"callback">>, Form, {str, Tag}), + RespCode = callback_to_hellgate(Tag, Callback), + cowboy_req:reply(RespCode, [{<<"content-type">>, <<"text/plain; charset=utf-8">>}], <<>>, Req2); +handle_adapter_callback(_, Req) -> + %% Method not allowed. + cowboy_req:reply(405, Req). + +callback_to_hellgate(Tag, Callback) -> + case hg_client_api:call( + cashreg_host_provider, 'ProcessReceiptCallback', [Tag, Callback], + hg_client_api:new(hg_ct_helper:get_hellgate_url()) + ) of + {{ok, _Response}, _} -> + 200; + {{error, _}, _} -> + 500 + end. + +get_default_receipt_result() -> + #'cashreg_main_ReceiptRegistrationEntry'{id = <<"1">>, metadata = {nl, #cashreg_msgpack_Nil{}}}. diff --git a/apps/hg_proto/src/hg_proto.app.src b/apps/hg_proto/src/hg_proto.app.src index afab70ba7..d9d9d62d5 100644 --- a/apps/hg_proto/src/hg_proto.app.src +++ b/apps/hg_proto/src/hg_proto.app.src @@ -7,6 +7,7 @@ stdlib, thrift, dmsl, - mg_proto + mg_proto, + cashreg_proto ]} ]}. diff --git a/apps/hg_proto/src/hg_proto.erl b/apps/hg_proto/src/hg_proto.erl index d56550d17..23fa4f631 100644 --- a/apps/hg_proto/src/hg_proto.erl +++ b/apps/hg_proto/src/hg_proto.erl @@ -44,7 +44,15 @@ get_service(automaton) -> get_service(processor) -> {mg_proto_state_processing_thrift, 'Processor'}; get_service(eventsink) -> - {mg_proto_state_processing_thrift, 'EventSink'}. + {mg_proto_state_processing_thrift, 'EventSink'}; +get_service(cashreg) -> + {cashreg_proto_processing_thrift, 'CashRegister'}; +get_service(cashreg_eventsink) -> + {cashreg_proto_processing_thrift, 'EventSink'}; +get_service(cashreg_provider) -> + {cashreg_proto_adapter_provider_thrift, 'ProviderAdapter'}; +get_service(cashreg_host_provider) -> + {cashreg_proto_adapter_provider_thrift, 'ProviderAdapterHost'}. -spec get_service_spec(Name :: atom()) -> service_spec(). @@ -67,7 +75,13 @@ get_service_spec(Name = recurrent_paytool, #{}) -> {?VERSION_PREFIX ++ "/processing/recpaytool", get_service(Name)}; get_service_spec(Name = recurrent_paytool_eventsink, #{}) -> {?VERSION_PREFIX ++ "/processing/recpaytool/eventsink", get_service(Name)}; +get_service_spec(Name = cashreg, #{}) -> + {?VERSION_PREFIX ++ "/processing/cashreg", get_service(Name)}; +get_service_spec(Name = cashreg_eventsink, #{}) -> + {?VERSION_PREFIX ++ "/processing/cashreg/eventsink", get_service(Name)}; get_service_spec(Name = processor, #{namespace := Ns}) when is_binary(Ns) -> {?VERSION_PREFIX ++ "/stateproc/" ++ binary_to_list(Ns), get_service(Name)}; get_service_spec(Name = proxy_host_provider, #{}) -> - {?VERSION_PREFIX ++ "/proxyhost/provider", get_service(Name)}. + {?VERSION_PREFIX ++ "/proxyhost/provider", get_service(Name)}; +get_service_spec(Name = cashreg_host_provider, #{}) -> + {?VERSION_PREFIX ++ "/cashreghost/provider", get_service(Name)}. diff --git a/config/sys.config b/config/sys.config index fdeddae9d..9b8c95e3d 100644 --- a/config/sys.config +++ b/config/sys.config @@ -30,7 +30,8 @@ party_management => <<"http://hellgate:8022/v1/processing/partymgmt">>, customer_management => <<"http://hellgate:8022/v1/processing/customer_management">>, % TODO make more consistent - recurrent_paytool => <<"http://hellgate:8022/v1/processing/recpaytool">> + recurrent_paytool => <<"http://hellgate:8022/v1/processing/recpaytool">>, + cashreg => <<"http://hellgate:8022/v1/processing/cashreg">> }}, {proxy_opts, #{ transport_opts => #{ diff --git a/docker-compose.sh b/docker-compose.sh index 7ea768ce7..b5f9f30c9 100755 --- a/docker-compose.sh +++ b/docker-compose.sh @@ -17,7 +17,7 @@ services: condition: service_healthy dominant: - image: dr.rbkmoney.com/rbkmoney/dominant:08049aeb4e74fba84d793ae8cf6773314410115c + image: dr.rbkmoney.com/rbkmoney/dominant:74cbda625ac8ad6469f3bdefa4b022a9068f01f1 command: /opt/dominant/bin/dominant foreground depends_on: machinegun: diff --git a/rebar.config b/rebar.config index 91028cc68..c3195d6ca 100644 --- a/rebar.config +++ b/rebar.config @@ -39,10 +39,11 @@ {branch, "master"} } }, - {dmsl , {git, "git@github.com:rbkmoney/damsel.git", {branch, "release/erlang/master"}}}, - {mg_proto , {git, "git@github.com:rbkmoney/machinegun_proto.git", {branch, "master"}}}, - {dmt_client , {git, "git@github.com:rbkmoney/dmt_client.git", {branch, "master"}}}, - {scoper , {git, "git@github.com:rbkmoney/scoper.git", {branch, "master"}}} + {dmsl , {git, "git@github.com:rbkmoney/damsel.git", {branch, "release/erlang/epic/sync_cashreg"}}}, + {mg_proto , {git, "git@github.com:rbkmoney/machinegun_proto.git", {branch, "master"}}}, + {cashreg_proto , {git, "git@github.com:rbkmoney/cashreg-proto.git", {branch, "epic/sync_cashreg"}}}, + {dmt_client , {git, "git@github.com:rbkmoney/dmt_client.git", {branch, "master"}}}, + {scoper , {git, "git@github.com:rbkmoney/scoper.git", {branch, "master"}}} ]}. {xref_checks, [ diff --git a/rebar.lock b/rebar.lock index c2d234c76..851072c38 100644 --- a/rebar.lock +++ b/rebar.lock @@ -1,10 +1,14 @@ {"1.1.0", -[{<<"certifi">>,{pkg,<<"certifi">>,<<"0.7.0">>},2}, +[{<<"cashreg_proto">>, + {git,"git@github.com:rbkmoney/cashreg-proto.git", + {ref,"9a5d374f9682c8e4ad6a1656cb52193d102a2d7e"}}, + 0}, + {<<"certifi">>,{pkg,<<"certifi">>,<<"0.7.0">>},2}, {<<"cowboy">>,{pkg,<<"cowboy">>,<<"1.0.4">>},1}, {<<"cowlib">>,{pkg,<<"cowlib">>,<<"1.0.2">>},2}, {<<"dmsl">>, {git,"git@github.com:rbkmoney/damsel.git", - {ref,"bef04e4564b92c44fa252dce45faefd430d89758"}}, + {ref,"55321a3614c2a700c69285d50aa1300db3a31347"}}, 0}, {<<"dmt_client">>, {git,"git@github.com:rbkmoney/dmt_client.git", @@ -26,7 +30,7 @@ {<<"lager">>,{pkg,<<"lager">>,<<"3.2.1">>},0}, {<<"lager_logstash_formatter">>, {git,"git@github.com:rbkmoney/lager_logstash_formatter.git", - {ref,"d7370337d4d55b37915a2c3202f5c39047674bb3"}}, + {ref,"83a0f21c03dacbd876c7289435f369f573c749b1"}}, 0}, {<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2}, {<<"mg_proto">>, diff --git a/test/machinegun/config.yaml b/test/machinegun/config.yaml index 159772491..b66e6abcc 100644 --- a/test/machinegun/config.yaml +++ b/test/machinegun/config.yaml @@ -15,6 +15,10 @@ namespaces: event_sink: recurrent_paytools processor: url: http://hellgate:8022/v1/stateproc/recurrent_paytools + cashreg: + event_sink: cashreg + processor: + url: http://hellgate:8022/v1/stateproc/cashreg party: event_sink: payproc processor: