TD-550: Adds new exceptions support for hold operation (#21)

* Adds new exceptions support for hold operation: `InvalidOperationCurrency`, `OperationContextNotSupported` and `PaymentToolNotSupported`.
* Adds testcases for new exceptions
This commit is contained in:
Aleksey Kashapov 2023-04-06 11:00:38 +03:00 committed by GitHub
parent 3eff7ddf1e
commit 5af0cc663a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 160 additions and 85 deletions

View File

@ -23,7 +23,7 @@
get(BodyType, Config, LimitContext) ->
do(fun() ->
ContextType = lim_config_machine:context_type(Config),
{ok, Operation} = lim_context:get_operation(ContextType, LimitContext),
Operation = unwrap(lim_context:get_operation(ContextType, LimitContext)),
Body = unwrap(get_body_for_operation(BodyType, ContextType, LimitContext)),
apply_op_behaviour(Operation, Body, Config)
end).

View File

@ -538,21 +538,24 @@ mk_shard_id(Prefix, Units, ShardSize) ->
ID = integer_to_binary(abs(Units) div ShardSize),
<<Prefix/binary, "/", ID/binary>>.
-spec mk_scope_prefix(config(), lim_context()) -> prefix().
-spec mk_scope_prefix(config(), lim_context()) -> {ok, prefix()} | {error, lim_context:context_error()}.
mk_scope_prefix(Config, LimitContext) ->
mk_scope_prefix_impl(scope(Config), context_type(Config), LimitContext).
-spec mk_scope_prefix_impl(limit_scope(), context_type(), lim_context()) -> prefix().
-spec mk_scope_prefix_impl(limit_scope(), context_type(), lim_context()) ->
{ok, prefix()} | {error, lim_context:context_error()}.
mk_scope_prefix_impl(Scope, ContextType, LimitContext) ->
do(fun() ->
Bits = enumerate_context_bits(Scope),
lists:foldl(
fun(Bit, Acc) ->
{ok, Value} = extract_context_bit(Bit, ContextType, LimitContext),
Value = unwrap(extract_context_bit(Bit, ContextType, LimitContext)),
append_prefix(Value, Acc)
end,
<<>>,
Bits
).
)
end).
-spec append_prefix(binary(), prefix()) -> prefix().
append_prefix(Fragment, Acc) ->
@ -606,18 +609,20 @@ get_context_bits(terminal) ->
get_context_bits(payer_contact_email) ->
[{prefix, <<"payer_contact_email">>}, {from, payer_contact_email}].
-spec extract_context_bit(context_bit(), context_type(), lim_context()) -> {ok, binary()}.
-spec extract_context_bit(context_bit(), context_type(), lim_context()) ->
{ok, binary()} | {error, lim_context:context_error()}.
extract_context_bit({prefix, Prefix}, _ContextType, _LimitContext) ->
{ok, Prefix};
extract_context_bit({from, payment_tool}, ContextType, LimitContext) ->
{ok, PaymentTool} = lim_context:get_value(ContextType, payment_tool, LimitContext),
case PaymentTool of
{bank_card, #{token := Token, exp_date := {Month, Year}}} ->
case lim_context:get_value(ContextType, payment_tool, LimitContext) of
{ok, {bank_card, #{token := Token, exp_date := {Month, Year}}}} ->
{ok, mk_scope_component([Token, Month, Year])};
{bank_card, #{token := Token, exp_date := undefined}} ->
{ok, {bank_card, #{token := Token, exp_date := undefined}}} ->
{ok, mk_scope_component([Token, <<"undefined">>])};
{digital_wallet, #{id := ID, service := Service}} ->
{ok, mk_scope_component([<<"DW">>, Service, ID])}
{ok, {digital_wallet, #{id := ID, service := Service}}} ->
{ok, mk_scope_component([<<"DW">>, Service, ID])};
{error, _} = Error ->
Error
end;
extract_context_bit({from, ValueName}, ContextType, LimitContext) ->
lim_context:get_value(ContextType, ValueName, LimitContext).
@ -913,22 +918,22 @@ check_calculate_year_shard_id_test() ->
-spec global_scope_empty_prefix_test() -> _.
global_scope_empty_prefix_test() ->
Context = #{context => ?PAYPROC_CTX_INVOICE(?INVOICE(<<"OWNER">>, <<"SHOP">>))},
?assertEqual(<<>>, mk_scope_prefix_impl(ordsets:new(), payment_processing, Context)).
?assertEqual({ok, <<>>}, mk_scope_prefix_impl(ordsets:new(), payment_processing, Context)).
-spec preserve_scope_prefix_order_test_() -> [_TestGen].
preserve_scope_prefix_order_test_() ->
Context = #{context => ?PAYPROC_CTX_INVOICE(?INVOICE(<<"OWNER">>, <<"SHOP">>))},
[
?_assertEqual(
<<"/OWNER/SHOP">>,
{ok, <<"/OWNER/SHOP">>},
mk_scope_prefix_impl(ordsets:from_list([shop, party]), payment_processing, Context)
),
?_assertEqual(
<<"/OWNER/SHOP">>,
{ok, <<"/OWNER/SHOP">>},
mk_scope_prefix_impl(ordsets:from_list([party, shop]), payment_processing, Context)
),
?_assertEqual(
<<"/OWNER/SHOP">>,
{ok, <<"/OWNER/SHOP">>},
mk_scope_prefix_impl(ordsets:from_list([shop]), payment_processing, Context)
)
].
@ -951,19 +956,19 @@ prefix_content_test_() ->
},
[
?_assertEqual(
<<"/terminal/22/2">>,
{ok, <<"/terminal/22/2">>},
mk_scope_prefix_impl(ordsets:from_list([terminal, provider]), payment_processing, Context)
),
?_assertEqual(
<<"/terminal/22/2">>,
{ok, <<"/terminal/22/2">>},
mk_scope_prefix_impl(ordsets:from_list([provider, terminal]), payment_processing, Context)
),
?_assertEqual(
<<"/OWNER/wallet/IDENTITY/WALLET">>,
{ok, <<"/OWNER/wallet/IDENTITY/WALLET">>},
mk_scope_prefix_impl(ordsets:from_list([wallet, identity, party]), withdrawal_processing, WithdrawalContext)
),
?_assertEqual(
<<"/token/2/2022/payer_contact_email/email">>,
{ok, <<"/token/2/2022/payer_contact_email/email">>},
mk_scope_prefix_impl(ordsets:from_list([payer_contact_email, payment_tool]), payment_processing, Context)
)
].

View File

@ -1,6 +1,7 @@
-module(lim_context).
-include_lib("limiter_proto/include/limproto_limiter_thrift.hrl").
-include_lib("damsel/include/dmsl_limiter_config_thrift.hrl").
-export([create/1]).
-export([woody_context/1]).
@ -26,12 +27,20 @@
-type context_inner() :: lim_payproc_context:context() | lim_wthdproc_context:context().
-type context_operation() :: lim_payproc_context:operation() | lim_wthdproc_context:operation().
-type unsupported_error(T) :: {unsupported, T}.
-type operation_context_not_supported_error() ::
{operation_context_not_supported, limproto_limiter_thrift:'LimitContextType'()}.
-type context_error() :: notfound | unsupported_error(_) | operation_context_not_supported_error().
-export_type([t/0]).
-export_type([context_type/0]).
-export_type([context_operation/0]).
-export_type([unsupported_error/1]).
-export_type([operation_context_not_supported_error/0]).
-export_type([context_error/0]).
-callback get_operation(context_inner()) -> {ok, context_operation()} | {error, notfound}.
-callback get_value(_Name :: atom(), context_inner()) -> {ok, term()} | {error, notfound | {unsupported, _}}.
-callback get_value(_Name :: atom(), context_inner()) -> {ok, term()} | {error, notfound | unsupported_error(_)}.
-spec create(woody_context()) -> t().
create(WoodyContext) ->
@ -56,24 +65,34 @@ set_clock(Clock, LimContext) ->
LimContext#{clock => Clock}.
-spec get_operation(context_type(), t()) ->
{ok, context_operation()} | {error, notfound}.
{ok, context_operation()} | {error, notfound | operation_context_not_supported_error()}.
get_operation(Type, Context) ->
{Mod, OperationContext} = get_operation_context(Type, Context),
Mod:get_operation(OperationContext).
case get_operation_context(Type, Context) of
{error, _} = Error -> Error;
{ok, Mod, OperationContext} -> Mod:get_operation(OperationContext)
end.
-spec get_value(context_type(), atom(), t()) ->
{ok, term()} | {error, notfound | {unsupported, _}}.
-spec get_value(context_type(), atom(), t()) -> {ok, term()} | {error, context_error()}.
get_value(Type, ValueName, Context) ->
{Mod, OperationContext} = get_operation_context(Type, Context),
Mod:get_value(ValueName, OperationContext).
case get_operation_context(Type, Context) of
{error, _} = Error -> Error;
{ok, Mod, OperationContext} -> Mod:get_value(ValueName, OperationContext)
end.
get_operation_context(payment_processing, #{context := #limiter_LimitContext{payment_processing = undefined}}) ->
{error,
{operation_context_not_supported,
{withdrawal_processing, #limiter_config_LimitContextTypeWithdrawalProcessing{}}}};
get_operation_context(
payment_processing,
#{context := #limiter_LimitContext{payment_processing = PayprocContext}}
) ->
{lim_payproc_context, PayprocContext};
{ok, lim_payproc_context, PayprocContext};
get_operation_context(withdrawal_processing, #{context := #limiter_LimitContext{withdrawal_processing = undefined}}) ->
{error,
{operation_context_not_supported, {payment_processing, #limiter_config_LimitContextTypePaymentProcessing{}}}};
get_operation_context(
withdrawal_processing,
#{context := #limiter_LimitContext{withdrawal_processing = WithdrawalContext}}
) ->
{lim_wthdproc_context, WithdrawalContext}.
{ok, lim_wthdproc_context, WithdrawalContext}.

View File

@ -90,6 +90,19 @@ handle_get_error(Error) ->
-spec handle_hold_error(_) -> no_return().
handle_hold_error({_, {invalid_request, Errors}}) ->
woody_error:raise(business, #base_InvalidRequest{errors = Errors});
handle_hold_error({_, {invalid_operation_currency, {Currency, ExpectedCurrency}}}) ->
woody_error:raise(business, #limiter_InvalidOperationCurrency{
currency = Currency,
expected_currency = ExpectedCurrency
});
handle_hold_error({_, {operation_context_not_supported, ContextType}}) ->
woody_error:raise(business, #limiter_OperationContextNotSupported{
context_type = ContextType
});
handle_hold_error({_, {unsupported, {payment_tool, Type}}}) ->
woody_error:raise(business, #limiter_PaymentToolNotSupported{
payment_tool = atom_to_binary(Type)
});
handle_hold_error(Error) ->
handle_default_error(Error).

View File

@ -16,11 +16,15 @@
-define(DEFAULT_FACTOR_NAME, <<"DEFAULT">>).
-spec convert(lim_body:cash(), lim_body:currency(), config(), limit_context()) ->
{ok, lim_body:cash()}
| {error, conversion_error()}.
{ok, lim_body:cash()} | {error, conversion_error() | lim_context:context_error()}.
convert(#{amount := Amount, currency := Currency}, DestinationCurrency, Config, LimitContext) ->
ContextType = lim_config_machine:context_type(Config),
{ok, Timestamp} = lim_context:get_value(ContextType, created_at, LimitContext),
case lim_context:get_value(ContextType, created_at, LimitContext) of
{ok, Timestamp} -> do_convert(Timestamp, Amount, Currency, DestinationCurrency, LimitContext);
{error, _} = Error -> Error
end.
do_convert(Timestamp, Amount, Currency, DestinationCurrency, LimitContext) ->
Request = #rate_ConversionRequest{
source = Currency,
destination = DestinationCurrency,

View File

@ -7,14 +7,15 @@
-type stage() :: hold | commit.
-type t() :: number | {amount, currency()}.
-type invalid_request_error() :: {invalid_request, list(binary())}.
-type invalid_operation_currency_error() :: {invalid_operation_currency, {currency(), currency()}}.
-export_type([t/0]).
-export_type([invalid_operation_currency_error/0]).
%%
-spec compute(t(), stage(), lim_config_machine:config(), lim_context:t()) ->
{ok, amount()} | {error, lim_rates:conversion_error()} | {error, invalid_request_error()}.
{ok, amount()} | {error, lim_rates:conversion_error()} | {error, invalid_operation_currency_error()}.
compute(number, hold, Config, LimitContext) ->
#{amount := Amount} = get_body(Config, LimitContext),
{ok, sign(Amount)};
@ -51,18 +52,12 @@ denominate(#{amount := Amount, currency := Currency}, Currency, _Config, _LimitC
{ok, Amount};
denominate(Body = #{currency := Currency}, DestinationCurrency, Config, LimitContext) ->
case genlib_app:env(limiter, currency_conversion, disabled) of
disabled -> invalid_request_currencies_mismatch(Currency, DestinationCurrency);
disabled -> currencies_mismatch_error(Currency, DestinationCurrency);
enabled -> convert_currency(Body, DestinationCurrency, Config, LimitContext)
end.
invalid_request_currencies_mismatch(Currency, DestinationCurrency) ->
{error,
{invalid_request, [
genlib:format(
"Invalid operation currency: ~p, expected limit currency: ~p",
[Currency, DestinationCurrency]
)
]}}.
currencies_mismatch_error(Currency, ExpectedCurrency) ->
{error, {invalid_operation_currency, {Currency, ExpectedCurrency}}}.
convert_currency(Body, DestinationCurrency, Config, LimitContext) ->
case lim_rates:convert(Body, DestinationCurrency, Config, LimitContext) of

View File

@ -26,7 +26,10 @@
-type hold_error() ::
lim_rates:conversion_error()
| lim_accounting:invalid_request_error().
| lim_accounting:invalid_request_error()
| lim_turnover_metric:invalid_operation_currency_error()
| lim_context:operation_context_not_supported_error()
| lim_context:unsupported_error({payment_tool, atom()}).
-type commit_error() ::
{forbidden_operation_amount, forbidden_operation_amount_error()}
@ -47,7 +50,7 @@
-spec get_limit(lim_id(), config(), lim_context()) -> {ok, limit()} | {error, get_limit_error()}.
get_limit(LimitID, Config, LimitContext) ->
do(fun() ->
{LimitRangeID, TimeRange} = compute_limit_time_range_location(LimitID, Config, LimitContext),
{LimitRangeID, TimeRange} = unwrap(compute_limit_time_range_location(LimitID, Config, LimitContext)),
Amount = find_range_balance_amount(LimitRangeID, TimeRange, LimitContext),
#limiter_Limit{
id = LimitRangeID,
@ -68,7 +71,7 @@ find_range_balance_amount(LimitRangeID, TimeRange, LimitContext) ->
-spec hold(lim_change(), config(), lim_context()) -> ok | {error, hold_error()}.
hold(LimitChange = #limiter_LimitChange{id = LimitID}, Config, LimitContext) ->
do(fun() ->
TimeRangeAccount = ensure_limit_time_range(LimitID, Config, LimitContext),
TimeRangeAccount = unwrap(ensure_limit_time_range(LimitID, Config, LimitContext)),
Metric = unwrap(compute_metric(hold, Config, LimitContext)),
Posting = lim_posting:new(TimeRangeAccount, Metric, currency(Config)),
unwrap(lim_accounting:hold(construct_plan_id(LimitChange), {1, [Posting]}, LimitContext))
@ -77,7 +80,7 @@ hold(LimitChange = #limiter_LimitChange{id = LimitID}, Config, LimitContext) ->
-spec commit(lim_change(), config(), lim_context()) -> ok | {error, commit_error()}.
commit(LimitChange = #limiter_LimitChange{id = LimitID}, Config, LimitContext) ->
do(fun() ->
TimeRangeAccount = ensure_limit_time_range(LimitID, Config, LimitContext),
TimeRangeAccount = unwrap(ensure_limit_time_range(LimitID, Config, LimitContext)),
PlanID = construct_plan_id(LimitChange),
Operations = construct_commit_plan(TimeRangeAccount, Config, LimitContext),
ok = lists:foreach(
@ -101,42 +104,49 @@ commit(LimitChange = #limiter_LimitChange{id = LimitID}, Config, LimitContext) -
-spec rollback(lim_change(), config(), lim_context()) -> ok | {error, rollback_error()}.
rollback(LimitChange = #limiter_LimitChange{id = LimitID}, Config, LimitContext) ->
do(fun() ->
TimeRangeAccount = ensure_limit_time_range(LimitID, Config, LimitContext),
TimeRangeAccount = unwrap(ensure_limit_time_range(LimitID, Config, LimitContext)),
Metric = unwrap(compute_metric(hold, Config, LimitContext)),
Posting = lim_posting:new(TimeRangeAccount, Metric, currency(Config)),
unwrap(lim_accounting:rollback(construct_plan_id(LimitChange), [{1, [Posting]}], LimitContext))
end).
compute_limit_time_range_location(LimitID, Config, LimitContext) ->
Timestamp = get_timestamp(Config, LimitContext),
LimitRangeID = construct_range_id(LimitID, Timestamp, Config, LimitContext),
do(fun() ->
Timestamp = unwrap(get_timestamp(Config, LimitContext)),
LimitRangeID = unwrap(construct_range_id(LimitID, Timestamp, Config, LimitContext)),
TimeRange = lim_config_machine:calculate_time_range(Timestamp, Config),
{LimitRangeID, TimeRange}.
{LimitRangeID, TimeRange}
end).
ensure_limit_time_range(LimitID, Config, LimitContext) ->
Timestamp = get_timestamp(Config, LimitContext),
{LimitRangeID, TimeRange} = compute_limit_time_range_location(LimitID, Config, LimitContext),
do(fun() ->
Timestamp = unwrap(get_timestamp(Config, LimitContext)),
{LimitRangeID, TimeRange} = unwrap(compute_limit_time_range_location(LimitID, Config, LimitContext)),
CreateParams = genlib_map:compact(#{
id => LimitRangeID,
type => lim_config_machine:time_range_type(Config),
created_at => Timestamp,
currency => currency(Config)
}),
unwrap(lim_range_machine:ensure_exists(CreateParams, TimeRange, LimitContext)).
unwrap(lim_range_machine:ensure_exists(CreateParams, TimeRange, LimitContext))
end).
get_timestamp(Config, LimitContext) ->
ContextType = lim_config_machine:context_type(Config),
{ok, Timestamp} = lim_context:get_value(ContextType, created_at, LimitContext),
Timestamp.
lim_context:get_value(ContextType, created_at, LimitContext).
construct_plan_id(#limiter_LimitChange{change_id = ChangeID}) ->
% DISCUSS
ChangeID.
construct_range_id(LimitID, Timestamp, Config, LimitContext) ->
Prefix = lim_config_machine:mk_scope_prefix(Config, LimitContext),
case lim_config_machine:mk_scope_prefix(Config, LimitContext) of
{ok, Prefix} ->
ShardID = lim_config_machine:calculate_shard_id(Timestamp, Config),
<<LimitID/binary, Prefix/binary, "/", ShardID/binary>>.
{ok, <<LimitID/binary, Prefix/binary, "/", ShardID/binary>>};
{error, _} = Error ->
Error
end.
construct_commit_plan(TimeRangeAccount, Config, LimitContext) ->
MetricHold = unwrap(compute_metric(hold, Config, LimitContext)),

View File

@ -3,7 +3,6 @@
-include_lib("limiter_proto/include/limproto_context_withdrawal_thrift.hrl").
-include_lib("limiter_proto/include/limproto_base_thrift.hrl").
-include_lib("damsel/include/dmsl_wthd_domain_thrift.hrl").
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
-behaviour(lim_context).
-export([get_operation/1]).

View File

@ -20,7 +20,9 @@
-export([commit_with_default_exchange/1]).
-export([partial_commit_with_exchange/1]).
-export([commit_with_exchange/1]).
-export([commit_with_disabled_exchange/1]).
-export([hold_with_disabled_exchange/1]).
-export([hold_with_wrong_operation_context/1]).
-export([hold_with_wrong_payment_tool/1]).
-export([get_limit_ok/1]).
-export([get_limit_notfound/1]).
-export([hold_ok/1]).
@ -76,7 +78,9 @@ groups() ->
commit_with_default_exchange,
partial_commit_with_exchange,
commit_with_exchange,
commit_with_disabled_exchange,
hold_with_disabled_exchange,
hold_with_wrong_operation_context,
hold_with_wrong_payment_tool,
get_limit_ok,
get_limit_notfound,
hold_ok,
@ -101,7 +105,9 @@ groups() ->
commit_with_default_exchange,
partial_commit_with_exchange,
commit_with_exchange,
commit_with_disabled_exchange,
hold_with_disabled_exchange,
hold_with_wrong_operation_context,
hold_with_wrong_payment_tool,
get_limit_ok,
get_limit_notfound,
hold_ok,
@ -280,15 +286,39 @@ commit_with_exchange(C) ->
{ok, {vector, _}} = hold_and_commit(?LIMIT_CHANGE(ID, ?CHANGE_ID, Version), Context, ?config(client, C)),
{ok, #limiter_Limit{amount = 10500}} = lim_client:get(ID, Version, Context, ?config(client, C)).
-spec commit_with_disabled_exchange(config()) -> _.
commit_with_disabled_exchange(C) ->
-spec hold_with_disabled_exchange(config()) -> _.
hold_with_disabled_exchange(C) ->
ok = application:set_env(limiter, currency_conversion, disabled),
Rational = #base_Rational{p = 1000000, q = 100},
_ = mock_exchange(Rational, C),
{ID, Version} = configure_limit(?time_range_month(), ?global(), C),
Cost = ?cash(10000, <<"USD">>),
ConfiguredCurrency = <<"RUB">>,
{ID, Version} = configure_limit(?time_range_month(), ?global(), ?turnover_metric_amount(ConfiguredCurrency), C),
Currency = <<"USD">>,
Cost = ?cash(10000, Currency),
Context = ?payproc_ctx_invoice(Cost),
{exception, #base_InvalidRequest{}} =
{exception, #limiter_InvalidOperationCurrency{currency = Currency, expected_currency = ConfiguredCurrency}} =
lim_client:hold(?LIMIT_CHANGE(ID, ?CHANGE_ID, Version), Context, ?config(client, C)).
-spec hold_with_wrong_operation_context(config()) -> _.
hold_with_wrong_operation_context(C) ->
Rational = #base_Rational{p = 1000000, q = 100},
_ = mock_exchange(Rational, C),
{ID, Version} = configure_limit(?time_range_month(), ?global(), C),
Cost = ?cash(10000),
Context = ?wthdproc_ctx_withdrawal(Cost),
{exception, #limiter_OperationContextNotSupported{
context_type = {withdrawal_processing, #limiter_config_LimitContextTypeWithdrawalProcessing{}}
}} =
lim_client:hold(?LIMIT_CHANGE(ID, ?CHANGE_ID, Version), Context, ?config(client, C)).
-spec hold_with_wrong_payment_tool(config()) -> _.
hold_with_wrong_payment_tool(C) ->
Rational = #base_Rational{p = 1000000, q = 100},
_ = mock_exchange(Rational, C),
{ID, Version} = configure_limit(?time_range_week(), ?scope([?scope_payment_tool()]), ?turnover_metric_number(), C),
NotSupportedPaymentTool = {crypto_currency, #domain_CryptoCurrencyRef{id = <<"wow;so-cryptic;much-hidden">>}},
Context = ?payproc_ctx_payment(?invoice_payment(?cash(10000), ?cash(10000), NotSupportedPaymentTool)),
{exception, #limiter_PaymentToolNotSupported{payment_tool = <<"crypto_currency">>}} =
lim_client:hold(?LIMIT_CHANGE(ID, ?CHANGE_ID, Version), Context, ?config(client, C)).
-spec get_limit_ok(config()) -> _.

View File

@ -9,7 +9,7 @@
{<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.11.0">>},2},
{<<"damsel">>,
{git,"https://github.com/valitydev/damsel.git",
{ref,"9859ca2843ad1617a0bf8549c125a7e94d1d54b7"}},
{ref,"698c7d275fbe6a8f06f209638ede093f3134fc9b"}},
0},
{<<"dmt_client">>,
{git,"https://github.com/valitydev/dmt_client.git",
@ -33,7 +33,7 @@
{<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},1},
{<<"limiter_proto">>,
{git,"https://github.com/valitydev/limiter-proto.git",
{ref,"9b76200a957c0e91bcdf6f16dfbab90d38a3f173"}},
{ref,"e46f732ae357ae302b9b9e4cd8e95725bfd060ee"}},
0},
{<<"machinery">>,
{git,"https://github.com/valitydev/machinery-erlang.git",