mirror of
https://github.com/valitydev/hellgate.git
synced 2024-11-06 02:45:20 +00:00
HG-3: Add stubbed invoice machine and all the wiring (#2)
* HG-3: Add stubbed invoice machine and all the wiring * HG-3: Bump damsel to a proper revision * HG-3: Get rid of precompile hook to make submodules work * HG-3: Add missing elvis config * HG-3: Switch to proto fork temporarily * HG-3: Merge dispatcher activities w/ machine behaviour * HG-4: Switch to new proto fork temporarily * HG-3: Avoid `submodule init` on every make invocation * HG-3: Allow to pass datetime in both native and iso8601 format * HG-4: Switch to new proto fork temporarily * HG-4: Adapt to new protocol + internal & external events * HG-3: Switch to proto fork already * HG-21: Add containerization maketargets * HG-4: Fix interfaces and add missing activities * HG-4: Switch to new proto fork temporarily * HG-6: Fix ruble currency code * HG-4: Fix interface issues * HG-4: Add default config * HG-4: Switch to new proto fork temporarily * HG-4: Start filling provider proxy interaction in * Publish TODOs * HG-4: Stub a provider proxy w/ settings from app env * HG-4: Fix copypasta * HG-4: Add dummy provider proxy, to be moved into testsuite * HG-4: Switch to new proto fork temporarily * HG-21: Remove nonfunctional target dependencies * HG-4: Rename hg_action to make its objective clearer * HG-4: Simplify interface address manipulation * HG-4: Compile proxy related thrift files * HG-4: Switch to new proto fork temporarily * HG-4: Update TODOs * HG-4: Isolate service specs and put them to the proto lib * HG-4: Move dummy provider into the test dir * HG-4: Fix getting events with respect to proto update * damsel@24a247b * HG-4: Introduce hg client + add preliminary test suite * HG-4: Merge woody handler with invoice module * HG-4: Fuse processor handler with machine * HG-4: Harden the build + fix typing errors alongside * HG-4: Add happy payment testcase + stateful client * HG-4: Update gitignore rules with respect to wercker beta * HG-4: Stash a couple of items into TODO * HG-4: Make trivial behaviour for test provider(s) * HG-4: Update elvis rules + lint tests' code * HG-4: Make UserInfo a part of the client + simplify test code with macros * HG-4: Cleanup dirty proxy state after testcases * HG-4: Rename test_provider to a wider test_proxy * HG-4: Explicitly mention requirement on manually started mgun
This commit is contained in:
parent
20a01ee95c
commit
ceb2013c89
1
.gitignore
vendored
1
.gitignore
vendored
@ -13,3 +13,4 @@ erl_crash.dump
|
||||
/_projects/
|
||||
/_steps/
|
||||
/_temp/
|
||||
/.wercker/
|
||||
|
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
[submodule "apps/hg_proto/damsel"]
|
||||
path = apps/hg_proto/damsel
|
||||
url = git@github.com:keynslug/damsel.git
|
41
Makefile
41
Makefile
@ -1,26 +1,36 @@
|
||||
REBAR := $(shell which rebar3 2>/dev/null || which ./rebar3)
|
||||
RELNAME = hellgate
|
||||
SUBMODULES = apps/hg_proto/damsel
|
||||
SUBTARGETS = $(patsubst %,%/.git,$(SUBMODULES))
|
||||
|
||||
.PHONY: all compile devrel start test clean distclean dialyze
|
||||
.PHONY: all submodules compile devrel start test clean distclean dialyze release containerize
|
||||
|
||||
all: compile
|
||||
|
||||
compile:
|
||||
$(REBAR) compile
|
||||
|
||||
rebar-update:
|
||||
$(REBAR) update
|
||||
|
||||
devrel:
|
||||
$(SUBTARGETS): %/.git: %
|
||||
git submodule update --init $<
|
||||
touch $@
|
||||
|
||||
submodules: $(SUBTARGETS)
|
||||
|
||||
compile: submodules
|
||||
$(REBAR) compile
|
||||
|
||||
devrel: submodules
|
||||
$(REBAR) release
|
||||
|
||||
start:
|
||||
start: submodules
|
||||
$(REBAR) run
|
||||
|
||||
test:
|
||||
test: submodules
|
||||
$(REBAR) ct
|
||||
|
||||
xref:
|
||||
lint: compile
|
||||
elvis rock
|
||||
|
||||
xref: submodules
|
||||
$(REBAR) xref
|
||||
|
||||
clean:
|
||||
@ -32,3 +42,16 @@ distclean:
|
||||
|
||||
dialyze:
|
||||
$(REBAR) dialyzer
|
||||
|
||||
DOCKER := $(shell which docker 2>/dev/null)
|
||||
PACKER := $(shell which packer 2>/dev/null)
|
||||
BASE_DIR := $(shell pwd)
|
||||
|
||||
release: ~/.docker/config.json distclean
|
||||
$(DOCKER) run --rm -v $(BASE_DIR):$(BASE_DIR) --workdir $(BASE_DIR) rbkmoney/build rebar3 as prod release
|
||||
|
||||
containerize: release ./packer.json
|
||||
$(PACKER) build packer.json
|
||||
|
||||
~/.docker/config.json:
|
||||
test -f ~/.docker/config.json || (echo "Please run: docker login" ; exit 1)
|
||||
|
15
TODO.md
Normal file
15
TODO.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Invoicing
|
||||
|
||||
* Handle error properly while calling `Automaton`, perfect to pass them untouched with the help of latest `woody` release.
|
||||
* Better and easier to compehend flow control in machines.
|
||||
* More familiar flow control handling of machines, e.g. catching and wrapping thrown exceptions.
|
||||
* Explicit stage denotion in the invoice machine?
|
||||
* __Submachine abstraction and payment submachine implementation__.
|
||||
* __Properly pass woody contexts around__.
|
||||
* __Invoice access control__.
|
||||
|
||||
# Tests
|
||||
|
||||
* Fix excess `localhost` definitions (as soon as service discovery strategy will be finalized, hopefully).
|
||||
* __Add generic albeit more complex test suite which covers as many state transitions with expected effects as possible__.
|
||||
* Employ macros to minimize pattern matching boilerplate.
|
3
apps/hellgate/rebar.config
Normal file
3
apps/hellgate/rebar.config
Normal file
@ -0,0 +1,3 @@
|
||||
{erl_opts, [
|
||||
{parse_transform, lager_transform}
|
||||
]}.
|
@ -1,11 +1,18 @@
|
||||
{application, hellgate, [
|
||||
{description, "A service that does something"},
|
||||
{description,
|
||||
"Umbrella project for state processors implementing payment processing activities"
|
||||
},
|
||||
{vsn, "1"},
|
||||
{registered, []},
|
||||
{mod, {hellgate, []}},
|
||||
{applications, [
|
||||
kernel,
|
||||
stdlib
|
||||
stdlib,
|
||||
lager,
|
||||
genlib,
|
||||
cowboy,
|
||||
woody,
|
||||
hg_proto
|
||||
]},
|
||||
{env, []},
|
||||
{modules, []},
|
||||
|
@ -7,14 +7,14 @@
|
||||
|
||||
%% API
|
||||
-export([start/0]).
|
||||
-export([stop /0]).
|
||||
-export([stop/0]).
|
||||
|
||||
%% Supervisor callbacks
|
||||
-export([init/1]).
|
||||
|
||||
%% Application callbacks
|
||||
-export([start/2]).
|
||||
-export([stop /1]).
|
||||
-export([stop/1]).
|
||||
|
||||
%%
|
||||
%% API
|
||||
@ -29,17 +29,38 @@ start() ->
|
||||
stop() ->
|
||||
application:stop(?MODULE).
|
||||
|
||||
%%
|
||||
%% Supervisor callbacks
|
||||
%%
|
||||
|
||||
-spec init([]) ->
|
||||
{ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
|
||||
|
||||
init([]) ->
|
||||
{ok, {
|
||||
{one_for_all, 0, 1}, []
|
||||
#{strategy => one_for_all, intensity => 6, period => 30},
|
||||
[get_api_child_spec()]
|
||||
}}.
|
||||
|
||||
%%
|
||||
get_api_child_spec() ->
|
||||
woody_server:child_spec(
|
||||
?MODULE,
|
||||
#{
|
||||
ip => hg_utils:get_hostname_ip(genlib_app:env(?MODULE, host, "localhost")),
|
||||
port => genlib_app:env(?MODULE, port, 8800),
|
||||
net_opts => [],
|
||||
event_handler => hg_woody_event_handler,
|
||||
handlers => [
|
||||
construct_service_handler(invoicing, hg_invoice, []),
|
||||
construct_service_handler(processor, hg_machine, [])
|
||||
]
|
||||
}
|
||||
).
|
||||
|
||||
construct_service_handler(Name, Module, Opts) ->
|
||||
{Name, Path, Service} = hg_proto:get_service_spec(Name),
|
||||
{Path, {Service, Module, Opts}}.
|
||||
|
||||
%% Application callbacks
|
||||
%%
|
||||
|
||||
-spec start(normal, any()) ->
|
||||
{ok, pid()} | {error, any()}.
|
||||
start(_StartType, _StartArgs) ->
|
||||
|
69
apps/hellgate/src/hg_domain.erl
Normal file
69
apps/hellgate/src/hg_domain.erl
Normal file
@ -0,0 +1,69 @@
|
||||
-module(hg_domain).
|
||||
-include_lib("hg_proto/include/hg_domain_thrift.hrl").
|
||||
|
||||
%%
|
||||
|
||||
-export([head/0]).
|
||||
-export([all/1]).
|
||||
-export([get/2]).
|
||||
|
||||
%%
|
||||
|
||||
-type revision() :: pos_integer().
|
||||
-type ref() :: _.
|
||||
-type data() :: _.
|
||||
|
||||
-spec head() -> revision().
|
||||
|
||||
head() ->
|
||||
42.
|
||||
|
||||
-spec all(revision()) -> hg_domain_thrift:'Domain'().
|
||||
|
||||
all(_Revision) ->
|
||||
get_fixture().
|
||||
|
||||
-spec get(revision(), ref()) -> data().
|
||||
|
||||
get(Revision, Ref) ->
|
||||
% FIXME: the dirtiest hack you'll ever see
|
||||
Name = type_to_name(Ref),
|
||||
case maps:get({Name, Ref}, all(Revision), undefined) of
|
||||
{Name, {_, Ref, Data}} ->
|
||||
Data;
|
||||
undefined ->
|
||||
undefined
|
||||
end.
|
||||
|
||||
type_to_name(#'CurrencyRef'{}) ->
|
||||
currency;
|
||||
type_to_name(#'ProxyRef'{}) ->
|
||||
proxy.
|
||||
|
||||
%%
|
||||
|
||||
-define(
|
||||
object(ObjectName, Ref, Data),
|
||||
{type_to_name(Ref), Ref} => {type_to_name(Ref), {ObjectName, Ref, Data}}
|
||||
).
|
||||
|
||||
get_fixture() ->
|
||||
#{
|
||||
?object('CurrencyObject',
|
||||
#'CurrencyRef'{symbolic_code = <<"RUB">>},
|
||||
#'Currency'{
|
||||
name = <<"Russian rubles">>,
|
||||
numeric_code = 643,
|
||||
symbolic_code = <<"RUB">>,
|
||||
exponent = 2
|
||||
}
|
||||
),
|
||||
?object('ProxyObject',
|
||||
#'ProxyRef'{id = 1},
|
||||
#'Proxy'{
|
||||
type = provider,
|
||||
url = genlib_app:env(hellgate, provider_proxy_url),
|
||||
options = genlib_app:env(hellgate, provider_proxy_options, #{})
|
||||
}
|
||||
)
|
||||
}.
|
13
apps/hellgate/src/hg_duration.hrl
Normal file
13
apps/hellgate/src/hg_duration.hrl
Normal file
@ -0,0 +1,13 @@
|
||||
-ifndef(__hg_duration__).
|
||||
-define(__hg_duration__, 42).
|
||||
|
||||
-define(seconds(V) , (V)).
|
||||
-define(minutes(V) , (?seconds(V) * 60)).
|
||||
-define(hours(V) , (?minutes(V) * 60)).
|
||||
-define(days(V) , (?hours(V) * 24)).
|
||||
|
||||
-define(MINUTE , ?minutes(1)).
|
||||
-define(HOUR , ?hours(1)).
|
||||
-define(DAY , ?day(1)).
|
||||
|
||||
-endif.
|
411
apps/hellgate/src/hg_invoice.erl
Normal file
411
apps/hellgate/src/hg_invoice.erl
Normal file
@ -0,0 +1,411 @@
|
||||
-module(hg_invoice).
|
||||
-include_lib("hg_proto/include/hg_payment_processing_thrift.hrl").
|
||||
|
||||
%% Woody handler
|
||||
|
||||
-behaviour(woody_server_thrift_handler).
|
||||
|
||||
-export([handle_function/4]).
|
||||
-export([handle_error/4]).
|
||||
|
||||
%% Machine callbacks
|
||||
|
||||
-behaviour(hg_machine).
|
||||
|
||||
-export([init/2]).
|
||||
-export([process_signal/2]).
|
||||
-export([process_call/2]).
|
||||
|
||||
%%
|
||||
|
||||
-spec handle_function(woody_t:func(), woody_server_thrift_handler:args(), woody_client:context(), []) ->
|
||||
{ok, term()} | no_return().
|
||||
|
||||
handle_function('Create', {UserInfo, InvoiceParams}, Context, _Opts) ->
|
||||
InvoiceID = hg_machine:start(?MODULE, {InvoiceParams, UserInfo}, opts(Context)),
|
||||
{ok, InvoiceID};
|
||||
|
||||
handle_function('Get', {UserInfo, InvoiceID}, Context, _Opts) ->
|
||||
InvoiceState = get_invoice_state(get_state(UserInfo, InvoiceID, opts(Context))),
|
||||
{ok, InvoiceState};
|
||||
|
||||
handle_function('GetEvents', {UserInfo, InvoiceID, Range}, Context, _Opts) ->
|
||||
#'EventRange'{'after' = AfterID, limit = Limit} = Range,
|
||||
History = get_history(UserInfo, InvoiceID, opts(Context)),
|
||||
{ok, map_events(select_range(AfterID, Limit, map_history(History)))};
|
||||
|
||||
handle_function('StartPayment', {UserInfo, InvoiceID, PaymentParams}, Context, _Opts) ->
|
||||
Call = {start_payment, PaymentParams, UserInfo},
|
||||
PaymentID = hg_machine:call(?MODULE, InvoiceID, Call, opts(Context)),
|
||||
{ok, PaymentID};
|
||||
|
||||
handle_function('GetPayment', {UserInfo, PaymentID}, Context, _Opts) ->
|
||||
St = get_state(UserInfo, deduce_invoice_id(PaymentID), opts(Context)),
|
||||
case get_payment(PaymentID, St) of
|
||||
Payment = #'InvoicePayment'{} ->
|
||||
{ok, Payment};
|
||||
false ->
|
||||
throw(payment_not_found())
|
||||
end;
|
||||
|
||||
handle_function('Fulfill', {UserInfo, InvoiceID, Reason}, Context, _Opts) ->
|
||||
Result = hg_machine:call(?MODULE, InvoiceID, {fulfill, Reason, UserInfo}, opts(Context)),
|
||||
{ok, Result};
|
||||
|
||||
handle_function('Void', {UserInfo, InvoiceID, Reason}, Context, _Opts) ->
|
||||
Result = hg_machine:call(?MODULE, InvoiceID, {void, Reason, UserInfo}, opts(Context)),
|
||||
{ok, Result}.
|
||||
|
||||
opts(Context) ->
|
||||
#{context => Context}.
|
||||
|
||||
-spec handle_error(woody_t:func(), term(), woody_client:context(), []) ->
|
||||
_.
|
||||
|
||||
handle_error(_Function, _Reason, _Context, _Opts) ->
|
||||
ok.
|
||||
|
||||
%%
|
||||
|
||||
get_history(_UserInfo, InvoiceID, Opts) ->
|
||||
hg_machine:get_history(?MODULE, InvoiceID, Opts).
|
||||
|
||||
get_state(UserInfo, InvoiceID, Opts) ->
|
||||
collapse_history(get_history(UserInfo, InvoiceID, Opts)).
|
||||
|
||||
map_events(Evs) ->
|
||||
[construct_external_event(ID, Ev) || {ID, Ev} <- Evs].
|
||||
|
||||
construct_external_event(ID, Ev) ->
|
||||
#'Event'{id = ID, ev = wrap_external_event(Ev)}.
|
||||
|
||||
wrap_external_event(Ev = #'InvoiceStatusChanged'{}) ->
|
||||
{invoice_status_changed, Ev};
|
||||
wrap_external_event(Ev = #'InvoicePaymentStatusChanged'{}) ->
|
||||
{invoice_payment_status_changed, Ev}.
|
||||
|
||||
%%
|
||||
|
||||
-type invoice() :: hg_domain_thrift:'Invoice'().
|
||||
-type invoice_id() :: hg_domain_thrift:'InvoiceID'().
|
||||
-type user_info() :: hg_payment_processing_thrift:'UserInfo'().
|
||||
-type invoice_params() :: hg_payment_processing_thrift:'InvoiceParams'().
|
||||
-type payment() :: hg_domain_thrift:'InvoicePayment'().
|
||||
-type payment_params() :: hg_payment_processing_thrift:'InvoicePaymentParams'().
|
||||
-type invoice_status() :: hg_domain_thrift:'InvoiceStatus'().
|
||||
-type payment_id() :: hg_domain_thrift:'InvoicePaymentID'().
|
||||
-type payment_st() :: hg_invoice_payment:st().
|
||||
-type payment_trx() :: hg_domain_thrift:'TransactionInfo'().
|
||||
-type detail() :: binary().
|
||||
-type error() :: hg_domain_thrift:'OperationError'().
|
||||
|
||||
-type stage() ::
|
||||
idling |
|
||||
{processing_payment, payment_id(), payment_st()}.
|
||||
|
||||
-record(st, {
|
||||
invoice :: invoice(),
|
||||
payments = [] :: [payment()],
|
||||
stage = idling :: stage()
|
||||
}).
|
||||
|
||||
-type st() :: #st{}.
|
||||
|
||||
-type ev() ::
|
||||
{stage_changed, stage()} |
|
||||
{invoice_created, invoice()} |
|
||||
{invoice_status_changed, invoice_status(), detail()} |
|
||||
{payment_created, payment()} |
|
||||
{payment_state_changed, payment_id(), payment_st()} |
|
||||
{payment_bound, payment_id(), payment_trx() | undefined} |
|
||||
{payment_succeeded, payment_id()} |
|
||||
{payment_failed, payment_id(), error()}.
|
||||
|
||||
-spec init(invoice_id(), {invoice_params(), user_info()}) ->
|
||||
{ok, hg_machine:result([ev()])}.
|
||||
|
||||
init(ID, {InvoiceParams, _UserInfo}) ->
|
||||
Invoice = create_invoice(ID, InvoiceParams),
|
||||
Event = {invoice_created, Invoice},
|
||||
ok(Event, set_invoice_timer(Invoice)).
|
||||
|
||||
-spec process_signal(hg_machine:signal(), hg_machine:history(ev())) ->
|
||||
{ok, hg_machine:result([ev()])}.
|
||||
|
||||
process_signal(timeout, History) ->
|
||||
St = #st{invoice = Invoice, stage = Stage} = collapse_history(History),
|
||||
Status = get_invoice_status(Invoice),
|
||||
case Stage of
|
||||
{processing_payment, PaymentID, PaymentState} ->
|
||||
% there's a payment pending
|
||||
process_payment(PaymentID, PaymentState, St);
|
||||
idling when Status == unpaid ->
|
||||
% invoice is expired
|
||||
process_expiration(St);
|
||||
_ ->
|
||||
ok()
|
||||
end;
|
||||
|
||||
process_signal({repair, _}, History) ->
|
||||
#st{invoice = Invoice} = collapse_history(History),
|
||||
ok([], set_invoice_timer(Invoice)).
|
||||
|
||||
process_expiration(#st{invoice = Invoice}) ->
|
||||
{ok, Event} = cancel_invoice(overdue, Invoice),
|
||||
ok(Event).
|
||||
|
||||
process_payment(PaymentID, PaymentState0, St = #st{invoice = Invoice}) ->
|
||||
% FIXME: code looks shitty, destined to be in payment submachine
|
||||
Payment = get_payment(PaymentID, St),
|
||||
case hg_invoice_payment:process(Payment, Invoice, PaymentState0) of
|
||||
% TODO: check proxy contracts
|
||||
% binding different trx ids is not allowed
|
||||
% empty action is questionable to allow
|
||||
{ok, Trx} ->
|
||||
% payment finished successfully
|
||||
Events = [{payment_succeeded, PaymentID}, {invoice_status_changed, paid, <<>>}],
|
||||
ok(construct_payment_events(PaymentID, Trx, Events));
|
||||
{{error, Error = #'OperationError'{}}, Trx} ->
|
||||
% payment finished with error
|
||||
Event = {payment_failed, PaymentID, Error},
|
||||
ok(construct_payment_events(PaymentID, Trx, Event));
|
||||
{{next, Action, PaymentState}, Trx} ->
|
||||
% payment progressing yet
|
||||
Event = {payment_state_changed, PaymentID, PaymentState},
|
||||
ok(construct_payment_events(PaymentID, Trx, Event), Action)
|
||||
end.
|
||||
|
||||
construct_payment_events(PaymentID, Trx = #'TransactionInfo'{}, Events) ->
|
||||
[{payment_bound, PaymentID, Trx} | wrap_event_list(Events)];
|
||||
construct_payment_events(_PaymentID, undefined, Events) ->
|
||||
Events.
|
||||
|
||||
-type call() ::
|
||||
{start_payment, payment_params(), user_info()} |
|
||||
{fulfill, binary(), user_info()} |
|
||||
{void, binary(), user_info()}.
|
||||
|
||||
-type response() ::
|
||||
ok | {ok, term()} | {exception, term()}.
|
||||
|
||||
-spec process_call(call(), hg_machine:history(ev())) ->
|
||||
{ok, response(), hg_machine:result([ev()])}.
|
||||
|
||||
process_call({start_payment, PaymentParams, _UserInfo}, History) ->
|
||||
#st{invoice = Invoice, stage = Stage} = collapse_history(History),
|
||||
Status = get_invoice_status(Invoice),
|
||||
case Stage of
|
||||
idling when Status == unpaid ->
|
||||
Payment = create_payment(PaymentParams, Invoice),
|
||||
PaymentID = get_payment_id(Payment),
|
||||
Events = [
|
||||
{payment_created, Payment},
|
||||
{payment_state_changed, PaymentID, undefined}
|
||||
],
|
||||
respond({ok, PaymentID}, Events, hg_machine_action:instant());
|
||||
{processing_payment, PaymentID, _} ->
|
||||
raise(payment_pending(PaymentID));
|
||||
_ ->
|
||||
raise(invalid_invoice_status(Invoice))
|
||||
end;
|
||||
|
||||
process_call({fulfill, Reason, _UserInfo}, History) ->
|
||||
#st{invoice = Invoice} = collapse_history(History),
|
||||
case fulfill_invoice(Reason, Invoice) of
|
||||
{ok, Event} ->
|
||||
respond(ok, Event, set_invoice_timer(Invoice));
|
||||
{error, Exception} ->
|
||||
raise(Exception, set_invoice_timer(Invoice))
|
||||
end;
|
||||
|
||||
process_call({void, Reason, _UserInfo}, History) ->
|
||||
#st{invoice = Invoice} = collapse_history(History),
|
||||
case cancel_invoice({void, Reason}, Invoice) of
|
||||
{ok, Event} ->
|
||||
respond(ok, Event, set_invoice_timer(Invoice));
|
||||
{error, Exception} ->
|
||||
raise(Exception, set_invoice_timer(Invoice))
|
||||
end.
|
||||
|
||||
set_invoice_timer(#'Invoice'{status = unpaid, due = Due}) when Due /= undefined ->
|
||||
Ts = genlib_time:daytime_to_unixtime(genlib_format:parse_datetime_iso8601(Due)),
|
||||
hg_machine_action:set_timeout(max(Ts - genlib_time:unow(), 0));
|
||||
set_invoice_timer(_Invoice) ->
|
||||
hg_machine_action:new().
|
||||
|
||||
ok() ->
|
||||
ok([]).
|
||||
ok(Event) ->
|
||||
ok(Event, hg_machine_action:new()).
|
||||
ok(Event, Action) ->
|
||||
{ok, {wrap_event_list(Event), Action}}.
|
||||
|
||||
respond(Response, Event, Action) ->
|
||||
{ok, Response, {wrap_event_list(Event), Action}}.
|
||||
|
||||
raise(Exception) ->
|
||||
raise(Exception, hg_machine_action:new()).
|
||||
raise(Exception, Action) ->
|
||||
{ok, {exception, Exception}, {[], Action}}.
|
||||
|
||||
wrap_event_list(Event) when is_tuple(Event) ->
|
||||
[Event];
|
||||
wrap_event_list(Events) when is_list(Events) ->
|
||||
Events.
|
||||
|
||||
%%
|
||||
|
||||
create_invoice(ID, V = #'InvoiceParams'{}) ->
|
||||
Revision = hg_domain:head(),
|
||||
#'Invoice'{
|
||||
id = ID,
|
||||
created_at = get_datetime_utc(),
|
||||
status = unpaid,
|
||||
domain_revision = Revision,
|
||||
due = V#'InvoiceParams'.due,
|
||||
product = V#'InvoiceParams'.product,
|
||||
description = V#'InvoiceParams'.description,
|
||||
context = V#'InvoiceParams'.context,
|
||||
cost = #'Funds'{
|
||||
amount = V#'InvoiceParams'.amount,
|
||||
currency = hg_domain:get(Revision, V#'InvoiceParams'.currency)
|
||||
}
|
||||
}.
|
||||
|
||||
create_payment(V = #'InvoicePaymentParams'{}, Invoice) ->
|
||||
#'InvoicePayment'{
|
||||
id = create_payment_id(Invoice),
|
||||
created_at = get_datetime_utc(),
|
||||
status = pending,
|
||||
payer = V#'InvoicePaymentParams'.payer,
|
||||
payment_tool = V#'InvoicePaymentParams'.payment_tool,
|
||||
session = V#'InvoicePaymentParams'.session
|
||||
}.
|
||||
|
||||
create_payment_id(Invoice = #'Invoice'{}) ->
|
||||
create_payment_id(get_invoice_id(Invoice));
|
||||
create_payment_id(InvoiceID) ->
|
||||
<<InvoiceID/binary, ":", "0">>.
|
||||
|
||||
deduce_invoice_id(PaymentID) ->
|
||||
case binary:split(PaymentID, <<":">>) of
|
||||
[InvoiceID, _] ->
|
||||
InvoiceID;
|
||||
_ ->
|
||||
<<>>
|
||||
end.
|
||||
|
||||
get_invoice_id(#'Invoice'{id = ID}) ->
|
||||
ID.
|
||||
|
||||
get_invoice_status(#'Invoice'{status = Status}) ->
|
||||
Status.
|
||||
|
||||
get_payment_id(#'InvoicePayment'{id = ID}) ->
|
||||
ID.
|
||||
|
||||
cancel_invoice(Reason, #'Invoice'{status = unpaid}) ->
|
||||
{ok, {invoice_status_changed, cancelled, format_reason(Reason)}};
|
||||
cancel_invoice(_Reason, Invoice) ->
|
||||
{error, invalid_invoice_status(Invoice)}.
|
||||
|
||||
fulfill_invoice(Reason, #'Invoice'{status = paid}) ->
|
||||
{ok, {invoice_status_changed, fulfilled, format_reason(Reason)}};
|
||||
fulfill_invoice(_Reason, Invoice) ->
|
||||
{error, invalid_invoice_status(Invoice)}.
|
||||
|
||||
invalid_invoice_status(Invoice) ->
|
||||
#'InvalidInvoiceStatus'{status = get_invoice_status(Invoice)}.
|
||||
payment_not_found() ->
|
||||
#'InvoicePaymentNotFound'{}.
|
||||
payment_pending(PaymentID) ->
|
||||
#'InvoicePaymentPending'{id = PaymentID}.
|
||||
|
||||
%%
|
||||
|
||||
-spec collapse_history([ev()]) -> st().
|
||||
|
||||
collapse_history(History) ->
|
||||
lists:foldl(fun ({_ID, Ev}, St) -> merge_history(Ev, St) end, #st{}, History).
|
||||
|
||||
merge_history(Events, St) when is_list(Events) ->
|
||||
lists:foldl(fun merge_history/2, St, Events);
|
||||
|
||||
merge_history({invoice_created, Invoice}, St) ->
|
||||
St#st{invoice = Invoice};
|
||||
merge_history({invoice_status_changed, Status, Details}, St = #st{invoice = I}) ->
|
||||
St#st{invoice = I#'Invoice'{status = Status, details = Details}};
|
||||
|
||||
merge_history({payment_created, Payment}, St) ->
|
||||
set_payment(Payment, St);
|
||||
merge_history({payment_state_changed, PaymentID, PaymentState}, St) ->
|
||||
set_stage({processing_payment, PaymentID, PaymentState}, St);
|
||||
merge_history({payment_bound, PaymentID, Trx}, St) ->
|
||||
Payment = get_payment(PaymentID, St),
|
||||
set_payment(Payment#'InvoicePayment'{trx = Trx}, St);
|
||||
merge_history({payment_succeeded, PaymentID}, St) ->
|
||||
Payment = get_payment(PaymentID, St),
|
||||
set_payment(Payment#'InvoicePayment'{status = succeeded}, set_stage(idling, St));
|
||||
merge_history({payment_failed, PaymentID, Error}, St) ->
|
||||
Payment = get_payment(PaymentID, St),
|
||||
set_payment(Payment#'InvoicePayment'{status = failed, err = Error}, St).
|
||||
|
||||
set_stage(Stage, St) ->
|
||||
St#st{stage = Stage}.
|
||||
|
||||
get_payment(PaymentID, St) ->
|
||||
lists:keyfind(PaymentID, #'InvoicePayment'.id, St#st.payments).
|
||||
set_payment(Payment, St) ->
|
||||
St#st{payments = lists:keystore(get_payment_id(Payment), #'InvoicePayment'.id, St#st.payments, Payment)}.
|
||||
|
||||
get_invoice_state(#st{invoice = Invoice, payments = Payments}) ->
|
||||
#'InvoiceState'{invoice = Invoice, payments = Payments}.
|
||||
|
||||
%%
|
||||
|
||||
map_history(History) ->
|
||||
lists:reverse(element(2, lists:foldl(
|
||||
fun ({ID, Evs}, {St, Acc}) -> map_history([{ID, Ev} || Ev <- Evs], St, Acc) end,
|
||||
{#st{}, []},
|
||||
History
|
||||
))).
|
||||
|
||||
map_history(Evs, St, Acc) when is_list(Evs) ->
|
||||
lists:foldl(fun ({ID, Ev}, {St0, Acc0}) -> map_history(ID, Ev, St0, Acc0) end, {St, Acc}, Evs).
|
||||
|
||||
map_history(ID, Ev, St, Acc) ->
|
||||
St1 = merge_history(Ev, St),
|
||||
{St1, [{ID, Ev1} || Ev1 <- map_history(Ev, St1)] ++ Acc}.
|
||||
|
||||
map_history({invoice_created, _}, #st{invoice = Invoice}) ->
|
||||
[#'InvoiceStatusChanged'{invoice = Invoice}];
|
||||
map_history({invoice_status_changed, _, _}, #st{invoice = Invoice}) ->
|
||||
[#'InvoiceStatusChanged'{invoice = Invoice}];
|
||||
|
||||
map_history({payment_created, Payment}, _St) ->
|
||||
[#'InvoicePaymentStatusChanged'{payment = Payment}];
|
||||
map_history({payment_succeeded, PaymentID}, St) ->
|
||||
[#'InvoicePaymentStatusChanged'{payment = get_payment(PaymentID, St)}];
|
||||
map_history({payment_failed, PaymentID, _}, St) ->
|
||||
[#'InvoicePaymentStatusChanged'{payment = get_payment(PaymentID, St)}];
|
||||
map_history(_Event, _St) ->
|
||||
[].
|
||||
|
||||
select_range(undefined, Limit, History) ->
|
||||
select_range(Limit, History);
|
||||
select_range(AfterID, Limit, History) ->
|
||||
select_range(Limit, lists:dropwhile(fun ({ID, _}) -> ID =< AfterID end, History)).
|
||||
|
||||
select_range(Limit, History) ->
|
||||
lists:sublist(History, Limit).
|
||||
|
||||
%%
|
||||
|
||||
%% TODO: fix this dirty hack
|
||||
format_reason({Pre, V}) ->
|
||||
genlib:format("~s: ~s", [Pre, genlib:to_binary(V)]);
|
||||
format_reason(V) ->
|
||||
genlib:to_binary(V).
|
||||
|
||||
get_datetime_utc() ->
|
||||
genlib_format:format_datetime_iso8601(calendar:universal_time()).
|
135
apps/hellgate/src/hg_invoice_payment.erl
Normal file
135
apps/hellgate/src/hg_invoice_payment.erl
Normal file
@ -0,0 +1,135 @@
|
||||
-module(hg_invoice_payment).
|
||||
-include_lib("hg_proto/include/hg_domain_thrift.hrl").
|
||||
-include_lib("hg_proto/include/hg_proxy_provider_thrift.hrl").
|
||||
|
||||
%% API
|
||||
|
||||
-export([process/3]).
|
||||
|
||||
%% Machine callbacks
|
||||
|
||||
% -behaviour(hg_machine).
|
||||
|
||||
% -export([init/2]).
|
||||
% -export([process_signal/2]).
|
||||
% -export([process_call/2]).
|
||||
|
||||
%%
|
||||
|
||||
-record(st, {
|
||||
stage :: process_payment | capture_payment,
|
||||
proxy_ref :: hg_domain_thrift:'ProxyRef'(),
|
||||
proxy_state :: binary() | undefined,
|
||||
context :: woody_client:context()
|
||||
}).
|
||||
|
||||
-opaque st() :: #st{}.
|
||||
-export_type([st/0]).
|
||||
|
||||
-type invoice() :: hg_domain_thrift:'Invoice'().
|
||||
-type payment() :: hg_domain_thrift:'InvoicePayment'().
|
||||
-type payment_trx() :: hg_domain_thrift:'TransactionInfo'() | undefined.
|
||||
-type error() :: hg_domain_thrift:'OperationError'().
|
||||
|
||||
-spec process(payment(), invoice(), st() | undefined) ->
|
||||
{ok, payment_trx()} |
|
||||
{{error, error()}, payment_trx()} |
|
||||
{{next, hg_machine_action:t(), st()}, payment_trx()}.
|
||||
|
||||
process(Payment, Invoice, undefined) ->
|
||||
process(Payment, Invoice, construct_state(Payment, Invoice));
|
||||
process(Payment, Invoice, St = #st{}) ->
|
||||
Proxy = get_proxy(Invoice, St),
|
||||
PaymentInfo = construct_payment_info(Payment, Invoice, Proxy, St),
|
||||
process_(Proxy, PaymentInfo, St).
|
||||
|
||||
process_(Proxy, PaymentInfo, St = #st{stage = process_payment, context = Context}) ->
|
||||
% FIXME: dirty simulation of one-phase payment through the two-phase interaction
|
||||
case handle_process_result(process_payment(Proxy, PaymentInfo, Context), St) of
|
||||
{ok, Trx} ->
|
||||
NextSt = St#st{stage = capture_payment, proxy_state = undefined},
|
||||
{{next, hg_machine_action:instant(), NextSt}, Trx};
|
||||
Result ->
|
||||
Result
|
||||
end;
|
||||
process_(Proxy, PaymentInfo, St = #st{stage = capture_payment, context = Context}) ->
|
||||
handle_process_result(capture_payment(Proxy, PaymentInfo, Context), St).
|
||||
|
||||
handle_process_result(#'ProcessResult'{intent = {_, Intent}, trx = Trx, next_state = ProxyStateNext}, St) ->
|
||||
handle_process_result(Intent, Trx, St#st{proxy_state = ProxyStateNext}).
|
||||
|
||||
handle_process_result(#'FinishIntent'{status = {ok, _}}, Trx, _) ->
|
||||
{ok, Trx};
|
||||
handle_process_result(#'FinishIntent'{status = {failure, Error}}, Trx, _) ->
|
||||
{{error, map_error(Error)}, Trx};
|
||||
handle_process_result(#'SleepIntent'{timer = Timer}, Trx, StNext) ->
|
||||
{{next, hg_machine_action:set_timer(Timer), StNext}, Trx}.
|
||||
|
||||
get_proxy(#'Invoice'{domain_revision = Revision}, #st{proxy_ref = Ref}) ->
|
||||
hg_domain:get(Revision, Ref).
|
||||
|
||||
construct_payment_info(Payment, Invoice, Proxy, #st{proxy_state = ProxyState}) ->
|
||||
#'PaymentInfo'{
|
||||
invoice = Invoice,
|
||||
payment = Payment,
|
||||
options = Proxy#'Proxy'.options,
|
||||
state = ProxyState
|
||||
}.
|
||||
|
||||
map_error(#'Error'{code = Code, description = Description}) ->
|
||||
#'OperationError'{code = Code, description = Description}.
|
||||
|
||||
%%
|
||||
|
||||
construct_state(Payment, Invoice) ->
|
||||
#st{
|
||||
stage = process_payment,
|
||||
proxy_ref = select_proxy(Payment, Invoice),
|
||||
context = construct_context()
|
||||
}.
|
||||
|
||||
construct_context() ->
|
||||
ReqID = genlib_format:format_int_base(genlib_time:ticks(), 62),
|
||||
woody_client:new_context(ReqID, hg_woody_event_handler).
|
||||
|
||||
select_proxy(_, _) ->
|
||||
% FIXME: turbo routing
|
||||
#'ProxyRef'{id = 1}.
|
||||
|
||||
%% Proxy provider client
|
||||
|
||||
-define(SERVICE, {hg_proxy_provider_thrift, 'ProviderProxy'}).
|
||||
|
||||
-type process_payment_result() :: hg_proxy_provider_thrift:'ProcessResult'().
|
||||
|
||||
-spec process_payment(
|
||||
hg_domain_thrift:'Proxy'(),
|
||||
hg_proxy_provider_thrift:'PaymentInfo'(),
|
||||
woody_client:context()
|
||||
) ->
|
||||
process_payment_result().
|
||||
process_payment(Proxy, PaymentInfo, Context) ->
|
||||
call(Context, Proxy, {?SERVICE, 'ProcessPayment', [PaymentInfo]}).
|
||||
|
||||
-spec capture_payment(
|
||||
hg_domain_thrift:'Proxy'(),
|
||||
hg_proxy_provider_thrift:'PaymentInfo'(),
|
||||
woody_client:context()
|
||||
) ->
|
||||
process_payment_result().
|
||||
capture_payment(Proxy, PaymentInfo, Context) ->
|
||||
call(Context, Proxy, {?SERVICE, 'CapturePayment', [PaymentInfo]}).
|
||||
|
||||
call(Context, Proxy, Call) ->
|
||||
Endpoint = get_call_options(Proxy),
|
||||
try woody_client:call(Context, Call, Endpoint) of
|
||||
{{ok, Result = #'ProcessResult'{}}, _} ->
|
||||
Result
|
||||
catch
|
||||
% TODO: support retry strategies
|
||||
{#'TryLater'{e = _Error}, _} ->
|
||||
#'ProcessResult'{intent = {sleep, #'SleepIntent'{timer = {timeout, 10}}}}
|
||||
end.
|
||||
|
||||
get_call_options(#'Proxy'{url = Url}) ->
|
||||
#{url => Url}.
|
225
apps/hellgate/src/hg_machine.erl
Normal file
225
apps/hellgate/src/hg_machine.erl
Normal file
@ -0,0 +1,225 @@
|
||||
-module(hg_machine).
|
||||
|
||||
-type id() :: binary().
|
||||
-type args() :: _.
|
||||
-type event() :: _.
|
||||
|
||||
-type history(Event) :: [Event].
|
||||
-type history() :: history(event()).
|
||||
|
||||
-type result(Event) :: {Event, hg_machine_action:t()}.
|
||||
-type result() :: result(event()).
|
||||
|
||||
-callback init(id(), args()) ->
|
||||
{ok, result()}.
|
||||
|
||||
-type signal() ::
|
||||
timeout | {repair, args()}.
|
||||
|
||||
-callback process_signal(signal(), history()) ->
|
||||
{ok, result()}.
|
||||
|
||||
-type call() :: _.
|
||||
-type response() :: ok | {ok, term()} | {exception, term()}.
|
||||
|
||||
-callback process_call(call(), history()) ->
|
||||
{ok, response(), result()}.
|
||||
|
||||
-export_type([id/0]).
|
||||
-export_type([event/0]).
|
||||
-export_type([signal/0]).
|
||||
-export_type([history/0]).
|
||||
-export_type([history/1]).
|
||||
-export_type([result/0]).
|
||||
-export_type([result/1]).
|
||||
|
||||
-export([start/3]).
|
||||
-export([call/4]).
|
||||
-export([get_history/3]).
|
||||
|
||||
-export([dispatch_signal/3]).
|
||||
-export([dispatch_call/3]).
|
||||
|
||||
%% Woody handler
|
||||
|
||||
-behaviour(woody_server_thrift_handler).
|
||||
|
||||
-export([handle_function/4]).
|
||||
-export([handle_error/4]).
|
||||
|
||||
%%
|
||||
|
||||
-include_lib("hg_proto/include/hg_state_processing_thrift.hrl").
|
||||
|
||||
-type opts() :: #{
|
||||
context => woody_client:context()
|
||||
}.
|
||||
|
||||
%%
|
||||
|
||||
-spec start(module(), term(), opts()) -> id().
|
||||
|
||||
start(Module, Args, #{context := Context}) ->
|
||||
{{ok, Response}, _} = call_automaton('start', [#'Args'{arg = wrap_args(Module, Args)}], Context),
|
||||
#'StartResult'{id = ID} = Response,
|
||||
ID.
|
||||
|
||||
-spec call(module(), id(), term(), opts()) -> term() | no_return().
|
||||
|
||||
call(Module, ID, Args, #{context := Context}) ->
|
||||
case call_automaton('call', [{id, ID}, wrap_args(Module, Args)], Context) of
|
||||
{{ok, Response}, _} ->
|
||||
% should be specific to a processing interface already
|
||||
case unmarshal_term(Response) of
|
||||
ok ->
|
||||
ok;
|
||||
{ok, Result} ->
|
||||
Result;
|
||||
{exception, Exception} ->
|
||||
throw(Exception)
|
||||
end;
|
||||
{{exception, Exception}, _} ->
|
||||
% TODO: exception mapping
|
||||
throw(Exception);
|
||||
{{error, Reason}, _} ->
|
||||
error(Reason)
|
||||
end.
|
||||
|
||||
-spec get_history(module(), id(), opts()) -> history().
|
||||
|
||||
get_history(Module, ID, #{context := Context}) ->
|
||||
case call_automaton('getHistory', [{id, ID}, #'HistoryRange'{}], Context) of
|
||||
{{ok, History0}, _} ->
|
||||
{Module, History} = unwrap_history(unmarshal_history(History0)),
|
||||
History;
|
||||
{{exception, Exception}, _} ->
|
||||
% TODO: exception mapping
|
||||
throw(Exception);
|
||||
{{error, Reason}, _} ->
|
||||
error(Reason)
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
call_automaton(Function, Args, Context) ->
|
||||
% TODO: hg_config module, aware of config entry semantics
|
||||
Url = genlib_app:env(hellgate, automaton_service_url),
|
||||
Service = {hg_state_processing_thrift, 'Automaton'},
|
||||
woody_client:call_safe(Context, {Service, Function, Args}, #{url => Url}).
|
||||
|
||||
%%
|
||||
|
||||
-type func() :: 'processSignal' | 'processCall'.
|
||||
|
||||
-spec handle_function(func(), woody_server_thrift_handler:args(), woody_client:context(), []) ->
|
||||
{ok, term()} | no_return().
|
||||
|
||||
handle_function('processSignal', {Args}, Context, _Opts) ->
|
||||
#'SignalArgs'{signal = {_Type, Signal}, history = History} = Args,
|
||||
{ok, dispatch_signal(Signal, unmarshal_history(History), opts(Context))};
|
||||
|
||||
handle_function('processCall', {Args}, Context, _Opts) ->
|
||||
#'CallArgs'{call = Payload, history = History} = Args,
|
||||
{ok, dispatch_call(Payload, unmarshal_history(History), opts(Context))}.
|
||||
|
||||
opts(Context) ->
|
||||
#{context => Context}.
|
||||
|
||||
-spec handle_error(woody_t:func(), term(), woody_client:context(), []) ->
|
||||
_.
|
||||
|
||||
handle_error(_Function, _Reason, _Context, _Opts) ->
|
||||
ok.
|
||||
|
||||
%%
|
||||
|
||||
-spec dispatch_signal(Signal, hg_machine:history(), opts()) -> Result when
|
||||
Signal ::
|
||||
hg_state_processing_thrift:'InitSignal'() |
|
||||
hg_state_processing_thrift:'TimeoutSignal'() |
|
||||
hg_state_processing_thrift:'RepairSignal'(),
|
||||
Result ::
|
||||
hg_state_processing_thrift:'SignalResult'().
|
||||
|
||||
dispatch_signal(#'InitSignal'{id = ID, arg = Payload}, [], _Opts) ->
|
||||
% TODO: do not ignore `Opts`
|
||||
{Module, Args} = unwrap_args(Payload),
|
||||
_ = lager:debug("[machine] [~p] dispatch init (~p: ~p) with history: ~p", [Module, ID, Args, []]),
|
||||
marshal_signal_result(Module:init(ID, Args), Module);
|
||||
|
||||
dispatch_signal(#'TimeoutSignal'{}, History0, _Opts) ->
|
||||
% TODO: do not ignore `Opts`
|
||||
% TODO: deducing module from signal payload looks more natural
|
||||
% opaque payload in every event?
|
||||
{Module, History} = unwrap_history(History0),
|
||||
_ = lager:debug("[machine] [~p] dispatch timeout with history: ~p", [Module, History]),
|
||||
marshal_signal_result(Module:process_signal(timeout, History), Module);
|
||||
|
||||
dispatch_signal(#'RepairSignal'{arg = Payload}, History0, _Opts) ->
|
||||
% TODO: do not ignore `Opts`
|
||||
{Module, History} = unwrap_history(History0),
|
||||
Args = unmarshal_term(Payload),
|
||||
_ = lager:debug("[machine] [~p] dispatch repair (~p) with history: ~p", [Module, Args, History]),
|
||||
marshal_signal_result(Module:process_signal({repair, Args}, History), Module).
|
||||
|
||||
marshal_signal_result({ok, {Event, Action}}, Module) ->
|
||||
_ = lager:debug("[machine] [~p] result with event = ~p and action = ~p", [Module, Event, Action]),
|
||||
#'SignalResult'{
|
||||
ev = wrap_event(Module, Event),
|
||||
action = Action
|
||||
}.
|
||||
|
||||
|
||||
-spec dispatch_call(Call, hg_machine:history(), opts()) -> Result when
|
||||
Call :: hg_state_processing_thrift:'Call'(),
|
||||
Result :: hg_state_processing_thrift:'CallResult'().
|
||||
|
||||
dispatch_call(Payload, History0, _Opts) ->
|
||||
% TODO: do not ignore `Opts`
|
||||
% TODO: looks suspicious
|
||||
{Module, Args} = unwrap_args(Payload),
|
||||
{Module, History} = unwrap_history(History0),
|
||||
_ = lager:debug("[machine] [~p] dispatch call (~p) with history: ~p", [Module, Args, History]),
|
||||
marshal_call_result(Module:process_call(Args, History), Module).
|
||||
|
||||
%%
|
||||
|
||||
marshal_call_result({ok, Response, {Event, Action}}, Module) ->
|
||||
_ = lager:debug(
|
||||
"[machine] [~p] call response = ~p with event = ~p and action = ~p",
|
||||
[Module, Response, Event, Action]
|
||||
),
|
||||
#'CallResult'{
|
||||
ev = wrap_event(Module, Event),
|
||||
action = Action,
|
||||
response = marshal_term(Response)
|
||||
}.
|
||||
|
||||
%%
|
||||
|
||||
unmarshal_history(undefined) ->
|
||||
[];
|
||||
unmarshal_history(History) ->
|
||||
[{ID, Body} || #'Event'{id = ID, body = Body} <- History].
|
||||
|
||||
unwrap_history(History = [Event | _]) ->
|
||||
{_ID, {Module, _EventInner}} = unwrap_event(Event),
|
||||
{Module, [begin {ID, {_, EventInner}} = unwrap_event(E), {ID, EventInner} end || E <- History]}.
|
||||
|
||||
wrap_event(Module, EventInner) ->
|
||||
wrap_args(Module, EventInner).
|
||||
|
||||
unwrap_event({ID, Payload}) ->
|
||||
{ID, unwrap_args(Payload)}.
|
||||
|
||||
wrap_args(Module, Args) ->
|
||||
marshal_term({Module, Args}).
|
||||
|
||||
unwrap_args(Payload) ->
|
||||
unmarshal_term(Payload).
|
||||
|
||||
marshal_term(V) ->
|
||||
term_to_binary(V).
|
||||
|
||||
unmarshal_term(B) ->
|
||||
binary_to_term(B).
|
85
apps/hellgate/src/hg_machine_action.erl
Normal file
85
apps/hellgate/src/hg_machine_action.erl
Normal file
@ -0,0 +1,85 @@
|
||||
-module(hg_machine_action).
|
||||
|
||||
-export([new/0]).
|
||||
-export([instant/0]).
|
||||
-export([set_timeout/1]).
|
||||
-export([set_timeout/2]).
|
||||
-export([set_deadline/1]).
|
||||
-export([set_deadline/2]).
|
||||
-export([set_timer/1]).
|
||||
-export([set_timer/2]).
|
||||
-export([set_tag/1]).
|
||||
-export([set_tag/2]).
|
||||
|
||||
-include_lib("hg_proto/include/hg_state_processing_thrift.hrl").
|
||||
|
||||
%%
|
||||
|
||||
-type tag() :: binary().
|
||||
-type seconds() :: non_neg_integer().
|
||||
-type datetime_iso8601() :: binary().
|
||||
-type datetime() :: calendar:datetime() | datetime_iso8601().
|
||||
|
||||
-type timer() :: hg_base_thrift:'Timer'().
|
||||
-type t() :: hg_state_processing_thrift:'ComplexAction'().
|
||||
|
||||
-export_type([t/0]).
|
||||
|
||||
%%
|
||||
|
||||
-spec new() -> t().
|
||||
|
||||
new() ->
|
||||
#'ComplexAction'{}.
|
||||
|
||||
-spec instant() -> t().
|
||||
|
||||
instant() ->
|
||||
set_timeout(0, new()).
|
||||
|
||||
-spec set_timeout(seconds()) -> t().
|
||||
|
||||
set_timeout(Seconds) ->
|
||||
set_timeout(Seconds, new()).
|
||||
|
||||
-spec set_timeout(seconds(), t()) -> t().
|
||||
|
||||
set_timeout(Seconds, Action) when is_integer(Seconds) andalso Seconds >= 0 ->
|
||||
set_timer({timeout, Seconds}, Action).
|
||||
|
||||
-spec set_deadline(datetime()) -> t().
|
||||
|
||||
set_deadline(Deadline) ->
|
||||
set_deadline(Deadline, new()).
|
||||
|
||||
-spec set_deadline(datetime(), t()) -> t().
|
||||
|
||||
set_deadline(Deadline, Action) ->
|
||||
set_timer({deadline, try_format_dt(Deadline)}, Action).
|
||||
|
||||
-spec set_timer(timer()) -> t().
|
||||
|
||||
set_timer(Timer) ->
|
||||
set_timer(Timer, new()).
|
||||
|
||||
-spec set_timer(timer(), t()) -> t().
|
||||
|
||||
set_timer(Timer, Action = #'ComplexAction'{}) ->
|
||||
Action#'ComplexAction'{set_timer = #'SetTimerAction'{timer = Timer}}.
|
||||
|
||||
-spec set_tag(tag()) -> t().
|
||||
|
||||
set_tag(Tag) ->
|
||||
set_tag(Tag, new()).
|
||||
|
||||
-spec set_tag(tag(), t()) -> t().
|
||||
|
||||
set_tag(Tag, Action = #'ComplexAction'{}) when is_binary(Tag) andalso byte_size(Tag) > 0 ->
|
||||
Action#'ComplexAction'{tag = #'TagAction'{tag = Tag}}.
|
||||
|
||||
%%
|
||||
|
||||
try_format_dt(Datetime = {_, _}) ->
|
||||
genlib_format:format_datetime_iso8601(Datetime);
|
||||
try_format_dt(Datetime) when is_binary(Datetime) ->
|
||||
Datetime.
|
38
apps/hellgate/src/hg_utils.erl
Normal file
38
apps/hellgate/src/hg_utils.erl
Normal file
@ -0,0 +1,38 @@
|
||||
-module(hg_utils).
|
||||
|
||||
-export([shift_datetime/2]).
|
||||
-export([get_hostname_ip/1]).
|
||||
|
||||
%%
|
||||
|
||||
-type seconds() :: integer().
|
||||
-type datetime_iso8601() :: binary().
|
||||
|
||||
-type dt() :: calendar:datetime() | datetime_iso8601().
|
||||
|
||||
-spec shift_datetime(dt(), seconds()) -> dt().
|
||||
|
||||
shift_datetime(Dt, Seconds) when is_binary(Dt) ->
|
||||
format_dt(shift_datetime(parse_dt(Dt), Seconds));
|
||||
shift_datetime(Dt = {_, _}, Seconds) ->
|
||||
calendar:gregorian_seconds_to_datetime(calendar:datetime_to_gregorian_seconds(Dt) + Seconds).
|
||||
|
||||
format_dt(Dt) ->
|
||||
genlib_format:format_datetime_iso8601(Dt).
|
||||
parse_dt(Dt) ->
|
||||
genlib_format:parse_datetime_iso8601(Dt).
|
||||
|
||||
%%
|
||||
|
||||
-spec get_hostname_ip(Hostname | IP) -> IP when
|
||||
Hostname :: string(),
|
||||
IP :: inet:ip_address().
|
||||
|
||||
get_hostname_ip(Host) ->
|
||||
% TODO: respect preferred address family
|
||||
case inet:getaddr(Host, inet) of
|
||||
{ok, IP} ->
|
||||
IP;
|
||||
{error, Error} ->
|
||||
exit(Error)
|
||||
end.
|
22
apps/hellgate/src/hg_woody_event_handler.erl
Normal file
22
apps/hellgate/src/hg_woody_event_handler.erl
Normal file
@ -0,0 +1,22 @@
|
||||
-module(hg_woody_event_handler).
|
||||
-behaviour(woody_event_handler).
|
||||
|
||||
-export([handle_event/3]).
|
||||
|
||||
%%
|
||||
|
||||
-spec handle_event(EventType, RpcID, EventMeta)
|
||||
-> _ when
|
||||
EventType :: woody_event_handler:event_type(),
|
||||
RpcID :: woody_t:rpc_id(),
|
||||
EventMeta :: woody_event_handler:event_meta_type().
|
||||
|
||||
handle_event(EventType, RpcID, #{status := error, class := Class, reason := Reason, stack := Stack}) ->
|
||||
lager:error(
|
||||
maps:to_list(RpcID),
|
||||
"[server] ~s with ~s:~p at ~s",
|
||||
[EventType, Class, Reason, genlib_format:format_stacktrace(Stack, [newlines])]
|
||||
);
|
||||
|
||||
handle_event(EventType, RpcID, EventMeta) ->
|
||||
lager:debug(maps:to_list(RpcID), "[server] ~s: ~p", [EventType, EventMeta]).
|
@ -1,37 +0,0 @@
|
||||
-module(hellgate_tests_SUITE).
|
||||
-include_lib("common_test/include/ct.hrl").
|
||||
-compile(export_all).
|
||||
|
||||
%%
|
||||
%% tests descriptions
|
||||
%%
|
||||
all() ->
|
||||
[
|
||||
dummy_test
|
||||
].
|
||||
|
||||
%%
|
||||
%% starting/stopping
|
||||
%%
|
||||
init_per_suite(C) ->
|
||||
{ok, Apps} = application:ensure_all_started(hellgate),
|
||||
[{apps, Apps}|C].
|
||||
|
||||
init_per_suite(_, _C) ->
|
||||
[application_stop(App) || App <- proplists:get_value(apps)].
|
||||
|
||||
application_stop(App=sasl) ->
|
||||
%% hack for preventing sasl deadlock
|
||||
%% http://erlang.org/pipermail/erlang-questions/2014-May/079012.html
|
||||
error_logger:delete_report_handler(cth_log_redirect),
|
||||
application:stop(App),
|
||||
error_logger:add_report_handler(cth_log_redirect),
|
||||
ok;
|
||||
application_stop(App) ->
|
||||
application:stop(App).
|
||||
|
||||
%%
|
||||
%% tests
|
||||
%%
|
||||
dummy_test(_C) ->
|
||||
ok.
|
34
apps/hellgate/test/hg_ct_helper.erl
Normal file
34
apps/hellgate/test/hg_ct_helper.erl
Normal file
@ -0,0 +1,34 @@
|
||||
-module(hg_ct_helper).
|
||||
|
||||
-export([start_app/1]).
|
||||
-export([start_app/2]).
|
||||
|
||||
%%
|
||||
|
||||
-type app_name() :: atom().
|
||||
|
||||
-spec start_app(app_name()) -> [app_name()].
|
||||
|
||||
start_app(lager = AppName) ->
|
||||
start_app(AppName, [
|
||||
{async_threshold, 1},
|
||||
{async_threshold_window, 0},
|
||||
{error_logger_hwm, 600},
|
||||
{suppress_application_start_stop, true},
|
||||
{handlers, [
|
||||
{lager_common_test_backend, [debug, false]}
|
||||
]}
|
||||
]);
|
||||
|
||||
start_app(woody = AppName) ->
|
||||
start_app(AppName, [
|
||||
{acceptors_pool_size, 4}
|
||||
]);
|
||||
|
||||
start_app(AppName) ->
|
||||
genlib_app:start_application(AppName).
|
||||
|
||||
-spec start_app(app_name(), list()) -> [app_name()].
|
||||
|
||||
start_app(AppName, Env) ->
|
||||
genlib_app:start_application_with(AppName, Env).
|
75
apps/hellgate/test/hg_dummy_provider.erl
Normal file
75
apps/hellgate/test/hg_dummy_provider.erl
Normal file
@ -0,0 +1,75 @@
|
||||
-module(hg_dummy_provider).
|
||||
-behaviour(woody_server_thrift_handler).
|
||||
|
||||
-export([handle_function/4]).
|
||||
-export([handle_error/4]).
|
||||
|
||||
-behaviour(hg_test_provider).
|
||||
|
||||
-export([get_child_spec/2]).
|
||||
-export([get_url/2]).
|
||||
|
||||
%%
|
||||
|
||||
-spec get_child_spec(inet:hostname() | inet:ip_address(), inet:port_number()) ->
|
||||
supervisor:child_spec().
|
||||
|
||||
get_child_spec(Host, Port) ->
|
||||
{Name, Path, Service} = get_service_spec(),
|
||||
woody_server:child_spec(
|
||||
Name,
|
||||
#{
|
||||
ip => hg_utils:get_hostname_ip(Host),
|
||||
port => Port,
|
||||
net_opts => [],
|
||||
event_handler => hg_woody_event_handler,
|
||||
handlers => [{Path, {Service, ?MODULE, []}}]
|
||||
}
|
||||
).
|
||||
|
||||
-spec get_url(inet:hostname() | inet:ip_address(), inet:port_number()) ->
|
||||
woody_t:url().
|
||||
|
||||
get_url(Host, Port) ->
|
||||
{_Name, Path, _Service} = get_service_spec(),
|
||||
iolist_to_binary(["http://", Host, ":", integer_to_list(Port), Path]).
|
||||
|
||||
get_service_spec() ->
|
||||
{?MODULE, "/test/proxy/provider/dummy",
|
||||
{hg_proxy_provider_thrift, 'ProviderProxy'}}.
|
||||
|
||||
%%
|
||||
|
||||
-include_lib("hg_proto/include/hg_proxy_provider_thrift.hrl").
|
||||
|
||||
-spec handle_function(woody_t:func(), woody_server_thrift_handler:args(), woody_client:context(), []) ->
|
||||
{ok, term()} | no_return().
|
||||
|
||||
handle_function('ProcessPayment', {#'PaymentInfo'{state = undefined}}, _Context, _Opts) ->
|
||||
{ok, sleep(1, <<"sleeping">>)};
|
||||
handle_function('ProcessPayment', {#'PaymentInfo'{state = <<"sleeping">>} = PaymentInfo}, _Context, _Opts) ->
|
||||
{ok, finish(PaymentInfo)};
|
||||
|
||||
handle_function('CapturePayment', {PaymentInfo}, _Context, _Opts) ->
|
||||
{ok, finish(PaymentInfo)};
|
||||
|
||||
handle_function('CancelPayment', {PaymentInfo}, _Context, _Opts) ->
|
||||
{ok, finish(PaymentInfo)}.
|
||||
|
||||
finish(#'PaymentInfo'{payment = Payment}) ->
|
||||
#'ProcessResult'{
|
||||
intent = {finish, #'FinishIntent'{status = {ok, #'Ok'{}}}},
|
||||
trx = #'TransactionInfo'{id = Payment#'InvoicePayment'.id}
|
||||
}.
|
||||
|
||||
sleep(Timeout, State) ->
|
||||
#'ProcessResult'{
|
||||
intent = {sleep, #'SleepIntent'{timer = {timeout, Timeout}}},
|
||||
next_state = State
|
||||
}.
|
||||
|
||||
-spec handle_error(woody_t:func(), term(), woody_client:context(), []) ->
|
||||
_.
|
||||
|
||||
handle_error(_Function, _Reason, _Context, _Opts) ->
|
||||
ok.
|
23
apps/hellgate/test/hg_test_proxy.erl
Normal file
23
apps/hellgate/test/hg_test_proxy.erl
Normal file
@ -0,0 +1,23 @@
|
||||
-module(hg_test_proxy).
|
||||
|
||||
-type host() :: inet:hostname() | inet:ip_address().
|
||||
|
||||
-callback get_child_spec(host(), inet:port_number()) -> supervisor:child_spec().
|
||||
-callback get_url(host(), inet:port_number()) -> woody_t:url().
|
||||
|
||||
-export([get_child_spec/3]).
|
||||
-export([get_url/3]).
|
||||
|
||||
%%
|
||||
|
||||
-spec get_child_spec(module(), host(), inet:port_number()) ->
|
||||
supervisor:child_spec().
|
||||
|
||||
get_child_spec(Module, Host, Port) ->
|
||||
Module:get_child_spec(Host, Port).
|
||||
|
||||
-spec get_url(module(), host(), inet:port_number()) ->
|
||||
supervisor:child_spec().
|
||||
|
||||
get_url(Module, Host, Port) ->
|
||||
Module:get_url(Host, Port).
|
195
apps/hellgate/test/hg_tests_SUITE.erl
Normal file
195
apps/hellgate/test/hg_tests_SUITE.erl
Normal file
@ -0,0 +1,195 @@
|
||||
-module(hg_tests_SUITE).
|
||||
-include_lib("common_test/include/ct.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([invoice_cancellation/1]).
|
||||
-export([overdue_invoice_cancelled/1]).
|
||||
-export([payment_success/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}, []}}.
|
||||
|
||||
%%
|
||||
|
||||
-define(config(Key), begin element(2, lists:keyfind(Key, 1, C)) end).
|
||||
|
||||
%% tests descriptions
|
||||
|
||||
-type config() :: [{atom(), term()}].
|
||||
-type test_case_name() :: atom().
|
||||
|
||||
-spec all() -> [test_case_name()].
|
||||
|
||||
all() ->
|
||||
[
|
||||
invoice_cancellation,
|
||||
overdue_invoice_cancelled,
|
||||
payment_success
|
||||
].
|
||||
|
||||
%% starting/stopping
|
||||
|
||||
-spec init_per_suite(config()) -> config().
|
||||
|
||||
init_per_suite(C) ->
|
||||
Host = "localhost",
|
||||
% Port = rand:uniform(32768) + 32767,
|
||||
Port = 8042,
|
||||
RootUrl = "http://" ++ Host ++ ":" ++ integer_to_list(Port),
|
||||
Apps =
|
||||
hg_ct_helper:start_app(lager) ++
|
||||
hg_ct_helper:start_app(woody) ++
|
||||
hg_ct_helper:start_app(hellgate, [
|
||||
{host, Host},
|
||||
{port, Port},
|
||||
% FIXME:
|
||||
% You will need up and running mgun reachable at the following url,
|
||||
% properly configured to serve incoming requests and talk back to
|
||||
% the test hg instance.
|
||||
{automaton_service_url, <<"http://localhost:8022/v1/automaton_service">>}
|
||||
]),
|
||||
[{root_url, RootUrl}, {apps, lists:reverse(Apps)} | C].
|
||||
|
||||
-spec end_per_suite(config()) -> _.
|
||||
|
||||
end_per_suite(C) ->
|
||||
[application:stop(App) || App <- ?config(apps)].
|
||||
|
||||
%% tests
|
||||
|
||||
-include_lib("hg_proto/include/hg_payment_processing_thrift.hrl").
|
||||
|
||||
-define(ev_invoice_status(Status),
|
||||
#'InvoiceStatusChanged'{invoice = #'Invoice'{status = Status}}).
|
||||
-define(ev_invoice_status(Status, Details),
|
||||
#'InvoiceStatusChanged'{invoice = #'Invoice'{status = Status, details = Details}}).
|
||||
-define(ev_payment_status(PaymentID, Status),
|
||||
#'InvoicePaymentStatusChanged'{payment = #'InvoicePayment'{id = PaymentID, status = Status}}).
|
||||
|
||||
-spec init_per_testcase(test_case_name(), config()) -> config().
|
||||
|
||||
init_per_testcase(_Name, C) ->
|
||||
Client = hg_client:new(?config(root_url), make_userinfo()),
|
||||
{ok, SupPid} = supervisor:start_link(?MODULE, []),
|
||||
[{client, Client}, {test_sup, SupPid} | C].
|
||||
|
||||
-spec end_per_testcase(test_case_name(), config()) -> config().
|
||||
|
||||
end_per_testcase(_Name, C) ->
|
||||
_ = unlink(?config(test_sup)),
|
||||
_ = application:set_env(hellgate, provider_proxy_url, undefined),
|
||||
exit(?config(test_sup), shutdown).
|
||||
|
||||
-spec invoice_cancellation(config()) -> _ | no_return().
|
||||
|
||||
invoice_cancellation(C) ->
|
||||
Client = ?config(client),
|
||||
InvoiceParams = make_invoice_params(<<"rubberduck">>, 10000),
|
||||
{ok, InvoiceID} = hg_client:create_invoice(InvoiceParams, Client),
|
||||
{exception, #'InvalidInvoiceStatus'{}} = hg_client:fulfill_invoice(InvoiceID, <<"perfect">>, Client),
|
||||
ok = hg_client:void_invoice(InvoiceID, <<"whynot">>, Client).
|
||||
|
||||
-spec overdue_invoice_cancelled(config()) -> _ | no_return().
|
||||
|
||||
overdue_invoice_cancelled(C) ->
|
||||
Client = ?config(client),
|
||||
InvoiceParams = make_invoice_params(<<"rubberduck">>, make_due_date(1), 10000),
|
||||
{ok, InvoiceID} = hg_client:create_invoice(InvoiceParams, Client),
|
||||
{ok, ?ev_invoice_status(unpaid)} = hg_client:get_next_event(InvoiceID, 3000, Client),
|
||||
{ok, ?ev_invoice_status(cancelled, <<"overdue">>)} = hg_client:get_next_event(InvoiceID, 3000, Client).
|
||||
|
||||
-spec payment_success(config()) -> _ | no_return().
|
||||
|
||||
payment_success(C) ->
|
||||
Client = ?config(client),
|
||||
ProxyUrl = start_service_handler(hg_dummy_provider, C),
|
||||
ok = application:set_env(hellgate, provider_proxy_url, ProxyUrl),
|
||||
InvoiceParams = make_invoice_params(<<"rubberduck">>, make_due_date(5), 42000),
|
||||
PaymentParams = make_payment_params(),
|
||||
{ok, InvoiceID} = hg_client:create_invoice(InvoiceParams, Client),
|
||||
{ok, ?ev_invoice_status(unpaid)} = hg_client:get_next_event(InvoiceID, 3000, Client),
|
||||
{ok, PaymentID} = hg_client:start_payment(InvoiceID, PaymentParams, Client),
|
||||
{ok, ?ev_payment_status(PaymentID, pending)} = hg_client:get_next_event(InvoiceID, 3000, Client),
|
||||
{ok, ?ev_payment_status(PaymentID, succeeded)} = hg_client:get_next_event(InvoiceID, 3000, Client),
|
||||
% FIXME: will fail when eventlist feature lands in mg
|
||||
timeout = hg_client:get_next_event(InvoiceID, 3000, Client).
|
||||
|
||||
%%
|
||||
|
||||
start_service_handler(Module, C) ->
|
||||
Host = "localhost",
|
||||
Port = get_random_port(),
|
||||
ChildSpec = hg_test_proxy:get_child_spec(Module, Host, Port),
|
||||
{ok, _} = supervisor:start_child(?config(test_sup), ChildSpec),
|
||||
hg_test_proxy:get_url(Module, Host, Port).
|
||||
|
||||
get_random_port() ->
|
||||
rand:uniform(32768) + 32767.
|
||||
|
||||
%%
|
||||
|
||||
make_userinfo() ->
|
||||
#'UserInfo'{id = <<?MODULE_STRING>>}.
|
||||
|
||||
make_invoice_params(Product, Cost) ->
|
||||
make_invoice_params(Product, make_due_date(), Cost).
|
||||
|
||||
make_invoice_params(Product, Due, Cost) ->
|
||||
make_invoice_params(Product, Due, Cost, []).
|
||||
|
||||
make_invoice_params(Product, Due, Amount, Context) when is_integer(Amount) ->
|
||||
make_invoice_params(Product, Due, {Amount, <<"RUB">>}, Context);
|
||||
make_invoice_params(Product, Due, {Amount, Currency}, Context) ->
|
||||
#'InvoiceParams'{
|
||||
product = Product,
|
||||
amount = Amount,
|
||||
due = format_datetime(Due),
|
||||
currency = #'CurrencyRef'{symbolic_code = Currency},
|
||||
context = term_to_binary(Context)
|
||||
}.
|
||||
|
||||
make_payment_params() ->
|
||||
{PaymentTool, Session} = make_payment_tool(),
|
||||
make_payment_params(PaymentTool, Session).
|
||||
|
||||
make_payment_params(PaymentTool, Session) ->
|
||||
#'InvoicePaymentParams'{
|
||||
payer = #'Payer'{},
|
||||
payment_tool = PaymentTool,
|
||||
session = Session
|
||||
}.
|
||||
|
||||
make_payment_tool() ->
|
||||
{
|
||||
{bank_card, #'BankCard'{
|
||||
token = <<"TOKEN42">>,
|
||||
payment_system = visa,
|
||||
bin = <<"424242">>,
|
||||
masked_pan = <<"4242">>
|
||||
}},
|
||||
<<"SESSION42">>
|
||||
}.
|
||||
|
||||
make_due_date() ->
|
||||
make_due_date(24 * 60 * 60).
|
||||
|
||||
make_due_date(LifetimeSeconds) ->
|
||||
genlib_time:unow() + LifetimeSeconds.
|
||||
|
||||
format_datetime(Datetime = {_, _}) ->
|
||||
genlib_format:format_datetime_iso8601(Datetime);
|
||||
format_datetime(Timestamp) when is_integer(Timestamp) ->
|
||||
format_datetime(genlib_time:unixtime_to_daytime(Timestamp)).
|
3
apps/hg_client/rebar.config
Normal file
3
apps/hg_client/rebar.config
Normal file
@ -0,0 +1,3 @@
|
||||
{erl_opts, [
|
||||
{parse_transform, lager_transform}
|
||||
]}.
|
11
apps/hg_client/src/hg_client.app.src
Normal file
11
apps/hg_client/src/hg_client.app.src
Normal file
@ -0,0 +1,11 @@
|
||||
{application, hg_client, [
|
||||
{description, "Hellgate client"},
|
||||
{vsn, "0"},
|
||||
{registered, []},
|
||||
{applications, [
|
||||
kernel,
|
||||
stdlib,
|
||||
woody,
|
||||
hg_proto
|
||||
]}
|
||||
]}.
|
227
apps/hg_client/src/hg_client.erl
Normal file
227
apps/hg_client/src/hg_client.erl
Normal file
@ -0,0 +1,227 @@
|
||||
-module(hg_client).
|
||||
-include_lib("hg_proto/include/hg_payment_processing_thrift.hrl").
|
||||
|
||||
-export([new/2]).
|
||||
-export([new/3]).
|
||||
|
||||
-export([create_invoice/2]).
|
||||
-export([get_invoice/2]).
|
||||
-export([fulfill_invoice/3]).
|
||||
-export([void_invoice/3]).
|
||||
-export([start_payment/3]).
|
||||
|
||||
-export([get_next_event/2]).
|
||||
-export([get_next_event/3]).
|
||||
|
||||
-export_type([t/0]).
|
||||
|
||||
%%
|
||||
|
||||
-behaviour(gen_server).
|
||||
-export([init/1]).
|
||||
-export([handle_call/3]).
|
||||
-export([handle_cast/2]).
|
||||
-export([handle_info/2]).
|
||||
-export([terminate/2]).
|
||||
-export([code_change/3]).
|
||||
|
||||
|
||||
%%
|
||||
|
||||
-behaviour(woody_event_handler).
|
||||
-export([handle_event/3]).
|
||||
|
||||
%%
|
||||
|
||||
-define(POLL_INTERVAL, 1000).
|
||||
-define(DEFAULT_NEXT_EVENT_TIMEOUT, 5000).
|
||||
|
||||
-opaque t() :: pid().
|
||||
|
||||
-type user_info() :: hg_payment_processing_thrift:'UserInfo'().
|
||||
-type invoice_id() :: hg_domain_thrift:'InvoiceID'().
|
||||
-type payment_id() :: hg_domain_thrift:'InvoicePaymentID'().
|
||||
-type event_id() :: hg_payment_processing_thrift:'EventID'().
|
||||
-type invoice_params() :: hg_payment_processing_thrift:'InvoiceParams'().
|
||||
-type payment_params() :: hg_payment_processing_thrift:'InvoicePaymentParams'().
|
||||
|
||||
-spec new(woody_t:url(), user_info()) -> t().
|
||||
|
||||
new(RootUrl, UserInfo) ->
|
||||
new(RootUrl, UserInfo, construct_context()).
|
||||
|
||||
construct_context() ->
|
||||
ReqID = genlib_format:format_int_base(genlib_time:ticks(), 62),
|
||||
woody_client:new_context(ReqID, ?MODULE).
|
||||
|
||||
-spec new(woody_t:url(), user_info(), woody_client:context()) -> t().
|
||||
|
||||
new(RootUrl, UserInfo, Context) ->
|
||||
{ok, Pid} = gen_server:start_link(?MODULE, {RootUrl, UserInfo, Context}, []),
|
||||
Pid.
|
||||
|
||||
%%
|
||||
|
||||
-spec create_invoice(invoice_params(), t()) ->
|
||||
{{ok, invoice_id()} | woody_client:result_error(), t()}.
|
||||
|
||||
create_invoice(InvoiceParams, Client) ->
|
||||
do_service_call(Client, 'Create', [InvoiceParams]).
|
||||
|
||||
-spec get_invoice(invoice_id(), t()) ->
|
||||
{{ok, hg_payment_processing_thrift:'InvoiceState'()} | woody_client:result_error(), t()}.
|
||||
|
||||
get_invoice(InvoiceID, Client) ->
|
||||
do_service_call(Client, 'Get', [InvoiceID]).
|
||||
|
||||
-spec fulfill_invoice(invoice_id(), binary(), t()) ->
|
||||
{ok | woody_client:result_error(), t()}.
|
||||
|
||||
fulfill_invoice(InvoiceID, Reason, Client) ->
|
||||
do_service_call(Client, 'Fulfill', [InvoiceID, Reason]).
|
||||
|
||||
-spec void_invoice(invoice_id(), binary(), t()) ->
|
||||
{ok | woody_client:result_error(), t()}.
|
||||
|
||||
void_invoice(InvoiceID, Reason, Client) ->
|
||||
do_service_call(Client, 'Void', [InvoiceID, Reason]).
|
||||
|
||||
-spec start_payment(invoice_id(), payment_params(), t()) ->
|
||||
{{ok, payment_id()} | woody_client:result_error(), t()}.
|
||||
|
||||
start_payment(InvoiceID, PaymentParams, Client) ->
|
||||
do_service_call(Client, 'StartPayment', [InvoiceID, PaymentParams]).
|
||||
|
||||
-spec get_next_event(invoice_id(), t()) ->
|
||||
{{ok, tuple()} | timeout | woody_client:result_error(), t()}.
|
||||
|
||||
get_next_event(InvoiceID, Client) ->
|
||||
get_next_event(InvoiceID, ?DEFAULT_NEXT_EVENT_TIMEOUT, Client).
|
||||
|
||||
-spec get_next_event(invoice_id(), timeout(), t()) ->
|
||||
{{ok, tuple()} | timeout | woody_client:result_error(), t()}.
|
||||
|
||||
get_next_event(InvoiceID, Timeout, Client) ->
|
||||
% FIXME: infinity sounds dangerous
|
||||
gen_server:call(Client, {get_next_event, InvoiceID, Timeout}, infinity).
|
||||
|
||||
do_service_call(Client, Function, Args) ->
|
||||
% FIXME: infinity sounds dangerous
|
||||
gen_server:call(Client, {issue_service_call, Function, Args}, infinity).
|
||||
|
||||
%%
|
||||
|
||||
-record(cl, {
|
||||
root_url :: woody_t:url(),
|
||||
user_info :: user_info(),
|
||||
context :: woody_client:context(),
|
||||
last_events = #{} :: #{invoice_id() => event_id()}
|
||||
}).
|
||||
|
||||
-type cl() :: #cl{}.
|
||||
-type callref() :: {pid(), Tag :: reference()}.
|
||||
|
||||
-spec init({woody_t:url(), user_info(), woody_client:context()}) ->
|
||||
{ok, cl()}.
|
||||
|
||||
init({RootUrl, UserInfo, Context}) ->
|
||||
{ok, #cl{context = Context, user_info = UserInfo, root_url = RootUrl}}.
|
||||
|
||||
-spec handle_call(term(), callref(), cl()) ->
|
||||
{reply, term(), cl()} | {noreply, cl()}.
|
||||
|
||||
handle_call({issue_service_call, Function, Args}, _From, Client) ->
|
||||
{Result, ClientNext} = issue_service_call(Function, [get_user_info(Client) | Args], Client),
|
||||
{reply, Result, ClientNext};
|
||||
|
||||
handle_call({get_next_event, InvoiceID, Timeout}, _From, Client) ->
|
||||
{Result, ClientNext} = poll_next_event(InvoiceID, Timeout, Client),
|
||||
{reply, Result, ClientNext};
|
||||
|
||||
handle_call(Call, _From, State) ->
|
||||
_ = lager:warning("unexpected call received: ~tp", [Call]),
|
||||
{noreply, State}.
|
||||
|
||||
-spec handle_cast(_, cl()) ->
|
||||
{noreply, cl()}.
|
||||
|
||||
handle_cast(Cast, State) ->
|
||||
_ = lager:warning("unexpected cast received: ~tp", [Cast]),
|
||||
{noreply, State}.
|
||||
|
||||
-spec handle_info(_, cl()) ->
|
||||
{noreply, cl()}.
|
||||
|
||||
handle_info(Info, State) ->
|
||||
_ = lager:warning("unexpected info received: ~tp", [Info]),
|
||||
{noreply, State}.
|
||||
|
||||
-spec terminate(Reason, cl()) ->
|
||||
ok when
|
||||
Reason :: normal | shutdown | {shutdown, term()} | term().
|
||||
|
||||
terminate(_Reason, _State) ->
|
||||
ok.
|
||||
|
||||
-spec code_change(Vsn | {down, Vsn}, cl(), term()) ->
|
||||
{error, noimpl} when
|
||||
Vsn :: term().
|
||||
|
||||
code_change(_OldVsn, _State, _Extra) ->
|
||||
{error, noimpl}.
|
||||
|
||||
%%
|
||||
|
||||
poll_next_event(_InvoiceID, Timeout, Client) when Timeout =< 0 ->
|
||||
{timeout, Client};
|
||||
poll_next_event(InvoiceID, Timeout, Client) ->
|
||||
StartTs = genlib_time:ticks(),
|
||||
UserInfo = get_user_info(Client),
|
||||
Range = construct_range(InvoiceID, Client),
|
||||
{Result, ClientNext} = issue_service_call('GetEvents', [UserInfo, InvoiceID, Range], Client),
|
||||
case Result of
|
||||
{ok, []} ->
|
||||
_ = timer:sleep(?POLL_INTERVAL),
|
||||
poll_next_event(InvoiceID, compute_timeout_left(StartTs, Timeout), ClientNext);
|
||||
{ok, [#'Event'{id = EventID, ev = {_, Event}} | _Rest]} ->
|
||||
{{ok, Event}, update_last_events(InvoiceID, EventID, ClientNext)};
|
||||
{What, _} when What =:= exception; What =:= error ->
|
||||
{Result, ClientNext}
|
||||
end.
|
||||
|
||||
construct_range(InvoiceID, #cl{last_events = LastEvents}) ->
|
||||
#'EventRange'{'after' = genlib_map:get(InvoiceID, LastEvents), limit = 1}.
|
||||
|
||||
update_last_events(InvoiceID, EventID, Client = #cl{last_events = LastEvents}) ->
|
||||
Client#cl{last_events = LastEvents#{InvoiceID => EventID}}.
|
||||
|
||||
issue_service_call(Function, Args, Client = #cl{context = Context, root_url = RootUrl}) ->
|
||||
{_Name, Path, Service} = hg_proto:get_service_spec(invoicing),
|
||||
Url = iolist_to_binary([RootUrl, Path]),
|
||||
Request = {Service, Function, Args},
|
||||
{Result, ContextNext} = woody_client:call_safe(Context, Request, #{url => Url}),
|
||||
{Result, Client#cl{context = ContextNext}}.
|
||||
|
||||
compute_timeout_left(StartTs, TimeoutWas) ->
|
||||
TimeoutWas - (genlib_time:ticks() - StartTs) div 1000.
|
||||
|
||||
get_user_info(#cl{user_info = UserInfo}) ->
|
||||
UserInfo.
|
||||
|
||||
%%
|
||||
|
||||
-spec handle_event(EventType, RpcID, EventMeta)
|
||||
-> _ when
|
||||
EventType :: woody_event_handler:event_type(),
|
||||
RpcID :: woody_t:rpc_id(),
|
||||
EventMeta :: woody_event_handler:event_meta_type().
|
||||
|
||||
handle_event(EventType, RpcID, #{status := error, class := Class, reason := Reason, stack := Stack}) ->
|
||||
lager:error(
|
||||
maps:to_list(RpcID),
|
||||
"[client] ~s with ~s:~p at ~s",
|
||||
[EventType, Class, Reason, genlib_format:format_stacktrace(Stack, [newlines])]
|
||||
);
|
||||
|
||||
handle_event(EventType, RpcID, EventMeta) ->
|
||||
lager:debug(maps:to_list(RpcID), "[client] ~s: ~p", [EventType, EventMeta]).
|
2
apps/hg_proto/.gitignore
vendored
Normal file
2
apps/hg_proto/.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
include/hg_*_thrift.hrl
|
||||
src/hg_*_thrift.erl
|
1
apps/hg_proto/damsel
Submodule
1
apps/hg_proto/damsel
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit 24a247b6baa964445164bde5902a1285ed803b16
|
21
apps/hg_proto/rebar.config
Normal file
21
apps/hg_proto/rebar.config
Normal file
@ -0,0 +1,21 @@
|
||||
{plugins, [
|
||||
{rebar3_thrift_compiler,
|
||||
{git, "https://github.com/rbkmoney/rebar3_thrift_compiler.git", {tag, "0.2"}}}
|
||||
]}.
|
||||
|
||||
{provider_hooks, [
|
||||
{pre, [
|
||||
{compile, {thrift, compile}},
|
||||
{clean, {thrift, clean}}
|
||||
]}
|
||||
]}.
|
||||
|
||||
{thrift_compiler_opts, [
|
||||
{in_dir, "damsel/proto"},
|
||||
{in_files, [
|
||||
"state_processing.thrift",
|
||||
"payment_processing.thrift",
|
||||
"proxy_provider.thrift"
|
||||
]},
|
||||
{gen, "erlang:app_prefix=hg"}
|
||||
]}.
|
10
apps/hg_proto/src/hg_proto.app.src
Normal file
10
apps/hg_proto/src/hg_proto.app.src
Normal file
@ -0,0 +1,10 @@
|
||||
{application, hg_proto, [
|
||||
{description, "Processing protocol definitions"},
|
||||
{vsn, "0"},
|
||||
{registered, []},
|
||||
{applications, [
|
||||
kernel,
|
||||
stdlib,
|
||||
thrift
|
||||
]}
|
||||
]}.
|
26
apps/hg_proto/src/hg_proto.erl
Normal file
26
apps/hg_proto/src/hg_proto.erl
Normal file
@ -0,0 +1,26 @@
|
||||
-module(hg_proto).
|
||||
|
||||
-export([get_service_specs/0]).
|
||||
-export([get_service_spec/1]).
|
||||
|
||||
-export_type([service_spec/0]).
|
||||
|
||||
%%
|
||||
|
||||
-type service_spec() :: {Name :: atom(), Path :: string(), Service :: {module(), atom()}}.
|
||||
|
||||
-spec get_service_specs() -> [service_spec()].
|
||||
|
||||
get_service_specs() ->
|
||||
VersionPrefix = "/v1",
|
||||
[
|
||||
{invoicing, VersionPrefix ++ "/processing/invoicing",
|
||||
{hg_payment_processing_thrift, 'Invoicing'}},
|
||||
{processor, VersionPrefix ++ "/stateproc/processor",
|
||||
{hg_state_processing_thrift, 'Processor'}}
|
||||
].
|
||||
|
||||
-spec get_service_spec(Name :: atom()) -> service_spec() | false.
|
||||
|
||||
get_service_spec(Name) ->
|
||||
lists:keyfind(Name, 1, get_service_specs()).
|
68
apps/hg_proto/src/hg_proto_utils.erl
Normal file
68
apps/hg_proto/src/hg_proto_utils.erl
Normal file
@ -0,0 +1,68 @@
|
||||
-module(hg_proto_utils).
|
||||
|
||||
-export([serialize/2]).
|
||||
-export([deserialize/2]).
|
||||
|
||||
%%
|
||||
|
||||
%% TODO: move it to the thrift runtime lib?
|
||||
|
||||
-type thrift_type() ::
|
||||
thrift_base_type() |
|
||||
thrift_collection_type() |
|
||||
thrift_enum_type() |
|
||||
thrift_struct_type().
|
||||
|
||||
-type thrift_base_type() ::
|
||||
bool |
|
||||
double |
|
||||
i8 |
|
||||
i16 |
|
||||
i32 |
|
||||
i64 |
|
||||
string.
|
||||
|
||||
-type thrift_collection_type() ::
|
||||
{list, thrift_type()} |
|
||||
{set, thrift_type()} |
|
||||
{map, thrift_type(), thrift_type()}.
|
||||
|
||||
-type thrift_enum_type() ::
|
||||
{enum, thrift_type_ref()}.
|
||||
|
||||
-type thrift_struct_type() ::
|
||||
{struct, thrift_struct_flavor(), thrift_type_ref()}.
|
||||
|
||||
-type thrift_struct_flavor() :: struct | union | exception.
|
||||
|
||||
-type thrift_type_ref() :: {module(), Name :: atom()}.
|
||||
|
||||
%%
|
||||
|
||||
-spec serialize(thrift_type(), term()) -> {ok, binary()} | {error, any()}.
|
||||
|
||||
serialize(Type, Data) ->
|
||||
{ok, Trans} = thrift_membuffer_transport:new(),
|
||||
{ok, Proto} = new_protocol(Trans),
|
||||
case thrift_protocol:write(Proto, {Type, Data}) of
|
||||
{NewProto, ok} ->
|
||||
{_, Result} = thrift_protocol:close_transport(NewProto),
|
||||
{ok, Result};
|
||||
{_NewProto, {error, _Reason} = Error} ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec deserialize(thrift_type(), binary()) -> {ok, term()} | {error, any()}.
|
||||
|
||||
deserialize(Type, Data) ->
|
||||
{ok, Trans} = thrift_membuffer_transport:new(Data),
|
||||
{ok, Proto} = new_protocol(Trans),
|
||||
case thrift_protocol:read(Proto, Type) of
|
||||
{_NewProto, {ok, Result}} ->
|
||||
{ok, Result};
|
||||
{_NewProto, {error, _Reason} = Error} ->
|
||||
Error
|
||||
end.
|
||||
|
||||
new_protocol(Trans) ->
|
||||
thrift_binary_protocol:new(Trans, [{strict_read, true}, {strict_write, true}]).
|
@ -1,3 +1,14 @@
|
||||
[
|
||||
{hellgate, []}
|
||||
{lager, [
|
||||
{error_logger_hwm, 600},
|
||||
{handlers, [
|
||||
{lager_console_backend, debug}
|
||||
]}
|
||||
]},
|
||||
|
||||
{hellgate, [
|
||||
{host, "0.0.0.0"},
|
||||
{port, 8042},
|
||||
{automaton_service_url, <<"http://localhost:8022/v1/automaton_service">>}
|
||||
]}
|
||||
].
|
||||
|
70
elvis.config
Normal file
70
elvis.config
Normal file
@ -0,0 +1,70 @@
|
||||
[
|
||||
{elvis, [
|
||||
{config, [
|
||||
#{
|
||||
dirs => [
|
||||
"apps/*/src",
|
||||
"apps/*/test"
|
||||
],
|
||||
filter => "*.erl",
|
||||
ignore => ["_thrift.erl$"],
|
||||
rules => [
|
||||
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
|
||||
{elvis_style, no_tabs},
|
||||
{elvis_style, no_trailing_whitespace},
|
||||
{elvis_style, macro_module_names},
|
||||
{elvis_style, operator_spaces, #{rules => [{right, ","}, {right, "++"}, {left, "++"}]}},
|
||||
{elvis_style, nesting_level, #{level => 3}},
|
||||
{elvis_style, god_modules, #{limit => 25}},
|
||||
{elvis_style, no_if_expression},
|
||||
{elvis_style, invalid_dynamic_call, #{ignore => [elvis]}},
|
||||
{elvis_style, used_ignored_variable},
|
||||
{elvis_style, no_behavior_info},
|
||||
{elvis_style, module_naming_convention, #{regex => "^([a-z][a-z0-9]*_?)*(_SUITE)?$"}},
|
||||
{elvis_style, function_naming_convention, #{regex => "^([a-z][a-z0-9]*_?)*$"}},
|
||||
{elvis_style, state_record_and_type},
|
||||
{elvis_style, no_spec_with_records},
|
||||
{elvis_style, dont_repeat_yourself, #{min_complexity => 10}},
|
||||
{elvis_style, no_debug_call, #{ignore => [elvis, elvis_utils]}}
|
||||
]
|
||||
},
|
||||
#{
|
||||
dirs => ["."],
|
||||
filter => "Makefile",
|
||||
ruleset => makefiles
|
||||
},
|
||||
#{
|
||||
dirs => ["."],
|
||||
filter => "elvis.config",
|
||||
ruleset => elvis_config
|
||||
},
|
||||
#{
|
||||
dirs => ["apps", "apps/*"],
|
||||
filter => "rebar.config",
|
||||
rules => [
|
||||
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
|
||||
{elvis_style, no_tabs},
|
||||
{elvis_style, no_trailing_whitespace}
|
||||
]
|
||||
},
|
||||
#{
|
||||
dirs => ["."],
|
||||
filter => "rebar.config",
|
||||
rules => [
|
||||
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
|
||||
{elvis_style, no_tabs},
|
||||
{elvis_style, no_trailing_whitespace}
|
||||
]
|
||||
},
|
||||
#{
|
||||
dirs => ["apps/*/src"],
|
||||
filter => "*.app.src",
|
||||
rules => [
|
||||
{elvis_style, line_length, #{limit => 120, skip_comments => false}},
|
||||
{elvis_style, no_tabs},
|
||||
{elvis_style, no_trailing_whitespace}
|
||||
]
|
||||
}
|
||||
]}
|
||||
]}
|
||||
].
|
24
packer.json
Normal file
24
packer.json
Normal file
@ -0,0 +1,24 @@
|
||||
{
|
||||
"builders": [
|
||||
{
|
||||
"type": "docker",
|
||||
"image": "rbkmoney/service_erlang",
|
||||
"pull": "true",
|
||||
"commit": "true"
|
||||
}
|
||||
],
|
||||
"provisioners": [
|
||||
{
|
||||
"type": "file",
|
||||
"source": "./_build/prod/rel/hellgate",
|
||||
"destination": "/opt/"
|
||||
}
|
||||
],
|
||||
"post-processors": [
|
||||
{
|
||||
"type": "docker-tag",
|
||||
"repository": "rbkmoney/hellgate"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
52
rebar.config
52
rebar.config
@ -1,15 +1,36 @@
|
||||
{plugins, [
|
||||
rebar3_run
|
||||
]}.
|
||||
|
||||
% Common project erlang options.
|
||||
{erl_opts, [
|
||||
|
||||
% mandatory
|
||||
debug_info,
|
||||
warnings_as_errors
|
||||
warnings_as_errors,
|
||||
warn_export_all,
|
||||
warn_missing_spec,
|
||||
warn_untyped_record,
|
||||
warn_export_vars,
|
||||
|
||||
% by default
|
||||
warn_unused_record,
|
||||
warn_bif_clash,
|
||||
warn_obsolete_guard,
|
||||
warn_unused_vars,
|
||||
warn_shadow_vars,
|
||||
warn_unused_import,
|
||||
warn_unused_function,
|
||||
warn_deprecated_function
|
||||
|
||||
% at will
|
||||
% bin_opt_info
|
||||
% no_auto_import
|
||||
% warn_missing_spec_all
|
||||
|
||||
]}.
|
||||
|
||||
% Common project dependencies.
|
||||
{deps, [
|
||||
{lager, "3.0.2"},
|
||||
{genlib, {git, "https://github.com/rbkmoney/genlib.git", {branch, "master"}}},
|
||||
{woody, {git, "git@github.com:rbkmoney/woody_erlang.git", {branch, "master"}}}
|
||||
]}.
|
||||
|
||||
{xref_checks, [
|
||||
@ -20,9 +41,9 @@
|
||||
]}.
|
||||
|
||||
{relx, [
|
||||
{release, {hellgate, "0.1.0"}, [
|
||||
hellgate,
|
||||
sasl
|
||||
{release, {hellgate, "0.1"}, [
|
||||
sasl,
|
||||
hellgate
|
||||
]},
|
||||
{sys_config, "./config/sys.config"},
|
||||
{vm_args, "./config/vm.args"},
|
||||
@ -31,6 +52,17 @@
|
||||
{extended_start_script, true}
|
||||
]}.
|
||||
|
||||
{dialyzer, [
|
||||
{warnings, [
|
||||
% mandatory
|
||||
unmatched_returns,
|
||||
error_handling,
|
||||
race_conditions,
|
||||
unknown
|
||||
]},
|
||||
{plt_apps, all_deps}
|
||||
]}.
|
||||
|
||||
{profiles, [
|
||||
{prod, [
|
||||
{relx, [
|
||||
@ -42,3 +74,7 @@
|
||||
{deps, []}
|
||||
]}
|
||||
]}.
|
||||
|
||||
{plugins, [
|
||||
rebar3_run
|
||||
]}.
|
||||
|
28
rebar.lock
28
rebar.lock
@ -1 +1,27 @@
|
||||
[].
|
||||
[{<<"certifi">>,{pkg,<<"certifi">>,<<"0.4.0">>},2},
|
||||
{<<"cowboy">>,{pkg,<<"cowboy">>,<<"1.0.4">>},1},
|
||||
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"1.0.2">>},2},
|
||||
{<<"genlib">>,
|
||||
{git,"https://github.com/rbkmoney/genlib.git",
|
||||
{ref,"66db7fe296465a875b6894eb5ac944c90f82f913"}},
|
||||
0},
|
||||
{<<"goldrush">>,{pkg,<<"goldrush">>,<<"0.1.7">>},1},
|
||||
{<<"hackney">>,{pkg,<<"hackney">>,<<"1.5.7">>},1},
|
||||
{<<"idna">>,{pkg,<<"idna">>,<<"1.2.0">>},2},
|
||||
{<<"lager">>,{pkg,<<"lager">>,<<"3.0.2">>},0},
|
||||
{<<"metrics">>,{pkg,<<"metrics">>,<<"1.0.1">>},2},
|
||||
{<<"mimerl">>,{pkg,<<"mimerl">>,<<"1.0.2">>},2},
|
||||
{<<"ranch">>,{pkg,<<"ranch">>,<<"1.2.1">>},2},
|
||||
{<<"snowflake">>,
|
||||
{git,"https://github.com/tel/snowflake.git",
|
||||
{ref,"7a8eab0f12757133623b2151a7913b6d2707b629"}},
|
||||
1},
|
||||
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.0">>},2},
|
||||
{<<"thrift">>,
|
||||
{git,"https://github.com/rbkmoney/thrift_erlang.git",
|
||||
{ref,"4950a4cbb2d79f400a54664cf58e843fd8efcd59"}},
|
||||
1},
|
||||
{<<"woody">>,
|
||||
{git,"git@github.com:rbkmoney/woody_erlang.git",
|
||||
{ref,"bf74d4615060b776d423e138d257bc75de4733cb"}},
|
||||
0}].
|
||||
|
14
wercker.yml
14
wercker.yml
@ -1,4 +1,8 @@
|
||||
box: erlang:18
|
||||
box:
|
||||
id: rbkmoney/build
|
||||
username: $CI_BOT_GIT_USERNAME
|
||||
password: $CI_BOT_GIT_PASSWORD
|
||||
tag: latest
|
||||
|
||||
dev:
|
||||
steps:
|
||||
@ -10,6 +14,14 @@ build:
|
||||
- script:
|
||||
name: rebar update
|
||||
code: make rebar-update
|
||||
- script:
|
||||
name: lint
|
||||
code: |
|
||||
export ELVIS_VERSION="0.2.11"
|
||||
export ELVIS_PATH="/usr/local/bin/elvis"
|
||||
curl -sL -o "${ELVIS_PATH}" "https://github.com/inaka/elvis/releases/download/${ELVIS_VERSION}/elvis"
|
||||
chmod +x "${ELVIS_PATH}"
|
||||
make lint
|
||||
- script:
|
||||
name: run xref
|
||||
code: make xref
|
||||
|
Loading…
Reference in New Issue
Block a user