TD-288: Stop responding w/ payment tool tokens (#11)

* Bump to valitydev/swag-payments@c39e50b
* Also drop legacy idemp features handling
* Simplify bender client module
* Increase test coverage
* Disentangle conflict error and `logic_error`
* Deduplicate testcases code
This commit is contained in:
Andrew Mayorov 2022-05-16 11:16:36 +03:00 committed by GitHub
parent d4fe7e16bd
commit 7f28b21d25
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 180 additions and 1762 deletions

View File

@ -1,9 +0,0 @@
-ifndef(__capi_feature_schemas_legacy__).
-define(__capi_feature_schemas_legacy__, 42).
% Marking some feature as `discriminator` will make featureset comparator consider two sets with different
% `discriminator` values as _different everywhere_ which usually helps with diff readability.
-define(discriminator, -1).
-define(difference, -1).
-endif.

View File

@ -1,7 +1,6 @@
-module(capi_bender).
-include_lib("bender_proto/include/bender_thrift.hrl").
-include_lib("bender_proto/include/msgpack_thrift.hrl").
-type id() :: binary().
-type idempotent_key_prefix() :: binary() | atom().
@ -9,17 +8,11 @@
-type issuer_id() :: dmsl_domain_thrift:'PartyID'() | dmsl_payment_processing_thrift:'UserID'().
-type idempotent_key() :: binary().
-type idempotent_key_params() :: {idempotent_key_prefix(), issuer_id(), external_id() | undefined}.
%% TODO(ED-287): remove identity_request() from below
-opaque identity() :: {identity, identity_features(), identity_schema(), identity_request()}.
-opaque identity() :: {identity, identity_features(), identity_schema()}.
-type identity_features() :: feat:features().
%% TODO(ED-287): switch back to passing schema by value (`schemas:schema()`)
%% and not by name (`schema`) after V2 is removed
%% -type identity_schema() :: feat:schema().
-type identity_schema_name() :: atom().
-type identity_schema() :: identity_schema_name().
-type identity_schema() :: feat:schema().
-type identity_request() :: feat:request().
-type woody_context() :: woody_context:ctx().
-type context_data() :: #{binary() => term()}.
@ -34,6 +27,8 @@
-type external_id_conflict() :: {external_id_conflict, id(), difference(), identity_schema()}.
-type generation_error() :: external_id_conflict().
-type throws(_T) :: no_return().
-export_type([id/0]).
-export_type([external_id/0]).
-export_type([issuer_id/0]).
@ -50,50 +45,32 @@
-export([gen_snowflake/3]).
-export([gen_snowflake/4]).
-export([try_gen_snowflake/3]).
-export([try_gen_snowflake/4]).
-export([gen_sequence/4]).
-export([gen_sequence/5]).
-export([gen_sequence/6]).
-export([try_gen_sequence/6]).
-export([gen_constant/4]).
-export([gen_constant/5]).
-export([try_gen_constant/4]).
-export([try_gen_constant/5]).
-export([make_identity/2]).
-export([get_internal_id/2]).
-define(BENDER_NAMESPACE, <<"capi">>).
%% deprecated
-define(SCHEMA_VER2, 2).
-define(SCHEMA_VER3, 3).
-spec gen_snowflake(idempotent_key_params() | undefined, identity(), woody_context()) ->
{ok, id()} | {ok, id(), context_data()} | {error, generation_error()}.
id() | throws(generation_error()).
gen_snowflake(IdempotentKey, Identity, WoodyContext) ->
Context = #{},
gen_snowflake(IdempotentKey, Identity, WoodyContext, Context).
-spec gen_snowflake(idempotent_key_params() | undefined, identity(), woody_context(), context_data()) ->
{ok, id()} | {ok, id(), context_data()} | {error, generation_error()}.
id() | throws(generation_error()).
gen_snowflake(IdempotentKey, Identity, WoodyContext, Context) ->
IdSchema = {snowflake, #bender_SnowflakeSchema{}},
generate_id(IdSchema, IdempotentKey, Identity, WoodyContext, Context).
-spec try_gen_snowflake(idempotent_key_params() | undefined, identity(), woody_context()) -> id().
try_gen_snowflake(IdempotentKey, Identity, WoodyContext) ->
Context = #{},
try_gen_snowflake(IdempotentKey, Identity, WoodyContext, Context).
-spec try_gen_snowflake(idempotent_key_params() | undefined, identity(), woody_context(), context_data()) ->
id().
try_gen_snowflake(IdempotentKey, Identity, WoodyContext, Context) ->
IdSchema = {snowflake, #bender_SnowflakeSchema{}},
try_generate_id(IdSchema, IdempotentKey, Identity, WoodyContext, Context).
-spec gen_sequence(idempotent_key_params() | undefined, identity(), sequence_id(), woody_context()) ->
{ok, id()} | {ok, id(), context_data()} | {error, generation_error()}.
id() | throws(generation_error()).
gen_sequence(IdempotentKey, Identity, SequenceID, WoodyContext) ->
SequenceParams = #{},
gen_sequence(IdempotentKey, Identity, SequenceID, SequenceParams, WoodyContext).
@ -104,7 +81,7 @@ gen_sequence(IdempotentKey, Identity, SequenceID, WoodyContext) ->
sequence_id(),
sequence_params(),
woody_context()
) -> {ok, id()} | {ok, id(), context_data()} | {error, generation_error()}.
) -> id() | throws(generation_error()).
gen_sequence(IdempotentKey, Identity, SequenceID, SequenceParams, WoodyContext) ->
Context = #{},
gen_sequence(IdempotentKey, Identity, SequenceID, SequenceParams, WoodyContext, Context).
@ -116,61 +93,27 @@ gen_sequence(IdempotentKey, Identity, SequenceID, SequenceParams, WoodyContext)
sequence_params(),
woody_context(),
context_data()
) -> {ok, id()} | {ok, id(), context_data()} | {error, generation_error()}.
) -> id() | throws(generation_error()).
gen_sequence(IdempotentKey, Identity, SequenceID, SequenceParams, WoodyContext, Context) ->
IdSchema = build_sequence_schema(SequenceID, SequenceParams),
generate_id(IdSchema, IdempotentKey, Identity, WoodyContext, Context).
-spec try_gen_sequence(
idempotent_key_params() | undefined,
identity(),
sequence_id(),
sequence_params(),
woody_context(),
context_data()
) -> id() | no_return().
try_gen_sequence(IdempotentKey, Identity, SequenceID, SequenceParams, WoodyContext, ContextData) ->
IdSchema = build_sequence_schema(SequenceID, SequenceParams),
try_generate_id(IdSchema, IdempotentKey, Identity, WoodyContext, ContextData).
try_generate_id(IdSchema, IdempotentKey, Identity, WoodyContext, Context).
-spec gen_constant(idempotent_key_params(), identity(), constant_id(), woody_context()) ->
{ok, id()} | {ok, id(), context_data()} | {error, generation_error()}.
id() | throws(generation_error()).
gen_constant(IdempotentKey, Identity, ConstantID, WoodyContext) ->
Context = #{},
gen_constant(IdempotentKey, Identity, ConstantID, WoodyContext, Context).
-spec gen_constant(idempotent_key_params(), identity(), constant_id(), woody_context(), context_data()) ->
{ok, id()} | {ok, id(), context_data()} | {error, generation_error()}.
id() | throws(generation_error()).
gen_constant(IdempotentKey, Identity, ConstantID, WoodyContext, Context) ->
IdSchema = {constant, #bender_ConstantSchema{internal_id = ConstantID}},
generate_id(IdSchema, IdempotentKey, Identity, WoodyContext, Context).
-spec try_gen_constant(idempotent_key_params(), identity(), constant_id(), woody_context()) -> id().
try_gen_constant(IdempotentKey, Identity, ConstantID, WoodyContext) ->
Context = #{},
try_gen_constant(IdempotentKey, Identity, ConstantID, WoodyContext, Context).
-spec try_gen_constant(idempotent_key_params(), identity(), constant_id(), woody_context(), context_data()) ->
id().
try_gen_constant(IdempotentKey, Identity, ConstantID, WoodyContext, Context) ->
IdSchema = {constant, #bender_ConstantSchema{internal_id = ConstantID}},
try_generate_id(IdSchema, IdempotentKey, Identity, WoodyContext, Context).
-spec make_identity(identity_schema(), identity_request()) -> identity().
make_identity(Schema, Data) ->
Features = feat:read(read_schema(Schema), Data),
{identity, Features, Schema, Data}.
transform_identity_to_deprecated_v2({identity, _NewFeatures, Schema, Data}) ->
LegacyFeatures = capi_idemp_features_legacy:read(read_schema_deprecated_v2(Schema), Data),
{identity, LegacyFeatures, Schema, Data}.
%% TODO(ED-287): (see above)
read_schema(SchemaName) when is_atom(SchemaName) ->
capi_feature_schemas:SchemaName().
read_schema_deprecated_v2(SchemaName) when is_atom(SchemaName) ->
capi_feature_schemas_legacy:SchemaName().
Features = feat:read(Schema, Data),
{identity, Features, Schema}.
-spec get_internal_id(idempotent_key_params(), woody_context()) ->
{ok, binary(), context_data()} | {error, internal_id_not_found}.
@ -208,14 +151,8 @@ try_generate_id(BenderIdSchema, IdempotentKey, Identity, WoodyContext, CtxData)
case generate_id(BenderIdSchema, IdempotentKey, Identity, WoodyContext, CtxData) of
{ok, ID} ->
ID;
{error, {Err, ID, Difference, Schema}} when Err == external_id_conflict; Err == external_id_conflict_legacy ->
ReadableDiff =
case Err of
external_id_conflict ->
feat:list_diff_fields(read_schema(Schema), Difference);
external_id_conflict_legacy ->
capi_idemp_features_legacy:list_diff_fields(read_schema_deprecated_v2(Schema), Difference)
end,
{error, {external_id_conflict, ID, Difference, Schema}} ->
ReadableDiff = feat:list_diff_fields(Schema, Difference),
logger:warning("This externalID: ~p, used in another request.~nDifference: ~p", [ID, ReadableDiff]),
SourceID = get_external_id(IdempotentKey),
throw({external_id_conflict, ID, SourceID, Schema})
@ -241,19 +178,11 @@ make_idempotent_key({Prefix, PartyID, ExternalID}) ->
bender_client:get_idempotent_key(?BENDER_NAMESPACE, Prefix, PartyID, ExternalID).
bender_generate_id(BenderIdSchema, IdempKey, Identity, WoodyContext, CtxData) ->
{identity, Features, Schema, _Data} = Identity,
{identity, Features, Schema} = Identity,
BenderCtx = build_bender_ctx(Features, CtxData),
case bender_client:gen_id(IdempKey, BenderIdSchema, WoodyContext, BenderCtx) of
{ok, ID} ->
{ok, ID};
{ok, ID, #{<<"version">> := ?SCHEMA_VER2} = SavedBenderCtx} ->
{identity, FeaturesDeprecated, Schema, _} = transform_identity_to_deprecated_v2(Identity),
check_idempotent_conflict_deprecated_v2(
ID,
FeaturesDeprecated,
SavedBenderCtx,
Schema
);
{ok, ID, #{<<"version">> := ?SCHEMA_VER3} = SavedBenderCtx} ->
check_idempotent_conflict(ID, Features, SavedBenderCtx, Schema)
end.
@ -283,20 +212,6 @@ check_idempotent_conflict(ID, Features, SavedBenderCtx, Schema) ->
{error, {external_id_conflict, ID, Difference, Schema}}
end.
%% Deprecated idempotent context
check_idempotent_conflict_deprecated_v2(ID, Features, SavedBenderCtx, Schema) ->
#{
<<"version">> := ?SCHEMA_VER2,
<<"features">> := OtherFeatures
} = SavedBenderCtx,
case capi_idemp_features_legacy:compare(Features, OtherFeatures) of
true ->
{ok, ID};
{false, Difference} ->
{error, {external_id_conflict_legacy, ID, Difference, Schema}}
end.
-spec get_context_data(bender_context()) -> undefined | context_data().
get_context_data(Context) ->
maps:get(<<"context_data">>, Context, #{}).

View File

@ -1,984 +0,0 @@
-module(capi_feature_schemas_legacy).
-type schema() :: capi_idemp_features_legacy:schema().
-include("capi_feature_schemas_legacy.hrl").
-define(id, 1).
-define(invoice_id, 2).
-define(make_recurrent, 3).
-define(flow, 4).
-define(hold_exp, 5).
-define(payer, 6).
-define(payment_tool, 7).
-define(token, 8).
-define(bank_card, 9).
-define(exp_date, 10).
-define(terminal, 11).
-define(terminal_type, 12).
-define(wallet, 13).
-define(provider, 14).
-define(crypto, 15).
-define(currency, 16).
-define(mobile_commerce, 17).
-define(operator, 18).
-define(phone, 19).
-define(customer, 20).
-define(recurrent, 21).
-define(invoice, 22).
-define(payment, 23).
-define(shop_id, 24).
-define(amount, 25).
-define(product, 26).
-define(due_date, 27).
-define(cart, 28).
-define(quantity, 29).
-define(price, 30).
-define(tax, 31).
-define(rate, 32).
-define(bank_account, 33).
-define(account, 34).
-define(bank_bik, 35).
-define(payment_resource, 36).
-define(payment_session, 37).
-define(lifetime, 38).
-define(details, 39).
-define(days, 40).
-define(months, 41).
-define(years, 42).
-define(single_line, 43).
-define(multiline, 44).
-define(range, 45).
-define(fixed, 46).
-define(lower_bound, 47).
-define(upper_bound, 48).
-define(invoice_template_id, 49).
-define(contact_info, 50).
-define(email, 51).
-define(phone_number, 52).
-define(allocation, 53).
-define(target, 54).
-define(total, 55).
-define(fee, 56).
-define(share, 57).
-define(matisse, 58).
-define(exponent, 59).
-export([payment/0]).
-export([invoice/0]).
-export([invoice_template/0]).
-export([refund/0]).
-export([customer_binding/0]).
-export([customer/0]).
-spec payment() -> schema().
payment() ->
#{
?invoice_id => [<<"invoiceID">>],
?make_recurrent => [<<"makeRecurrent">>],
?flow => [
<<"flow">>,
#{
?discriminator => [<<"type">>],
?hold_exp => [<<"onHoldExpiration">>]
}
],
?payer => [
<<"payer">>,
#{
?discriminator => [<<"payerType">>],
?payment_tool => [<<"paymentTool">>, payment_tool_schema()],
?customer => [<<"customerID">>],
?recurrent => [
<<"recurrentParentPayment">>,
#{
?invoice => [<<"invoiceID">>],
?payment => [<<"paymentID">>]
}
]
}
]
}.
-spec invoice() -> schema().
invoice() ->
#{
?shop_id => [<<"shopID">>],
?amount => [<<"amount">>],
?currency => [<<"currency">>],
?product => [<<"product">>],
?due_date => [<<"dueDate">>],
?cart => [<<"cart">>, {set, cart_line_schema()}],
?bank_account => [<<"bankAccount">>, bank_account_schema()],
?invoice_template_id => [<<"invoiceTemplateID">>],
?allocation => [<<"allocation">>, {set, allocation_transaction()}]
}.
-spec invoice_template() -> schema().
invoice_template() ->
#{
?shop_id => [<<"shopID">>],
?lifetime => [<<"lifetime">>, lifetime_schema()],
?details => [<<"details">>, invoice_template_details_schema()]
}.
-spec invoice_template_details_schema() -> schema().
invoice_template_details_schema() ->
#{
?discriminator => [<<"templateType">>],
?single_line => #{
?product => [<<"product">>],
?price => [<<"price">>, invoice_template_line_cost()],
?tax => [<<"taxMode">>, tax_mode_schema()]
},
?multiline => #{
?currency => [<<"currency">>],
?cart => [<<"cart">>, {set, cart_line_schema()}]
}
}.
-spec refund() -> schema().
refund() ->
#{
?amount => [<<"amount">>],
?currency => [<<"currency">>],
?cart => [<<"cart">>, {set, cart_line_schema()}],
?allocation => [<<"allocation">>, {set, allocation_transaction()}]
}.
-spec customer() -> schema().
customer() ->
#{
?shop_id => [<<"shopID">>],
?contact_info => [<<"contactInfo">>, contact_info_schema()]
}.
-spec customer_binding() -> schema().
customer_binding() ->
#{
?payment_resource => [
<<"paymentResource">>,
#{
?payment_session => [<<"paymentSession">>],
?payment_tool => [<<"paymentTool">>, payment_tool_schema()]
}
]
}.
-spec payment_tool_schema() -> schema().
payment_tool_schema() ->
#{
?discriminator => [<<"type">>],
?bank_card => #{
?token => [<<"token">>],
?exp_date => [<<"exp_date">>]
},
?terminal => #{
?discriminator => [<<"terminal_type">>]
},
?wallet => #{
?provider => [<<"provider">>],
?id => [<<"id">>],
?token => [<<"token">>]
},
?crypto => #{
?currency => [<<"currency">>]
},
?mobile_commerce => #{
?operator => [<<"operator">>],
?phone => [<<"phone">>]
}
}.
-spec allocation_transaction() -> schema().
allocation_transaction() ->
#{
?target => [<<"target">>, allocation_target()],
?discriminator => [<<"allocationBodyType">>],
?amount => [<<"amount">>],
?total => [<<"total">>],
?currency => [<<"currency">>],
?fee => [
<<"fee">>,
#{
?target => [<<"target">>, allocation_target()],
?discriminator => [<<"allocationFeeType">>],
?amount => [<<"amount">>],
?share => [<<"share">>, decimal()]
}
],
?cart => [<<"cart">>, {set, cart_line_schema()}]
}.
-spec allocation_target() -> schema().
allocation_target() ->
#{
?discriminator => [<<"allocationTargetType">>],
?shop_id => [<<"shopID">>]
}.
-spec decimal() -> schema().
decimal() ->
#{
?matisse => [<<"m">>],
?exponent => [<<"exp">>]
}.
-spec cart_line_schema() -> schema().
cart_line_schema() ->
#{
?product => [<<"product">>],
?quantity => [<<"quantity">>],
?price => [<<"price">>],
?tax => [<<"taxMode">>, tax_mode_schema()]
}.
-spec tax_mode_schema() -> schema().
tax_mode_schema() ->
#{
?discriminator => [<<"type">>],
?rate => [<<"rate">>]
}.
-spec bank_account_schema() -> schema().
bank_account_schema() ->
#{
?discriminator => [<<"accountType">>],
?account => [<<"account">>],
?bank_bik => [<<"bankBik">>]
}.
invoice_template_line_cost() ->
#{
?discriminator => [<<"costType">>],
?range => #{
?currency => [<<"currency">>],
?range => [<<"range">>, cost_amount_range()]
},
?fixed => #{
?currency => [<<"currency">>],
?amount => [<<"amount">>]
}
%% Unlim has no params and is fully contained in discriminator
}.
-spec cost_amount_range() -> schema().
cost_amount_range() ->
#{
?upper_bound => [<<"upperBound">>],
?lower_bound => [<<"lowerBound">>]
}.
-spec lifetime_schema() -> schema().
lifetime_schema() ->
#{
?days => [<<"days">>],
?months => [<<"months">>],
?years => [<<"years">>]
}.
-spec contact_info_schema() -> schema().
contact_info_schema() ->
#{
?email => [<<"email">>],
?phone_number => [<<"phoneNumber">>]
}.
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-include_lib("capi_dummy_data.hrl").
deep_merge(M1, M2) ->
maps:fold(
fun
(K, V, MAcc) when is_map(V) ->
Value = deep_merge(maps:get(K, MAcc, #{}), V),
MAcc#{K => Value};
(K, V, MAcc) ->
MAcc#{K => V}
end,
M1,
M2
).
deep_fetch(Map, Keys) ->
lists:foldl(fun(K, M) -> maps:get(K, M) end, Map, Keys).
hash(Term) ->
capi_idemp_features_legacy:hash(Term).
read(Schema, Request) ->
capi_idemp_features_legacy:read(Schema, Request).
compare(Features1, Features2) ->
capi_idemp_features_legacy:compare(Features1, Features2).
list_diff_fields(Schema, Diff) ->
capi_idemp_features_legacy:list_diff_fields(Schema, Diff).
-spec test() -> _.
-spec read_payment_features_test() -> _.
read_payment_features_test() ->
PayerType = <<"PaymentResourcePayer">>,
ToolType = <<"bank_card">>,
Token = <<"cds token">>,
CardHolder = <<"0x42">>,
Category = <<"BUSINESS">>,
ExpDate = {exp_date, 02, 2022},
Flow = <<"PaymentFlowHold">>,
Request = #{
<<"flow">> => #{
<<"type">> => Flow
},
<<"payer">> => #{
<<"payerType">> => PayerType,
<<"paymentTool">> => #{
<<"type">> => ToolType,
<<"token">> => Token,
<<"exp_date">> => ExpDate,
<<"cardholder_name">> => CardHolder,
<<"category">> => Category
}
}
},
Payer = #{
?invoice_id => undefined,
?make_recurrent => undefined,
?flow => #{
?discriminator => hash(Flow),
?hold_exp => undefined
},
?payer => #{
?discriminator => hash(PayerType),
?customer => undefined,
?recurrent => undefined,
?payment_tool => #{
?discriminator => hash(ToolType),
?bank_card => #{
?exp_date => hash(ExpDate),
?token => hash(Token)
},
?crypto => #{?currency => undefined},
?mobile_commerce => #{
?operator => undefined,
?phone => undefined
},
?terminal => #{?discriminator => undefined},
?wallet => #{
?id => undefined,
?provider => undefined,
?token => hash(Token)
}
}
}
},
Features = read(payment(), Request),
?assertEqual(Payer, Features).
-spec compare_payment_bank_card_test() -> _.
compare_payment_bank_card_test() ->
Token2 = <<"cds token 2">>,
CardHolder2 = <<"Cake">>,
PaymentTool1 = bank_card(),
PaymentTool2 = PaymentTool1#{
<<"token">> => Token2,
<<"cardholder_name">> => CardHolder2
},
Request1 = payment_params(PaymentTool1),
Request2 = payment_params(PaymentTool2),
common_compare_tests(payment(), Request1, Request2, [
<<"payer.paymentTool.token">>
]).
-spec compare_different_payment_tool_test() -> _.
compare_different_payment_tool_test() ->
ToolType2 = <<"wallet">>,
Token2 = <<"wallet token">>,
PaymentTool1 = bank_card(),
PaymentTool2 = #{
<<"type">> => ToolType2,
<<"token">> => Token2
},
Request1 = payment_params(PaymentTool1),
Request2 = payment_params(PaymentTool2),
common_compare_tests(payment(), Request1, Request2, [<<"payer.paymentTool">>]).
-spec feature_multi_accessor_test() -> _.
feature_multi_accessor_test() ->
Request1 = #{
<<"payer">> => #{
<<"payerType">> => <<"PaymentResourcePayer">>,
<<"paymentTool">> => #{
<<"wrapper">> => bank_card()
}
}
},
Request2 = deep_merge(Request1, #{
<<"payer">> => #{
<<"paymentTool">> => #{
<<"wrapper">> => #{
<<"token">> => <<"cds token 2">>,
<<"cardholder_name">> => <<"Cake">>
}
}
}
}),
Schema = #{
<<"payer">> => [
<<"payer">>,
#{
<<"type">> => [<<"payerType">>],
<<"tool">> => [
<<"paymentTool">>,
<<"wrapper">>,
#{
<<"$type">> => [<<"type">>],
<<"bank_card">> => #{
<<"token">> => [<<"token">>],
<<"exp_date">> => [<<"exp_date">>]
}
}
]
}
]
},
common_compare_tests(Schema, Request1, Request2, [
<<"payer.paymentTool.wrapper.token">>
]).
-spec read_payment_customer_features_value_test() -> _.
read_payment_customer_features_value_test() ->
PayerType = <<"CustomerPayer">>,
CustomerID = <<"some customer id">>,
Request = #{
<<"payer">> => #{
<<"payerType">> => PayerType,
<<"customerID">> => CustomerID
}
},
Features = read(payment(), Request),
?assertEqual(
#{
?invoice_id => undefined,
?make_recurrent => undefined,
?flow => undefined,
?payer => #{
?discriminator => hash(PayerType),
?customer => hash(CustomerID),
?recurrent => undefined,
?payment_tool => undefined
}
},
Features
).
-spec read_invoice_features_test() -> _.
read_invoice_features_test() ->
ShopID = <<"shopus">>,
Cur = <<"XXX">>,
Prod1 = <<"yellow duck">>,
Prod2 = <<"blue duck">>,
DueDate = <<"2019-08-24T14:15:22Z">>,
Price1 = 10000,
Price2 = 20000,
Quantity = 1,
Product = #{
?product => hash(Prod1),
?quantity => hash(Quantity),
?price => hash(Price1),
?tax => undefined
},
Product2 = Product#{
?product => hash(Prod2),
?price => hash(Price2)
},
BankAccount = #{
?discriminator => hash(<<"InvoiceRussianBankAccount">>),
?account => hash(<<"12345678901234567890">>),
?bank_bik => hash(<<"123456789">>)
},
Invoice = #{
?amount => undefined,
?currency => hash(Cur),
?shop_id => hash(ShopID),
?product => undefined,
?due_date => hash(DueDate),
?bank_account => BankAccount,
?cart => [
[1, Product],
[0, Product2]
],
?invoice_template_id => undefined,
?allocation => undefined
},
Request = #{
<<"externalID">> => <<"externalID">>,
<<"dueDate">> => DueDate,
<<"shopID">> => ShopID,
<<"currency">> => Cur,
<<"description">> => <<"Wild birds.">>,
<<"bankAccount">> => #{
<<"accountType">> => <<"InvoiceRussianBankAccount">>,
<<"account">> => <<"12345678901234567890">>,
<<"bankBik">> => <<"123456789">>
},
<<"cart">> => [
#{<<"product">> => Prod2, <<"quantity">> => 1, <<"price">> => Price2},
#{<<"product">> => Prod1, <<"quantity">> => 1, <<"price">> => Price1, <<"not feature">> => <<"hmm">>}
],
<<"metadata">> => #{}
},
Features = read(invoice(), Request),
?assertEqual(Invoice, Features),
TemplateID = <<"42">>,
RequestWithTemplate = Request#{<<"invoiceTemplateID">> => TemplateID},
FeaturesWithTemplate = read(invoice(), RequestWithTemplate),
?assertEqual(hash(TemplateID), maps:get(?invoice_template_id, FeaturesWithTemplate)).
-spec compare_invoices_features_test() -> _.
compare_invoices_features_test() ->
ShopID = <<"shopus">>,
Cur = <<"RUB">>,
Prod1 = <<"yellow duck">>,
Prod2 = <<"blue duck">>,
Price1 = 10000,
Price2 = 20000,
Product = #{
<<"product">> => Prod1,
<<"quantity">> => 1,
<<"price">> => Price1,
<<"taxMode">> => #{
<<"type">> => <<"InvoiceLineTaxVAT">>,
<<"rate">> => <<"10%">>
}
},
Request1 = #{
<<"shopID">> => ShopID,
<<"currency">> => Cur,
<<"cart">> => [Product]
},
Request2 = deep_merge(Request1, #{
<<"cart">> => [#{<<"product">> => Prod2, <<"price">> => Price2}]
}),
Request3 = deep_merge(Request1, #{
<<"cart">> => [#{<<"product">> => Prod2, <<"price">> => Price2, <<"quantity">> => undefined}]
}),
Schema = invoice(),
Invoice1 = read(Schema, Request1),
InvoiceChg1 = read(Schema, Request1#{
<<"cart">> => [
Product#{
<<"price">> => Price2,
<<"taxMode">> => #{
<<"rate">> => <<"18%">>
}
}
]
}),
Invoice2 = read(Schema, Request2),
InvoiceWithFullCart = read(Schema, Request3),
?assertEqual(
{false, #{
?cart => #{
0 => #{
?price => ?difference,
?product => ?difference,
?quantity => ?difference,
?tax => ?difference
}
}
}},
compare(Invoice2, Invoice1)
),
?assert(compare(Invoice1, Invoice1)),
%% Feature was deleted
?assert(compare(InvoiceWithFullCart, Invoice2)),
%% Feature was add
?assert(compare(Invoice2, InvoiceWithFullCart)),
%% When second request didn't contain feature, this situation detected as conflict.
?assertEqual(
{false, #{?cart => ?difference}},
compare(Invoice1#{?cart => undefined}, Invoice1)
),
{false, Diff} = compare(Invoice1, InvoiceChg1),
?assertEqual(
[<<"cart.0.price">>, <<"cart.0.taxMode.rate">>],
list_diff_fields(Schema, Diff)
),
?assert(compare(Invoice1, Invoice1#{?cart => undefined})).
-spec read_customer_features_test() -> _.
read_customer_features_test() ->
Request = ?CUSTOMER_PARAMS,
Features = #{
?shop_id => hash(?STRING),
?contact_info => #{
?email => hash(<<"bla@bla.ru">>),
?phone_number => undefined
}
},
?assertEqual(
Features,
read(customer(), Request)
).
-spec compare_customer_features_test() -> _.
compare_customer_features_test() ->
Request = ?CUSTOMER_PARAMS,
RequestSame = Request#{
<<"partyID">> => <<"ANOTHER PARTY">>,
<<"metadata">> => #{<<"text">> => <<"sample text">>}
},
RequestDifferent = Request#{
<<"shopID">> => hash(<<"Another shop">>),
<<"contactInfo">> => #{
<<"email">> => hash(<<"bla@example.com">>),
<<"phoneNumber">> => <<"8-800-555-35-35">>
}
},
common_compare_tests(
customer(),
Request,
RequestSame,
RequestDifferent,
[
<<"shopID">>,
<<"contactInfo.email">>,
<<"contactInfo.phoneNumber">>
]
).
-spec read_customer_binding_features_test() -> _.
read_customer_binding_features_test() ->
Session = ?TEST_PAYMENT_SESSION(<<"Session">>),
Tool = ?TEST_PAYMENT_TOOL(<<"visa">>, <<"TOKEN">>),
Request = payment_resource(Session, Tool),
Features = #{
?payment_resource => #{
?payment_session => hash(Session),
?payment_tool => #{
?discriminator => hash(<<"bank_card">>),
?bank_card => #{
?token => hash(<<"TOKEN">>),
?exp_date => hash(<<"12/2012">>)
},
?terminal => #{
?discriminator => undefined
},
?wallet => #{
?provider => undefined,
?id => undefined,
?token => hash(<<"TOKEN">>)
},
?crypto => #{
?currency => undefined
},
?mobile_commerce => #{
?operator => undefined,
?phone => undefined
}
}
}
},
?assertEqual(
Features,
read(customer_binding(), Request)
).
-spec compare_customer_binding_features_test() -> _.
compare_customer_binding_features_test() ->
Session1 = ?TEST_PAYMENT_SESSION(<<"Session1">>),
Tool1 = ?TEST_PAYMENT_TOOL(<<"visa">>),
Request1 = payment_resource(Session1, Tool1),
Session2 = ?TEST_PAYMENT_SESSION(<<"Session2">>),
Tool2 = ?TEST_PAYMENT_TOOL(<<"mastercard">>)#{<<"exp_date">> => <<"01/2020">>},
Request2 = payment_resource(Session2, Tool2),
common_compare_tests(customer_binding(), Request1, Request2, [
<<"paymentResource.paymentTool.exp_date">>,
<<"paymentResource.paymentSession">>
]).
%% Add invoice_template tests
-spec read_invoice_template_features_test() -> _.
read_invoice_template_features_test() ->
ShopID = <<"1">>,
Request = #{
<<"shopID">> => ShopID,
<<"lifetime">> => lifetime_dummy(1, 2, 3),
<<"details">> => ?INVOICE_TMPL_DETAILS_PARAMS(42)
},
Features = #{
?shop_id => hash(ShopID),
?lifetime => #{
?days => hash(1),
?months => hash(2),
?years => hash(3)
},
?details => #{
?discriminator => hash(<<"InvoiceTemplateMultiLine">>),
?single_line => #{
?product => undefined,
?price => undefined,
?tax => undefined
},
?multiline => #{
?currency => hash(<<"RUB">>),
?cart => [
[
1,
#{
?product => hash(?STRING),
?quantity => hash(42),
?price => hash(?INTEGER),
?tax => #{?discriminator => hash(<<"InvoiceLineTaxVAT">>), ?rate => hash(<<"18%">>)}
}
],
[
0,
#{
?product => hash(?STRING),
?quantity => hash(42),
?price => hash(?INTEGER),
?tax => undefined
}
]
]
}
}
},
?assertEqual(
Features,
read(invoice_template(), Request)
).
-spec compare_invoice_template_features_test() -> _.
compare_invoice_template_features_test() ->
ShopID1 = <<"1">>,
ShopID2 = <<"2">>,
Request1 = #{
<<"shopID">> => ShopID1,
<<"lifetime">> => lifetime_dummy(1, 2, 3),
<<"details">> => ?INVOICE_TMPL_DETAILS_PARAMS(42)
},
Request2 = deep_merge(
Request1,
#{
<<"shopID">> => ShopID2,
<<"lifetime">> => lifetime_dummy(1, 2, 42),
<<"details">> => #{
<<"currency">> => ?USD,
<<"cart">> => [hd(deep_fetch(Request1, [<<"details">>, <<"cart">>]))]
}
}
),
common_compare_tests(invoice_template(), Request1, Request2, [
<<"shopID">>,
<<"lifetime.years">>,
<<"details.currency">>,
<<"details.cart">>
]).
-spec read_allocation_transaction_test_() -> _.
read_allocation_transaction_test_() ->
Request1 = ?ALLOCATION_TRANSACTION_PARAMS,
Features1 = #{
?target => #{
?discriminator => hash(<<"AllocationTargetShop">>),
?shop_id => hash(?STRING)
},
?discriminator => hash(<<"AllocationBodyTotal">>),
?amount => undefined,
?total => hash(?INTEGER),
?currency => hash(?USD),
?fee => #{
?target => #{
?discriminator => hash(<<"AllocationTargetShop">>),
?shop_id => hash(?STRING)
},
?discriminator => hash(<<"AllocationFeeShare">>),
?amount => hash(?INTEGER),
?share => #{
?matisse => hash(?INTEGER),
?exponent => hash(?INTEGER)
}
},
?cart => [
[
0,
#{
?product => hash(?STRING),
?quantity => hash(?INTEGER),
?price => hash(?INTEGER),
?tax => undefined
}
]
]
},
Request2 = Request1#{
<<"fee">> => #{
<<"target">> => ?ALLOCATION_TARGET,
<<"allocationFeeType">> => <<"AllocationFeeFixed">>,
<<"amount">> => 1024
}
},
Features2 = Features1#{
?fee => #{
?target => #{
?discriminator => hash(<<"AllocationTargetShop">>),
?shop_id => hash(?STRING)
},
?discriminator => hash(<<"AllocationFeeFixed">>),
?amount => hash(1024),
?share => undefined
}
},
[
?_assertEqual(Features1, read(allocation_transaction(), Request1)),
?_assertEqual(Features2, read(allocation_transaction(), Request2))
].
-spec compare_allocation_transaction_test() -> _.
compare_allocation_transaction_test() ->
Request1 = ?ALLOCATION_TRANSACTION_PARAMS,
Request2 = ?ALLOCATION_TRANSACTION_PARAMS#{
<<"total">> => 1024,
<<"amount">> => 512,
<<"fee">> => #{
<<"target">> => ?ALLOCATION_TARGET,
<<"allocationFeeType">> => <<"AllocationFeeFixed">>,
<<"amount">> => ?INTEGER,
<<"share">> => undefined
}
},
Request3 = #{
<<"target">> => ?ALLOCATION_TARGET#{<<"shopID">> => <<"SomeShop">>},
<<"allocationBodyType">> => <<"AllocationBodyAmount">>,
<<"amount">> => ?INTEGER,
<<"currency">> => ?RUB,
<<"cart">> => [
#{<<"product">> => ?STRING, <<"quantity">> => 1, <<"price">> => ?INTEGER}
]
},
Request4 = Request1#{
<<"fee">> => deep_merge(maps:get(<<"fee">>, Request1), #{
<<"amount">> => 1024,
<<"share">> => #{<<"m">> => 1024, <<"exp">> => 1024}
})
},
common_compare_tests(allocation_transaction(), Request1, Request2, [
<<"amount">>, <<"total">>, <<"fee">>
]),
common_compare_tests(allocation_transaction(), Request1, Request3, [
<<"target.shopID">>, <<"allocationBodyType">>, <<"currency">>, <<"amount">>, <<"cart.0.quantity">>
]),
common_compare_tests(allocation_transaction(), Request1, Request4, [
<<"fee.amount">>, <<"fee.share.m">>, <<"fee.share.exp">>
]).
-spec demo_compare_allocation_transaction_test() -> _.
demo_compare_allocation_transaction_test() ->
Request1 = ?ALLOCATION_TRANSACTION_PARAMS,
Request2 = #{
<<"allocationBodyType">> => <<"AllocationBodyAmount">>
},
Request3 = #{
<<"fee">> => deep_merge(maps:get(<<"fee">>, Request1), #{
<<"allocationFeeType">> => <<"AllocationFeeFixed">>
})
},
common_compare_tests(allocation_transaction(), Request1, Request2, [
<<"allocationBodyType">>
]),
common_compare_tests(allocation_transaction(), Request1, Request3, [
<<"fee">>
]).
payment_resource(Session, Tool) ->
#{
<<"paymentResource">> => #{
<<"paymentSession">> => Session,
<<"paymentTool">> => Tool
}
}.
payment_params(ExternalID, MakeRecurrent) ->
genlib_map:compact(#{
<<"externalID">> => ExternalID,
<<"flow">> => #{<<"type">> => <<"PaymentFlowInstant">>},
<<"makeRecurrent">> => MakeRecurrent,
<<"metadata">> => #{<<"bla">> => <<"*">>},
<<"processingDeadline">> => <<"5m">>
}).
payment_params(ExternalID, Jwe, ContactInfo, MakeRecurrent) ->
Params = payment_params(ExternalID, MakeRecurrent),
genlib_map:compact(Params#{
<<"payer">> => #{
<<"payerType">> => <<"PaymentResourcePayer">>,
<<"paymentSession">> => <<"payment.session">>,
<<"paymentToolToken">> => Jwe,
<<"contactInfo">> => ContactInfo
}
}).
payment_params(PaymentTool) ->
Params = payment_params(<<"EID">>, <<"Jwe">>, #{}, false),
PaymentParams = deep_merge(Params, #{<<"payer">> => #{<<"paymentTool">> => PaymentTool}}),
PaymentParams.
bank_card() ->
#{
<<"type">> => <<"bank_card">>,
<<"token">> => <<"cds token">>,
<<"payment_system">> => <<"visa">>,
<<"bin">> => <<"411111">>,
<<"last_digits">> => <<"1111">>,
<<"exp_date">> => <<"2019-08-24T14:15:22Z">>,
<<"cardholder_name">> => <<"Degus Degusovich">>,
<<"is_cvv_empty">> => false
}.
lifetime_dummy(Days, Months, Years) ->
#{
<<"days">> => Days,
<<"months">> => Months,
<<"years">> => Years
}.
common_compare_tests(Schema, Request, RequestDifferent, DiffFeatures) ->
common_compare_tests(Schema, Request, Request, RequestDifferent, DiffFeatures).
common_compare_tests(Schema, Request, RequestWithIgnoredFields, RequestDifferent, DiffFeatures) ->
Features = read(Schema, Request),
FeaturesIgnored = read(Schema, RequestWithIgnoredFields),
FeaturesDifferent = read(Schema, RequestDifferent),
%% Equal to self
?assertEqual(true, compare(Features, Features)),
%% Equal to feature-wise same request
?assertEqual(true, compare(Features, FeaturesIgnored)),
%% Has correct diff with different request
Result = compare(Features, FeaturesDifferent),
?assertMatch({false, _}, Result),
{false, Diff} = Result,
?assertEqual(lists:sort(DiffFeatures), lists:sort(list_diff_fields(Schema, Diff))).
-endif.

View File

@ -6,7 +6,13 @@
-export([prepare/3]).
-import(capi_handler_utils, [general_error/2, logic_error/1, logic_error/2, map_service_result/1]).
-import(capi_handler_utils, [
general_error/2,
logic_error/1,
logic_error/2,
conflict_error/1,
map_service_result/1
]).
-spec prepare(
OperationID :: capi_handler:operation_id(),
@ -45,7 +51,7 @@ prepare('CreateCustomer' = OperationID, Req, Context) ->
end
catch
throw:{external_id_conflict, ID, UsedExternalID, _Schema} ->
{ok, logic_error('externalIDConflict', {ID, UsedExternalID})}
{ok, conflict_error({ID, UsedExternalID})}
end
end,
{ok, #{authorize => Authorize, process => Process}};
@ -165,7 +171,7 @@ prepare('CreateBinding' = OperationID, Req, Context) ->
{error, invalid_payment_session} ->
{ok, logic_error('invalidPaymentSession', <<"Specified payment session is invalid">>)};
{error, {external_id_conflict, ID, UsedExternalID, _Schema}} ->
{ok, logic_error('externalIDConflict', {ID, UsedExternalID})}
{ok, conflict_error({ID, UsedExternalID})}
end
end,
{ok, #{authorize => Authorize, process => Process}};
@ -290,8 +296,8 @@ mask_customer_notfound(Resolution) ->
generate_customer_id(OperationID, PartyID, CustomerParams, #{woody_context := WoodyContext}) ->
ExternalID = maps:get(<<"externalID">>, CustomerParams, undefined),
IdempKey = {OperationID, PartyID, ExternalID},
Identity = capi_bender:make_identity(customer, CustomerParams),
capi_bender:try_gen_snowflake(IdempKey, Identity, WoodyContext).
Identity = capi_bender:make_identity(capi_feature_schemas:customer(), CustomerParams),
capi_bender:gen_snowflake(IdempKey, Identity, WoodyContext).
encode_customer_params(CustomerID, PartyID, Params) ->
#payproc_CustomerParams{
@ -323,15 +329,18 @@ generate_binding_ids(OperationID, CustomerBindingParams, Context = #{woody_conte
CustomerBindingParams
),
Identity = capi_bender:make_identity(customer_binding, CustomerBindingParamsEncrypted),
Identity = capi_bender:make_identity(
capi_feature_schemas:customer_binding(),
CustomerBindingParamsEncrypted
),
OperationIDBin = erlang:atom_to_binary(OperationID),
CustomerBindingID = capi_bender:try_gen_snowflake(
CustomerBindingID = capi_bender:gen_snowflake(
{<<OperationIDBin/binary, "+CustomerBindingID">>, UserID, ExternalID},
Identity,
WoodyContext
),
RecPaymentToolID = capi_bender:try_gen_snowflake(
RecPaymentToolID = capi_bender:gen_snowflake(
{<<OperationIDBin/binary, "+RecPaymentToolID">>, UserID, ExternalID},
Identity,
WoodyContext

View File

@ -183,11 +183,9 @@ decode_payer(
customer_id = ID
}}
) ->
PaymentToolSwag = capi_handler_decoder_party:decode_payment_tool(PaymentTool),
#{
<<"payerType">> => <<"CustomerPayer">>,
<<"customerID">> => ID,
<<"paymentToolToken">> => capi_handler_decoder_party:wrap_payment_tool_token(PaymentToolSwag),
<<"paymentToolDetails">> => capi_handler_decoder_party:decode_payment_tool_details(PaymentTool)
};
decode_payer(
@ -197,10 +195,8 @@ decode_payer(
contact_info = ContactInfo
}}
) ->
PaymentToolSwag = capi_handler_decoder_party:decode_payment_tool(PaymentTool),
#{
<<"payerType">> => <<"RecurrentPayer">>,
<<"paymentToolToken">> => capi_handler_decoder_party:wrap_payment_tool_token(PaymentToolSwag),
<<"paymentToolDetails">> => capi_handler_decoder_party:decode_payment_tool_details(PaymentTool),
<<"contactInfo">> => capi_handler_decoder_party:decode_contact_info(ContactInfo),
<<"recurrentParentPayment">> => decode_recurrent_parent(RecurrentParent)

View File

@ -19,8 +19,6 @@
-export([decode_payment_tool/1]).
-export([decode_payment_tool_details/1]).
-export([wrap_payment_tool_token/1]).
%%
-spec decode_shop_location(capi_handler_encoder:encode_data()) -> capi_handler_decoder_utils:decode_data().
@ -177,74 +175,18 @@ decode_payment_tool({mobile_commerce, MobileCommerce}) ->
decode_payment_tool({crypto_currency, CryptoCurrency}) ->
decode_crypto_wallet(CryptoCurrency).
-spec wrap_payment_tool_token(capi_handler_decoder_utils:decode_data()) -> binary().
wrap_payment_tool_token(#{<<"type">> := <<"bank_card">>} = BankCard) ->
Fields = [
<<"token">>,
<<"payment_system">>,
<<"bin">>,
<<"masked_pan">>,
<<"token_provider">>,
<<"issuer_country">>,
<<"bank_name">>,
<<"metadata">>,
<<"is_cvv_empty">>
],
BankCard1 = maps:with(Fields, BankCard),
capi_utils:map_to_base64url(BankCard1);
wrap_payment_tool_token(#{<<"type">> := <<"payment_terminal">>} = PaymentTerminal) ->
capi_utils:map_to_base64url(PaymentTerminal);
wrap_payment_tool_token(#{<<"type">> := <<"digital_wallet">>} = DigitalWallet) ->
capi_utils:map_to_base64url(DigitalWallet);
wrap_payment_tool_token(#{<<"type">> := <<"crypto_currency">>} = CryptoCurrency) ->
capi_utils:map_to_base64url(CryptoCurrency);
wrap_payment_tool_token(#{<<"type">> := <<"mobile_commerce">>} = MobileCommerce) ->
capi_utils:map_to_base64url(MobileCommerce).
decode_bank_card(#domain_BankCard{
'token' = Token,
'payment_system' = PaymentSystem,
'bin' = Bin,
'last_digits' = LastDigits,
'payment_token' = BankCardTokenServiceRef,
'issuer_country' = IssuerCountry,
'bank_name' = BankName,
'metadata' = Metadata,
'is_cvv_empty' = IsCVVEmpty,
'exp_date' = ExpDate,
'cardholder_name' = CardHolder
% 'tokenization_method' = TokenizationMethod
'exp_date' = ExpDate
}) ->
genlib_map:compact(#{
<<"type">> => <<"bank_card">>,
<<"token">> => Token,
<<"payment_system">> => capi_handler_decoder_utils:decode_payment_system_ref(PaymentSystem),
<<"bin">> => Bin,
<<"masked_pan">> => LastDigits,
<<"token_provider">> => capi_utils:maybe(
BankCardTokenServiceRef,
fun capi_handler_decoder_utils:decode_bank_card_token_service_ref/1
),
<<"issuer_country">> => IssuerCountry,
<<"bank_name">> => BankName,
<<"metadata">> => decode_bank_card_metadata(Metadata),
<<"is_cvv_empty">> => decode_bank_card_cvv_flag(IsCVVEmpty),
<<"exp_date">> => ExpDate,
<<"cardholder_name">> => CardHolder
% TODO: Uncomment or delete this when we negotiate deploying non-breaking changes
% <<"tokenization_method">> => TokenizationMethod
<<"exp_date">> => ExpDate
}).
decode_bank_card_cvv_flag(undefined) ->
undefined;
decode_bank_card_cvv_flag(CVVFlag) when is_atom(CVVFlag) ->
erlang:atom_to_binary(CVVFlag, utf8).
decode_bank_card_metadata(undefined) ->
undefined;
decode_bank_card_metadata(Meta) ->
maps:map(fun(_, Data) -> capi_msgp_marshalling:unmarshal(Data) end, Meta).
decode_payment_terminal(#domain_PaymentTerminal{payment_service = PaymentService}) ->
#{
<<"type">> => <<"payment_terminal">>,
@ -345,15 +287,13 @@ decode_digital_wallet_details(#domain_DigitalWallet{payment_service = Provider},
-spec decode_disposable_payment_resource(capi_handler_encoder:encode_data()) ->
capi_handler_decoder_utils:decode_data().
decode_disposable_payment_resource(Resource) ->
#domain_DisposablePaymentResource{payment_tool = PaymentTool, payment_session_id = SessionID} = Resource,
ClientInfo = decode_client_info(Resource#domain_DisposablePaymentResource.client_info),
PaymentToolSwag = decode_payment_tool(PaymentTool),
decode_disposable_payment_resource(#domain_DisposablePaymentResource{
payment_tool = PaymentTool,
client_info = ClientInfo
}) ->
#{
<<"paymentToolToken">> => wrap_payment_tool_token(PaymentToolSwag),
<<"paymentSession">> => capi_handler_utils:wrap_payment_session(ClientInfo, SessionID),
<<"paymentToolDetails">> => decode_payment_tool_details(PaymentTool),
<<"clientInfo">> => ClientInfo
<<"clientInfo">> => decode_client_info(ClientInfo)
}.
decode_client_info(undefined) ->

View File

@ -7,7 +7,7 @@
-export([prepare/3]).
-import(capi_handler_utils, [general_error/2, logic_error/2, map_service_result/1]).
-import(capi_handler_utils, [general_error/2, logic_error/2, conflict_error/1, map_service_result/1]).
-spec prepare(
OperationID :: capi_handler:operation_id(),
@ -53,7 +53,7 @@ prepare('CreateInvoiceTemplate' = OperationID, Req, Context) ->
throw:zero_invoice_lifetime ->
{ok, logic_error('invalidRequest', <<"Lifetime cannot be zero">>)};
throw:{external_id_conflict, ID, UsedExternalID, _Schema} ->
{ok, logic_error('externalIDConflict', {ID, UsedExternalID})}
{ok, conflict_error({ID, UsedExternalID})}
end
end,
{ok, #{authorize => Authorize, process => Process}};
@ -185,7 +185,7 @@ prepare('CreateInvoiceWithTemplate' = OperationID, Req, Context) ->
throw:{bad_invoice_params, amount_no_currency} ->
{ok, logic_error('invalidRequest', <<"Currency is required for the amount">>)};
throw:{external_id_conflict, InvoiceID, ExternalID, _Schema} ->
{ok, logic_error('externalIDConflict', {InvoiceID, ExternalID})}
{ok, conflict_error({InvoiceID, ExternalID})}
end
end,
{ok, #{authorize => Authorize, process => Process}};
@ -245,8 +245,8 @@ create_invoice(PartyID, InvoiceTplID, InvoiceParams, Context, BenderPrefix) ->
ExternalID = maps:get(<<"externalID">>, InvoiceParams, undefined),
IdempotentKey = {BenderPrefix, PartyID, ExternalID},
InvoiceParamsWithTemplate = maps:put(<<"invoiceTemplateID">>, InvoiceTplID, InvoiceParams),
Identity = capi_bender:make_identity(invoice, InvoiceParamsWithTemplate),
InvoiceID = capi_bender:try_gen_snowflake(IdempotentKey, Identity, WoodyCtx),
Identity = capi_bender:make_identity(capi_feature_schemas:invoice(), InvoiceParamsWithTemplate),
InvoiceID = capi_bender:gen_snowflake(IdempotentKey, Identity, WoodyCtx),
CallArgs = {encode_invoice_params_with_tpl(InvoiceID, InvoiceTplID, InvoiceParams)},
Call = {invoicing, 'CreateWithTemplate', CallArgs},
capi_handler_utils:service_call_with([user_info], Call, Context).
@ -258,8 +258,8 @@ get_invoice_template(ID, Context) ->
generate_invoice_template_id(OperationID, TemplateParams, PartyID, #{woody_context := WoodyContext}) ->
ExternalID = maps:get(<<"externalID">>, TemplateParams, undefined),
IdempKey = {OperationID, PartyID, ExternalID},
Identity = capi_bender:make_identity(invoice_template, TemplateParams),
capi_bender:try_gen_snowflake(IdempKey, Identity, WoodyContext).
Identity = capi_bender:make_identity(capi_feature_schemas:invoice_template(), TemplateParams),
capi_bender:gen_snowflake(IdempKey, Identity, WoodyContext).
encode_invoice_tpl_create_params(InvoiceTemplateID, PartyID, Params) ->
Details = encode_invoice_tpl_details(genlib_map:get(<<"details">>, Params)),

View File

@ -6,7 +6,7 @@
-export([prepare/3]).
-import(capi_handler_utils, [general_error/2, logic_error/2, map_service_result/1]).
-import(capi_handler_utils, [general_error/2, logic_error/2, conflict_error/1, map_service_result/1]).
-spec prepare(
OperationID :: capi_handler:operation_id(),
@ -61,7 +61,7 @@ prepare('CreateInvoice' = OperationID, Req, Context) ->
throw:invalid_invoice_cost ->
{ok, logic_error('invalidInvoiceCost', <<"Invalid invoice amount">>)};
throw:{external_id_conflict, InvoiceID, ExternalID, _Schema} ->
{ok, logic_error('externalIDConflict', {InvoiceID, ExternalID})};
{ok, conflict_error({InvoiceID, ExternalID})};
throw:allocation_wrong_cart ->
{ok, logic_error('invalidAllocation', <<"Wrong cart">>)};
throw:allocation_duplicate ->
@ -274,8 +274,8 @@ create_invoice(PartyID, InvoiceParams, Context, BenderPrefix) ->
#{woody_context := WoodyCtx} = Context,
ExternalID = maps:get(<<"externalID">>, InvoiceParams, undefined),
IdempotentKey = {BenderPrefix, PartyID, ExternalID},
Identity = capi_bender:make_identity(invoice, InvoiceParams),
InvoiceID = capi_bender:try_gen_snowflake(IdempotentKey, Identity, WoodyCtx),
Identity = capi_bender:make_identity(capi_feature_schemas:invoice(), InvoiceParams),
InvoiceID = capi_bender:gen_snowflake(IdempotentKey, Identity, WoodyCtx),
Call = {invoicing, 'Create', {encode_invoice_params(InvoiceID, PartyID, InvoiceParams)}},
capi_handler_utils:service_call_with([user_info], Call, Context).

View File

@ -6,7 +6,7 @@
-export([prepare/3]).
-import(capi_handler_utils, [general_error/2, logic_error/1, logic_error/2]).
-import(capi_handler_utils, [general_error/2, logic_error/1, logic_error/2, conflict_error/1]).
-define(DEFAULT_PROCESSING_DEADLINE, <<"30m">>).
@ -60,7 +60,7 @@ prepare(OperationID = 'CreatePayment', Req, Context) ->
throw:invalid_processing_deadline ->
{ok, logic_error('invalidProcessingDeadline', <<"Specified processing deadline is invalid">>)};
throw:{external_id_conflict, PaymentID, ExternalID, _Schema} ->
{ok, logic_error('externalIDConflict', {PaymentID, ExternalID})}
{ok, conflict_error({PaymentID, ExternalID})}
end
end,
{ok, #{authorize => Authorize, process => Process}};
@ -307,7 +307,7 @@ prepare(OperationID = 'CreateRefund', Req, Context) ->
throw:invoice_cart_empty ->
{ok, logic_error('invalidInvoiceCart', <<"Wrong size. Path to item: cart">>)};
throw:{external_id_conflict, RefundID, ExternalID, _Schema} ->
{ok, logic_error('externalIDConflict', {RefundID, ExternalID})};
{ok, conflict_error({RefundID, ExternalID})};
throw:allocation_duplicate ->
{ok, logic_error('invalidAllocation', <<"Duplicate shop">>)};
throw:allocation_wrong_cart ->
@ -500,12 +500,12 @@ create_payment_id(Invoice, PaymentParams0, Context, OperationID, PaymentToolThri
IdempotentKey = {BenderPrefix, PartyID, ExternalID},
SequenceID = InvoiceID,
Identity = capi_bender:make_identity(payment, PaymentParams),
Identity = capi_bender:make_identity(capi_feature_schemas:payment(), PaymentParams),
SequenceParams = #{},
#{woody_context := WoodyCtx} = Context,
%% We put `invoice_id` in a context here because `get_payment_by_external_id()` needs it to work
CtxData = #{<<"invoice_id">> => InvoiceID},
capi_bender:try_gen_sequence(IdempotentKey, Identity, SequenceID, SequenceParams, WoodyCtx, CtxData).
capi_bender:gen_sequence(IdempotentKey, Identity, SequenceID, SequenceParams, WoodyCtx, CtxData).
find_payment_by_id(PaymentID, #payproc_Invoice{payments = Payments}) ->
Fun = fun(#payproc_InvoicePayment{payment = #domain_InvoicePayment{id = ID}}) ->
@ -706,13 +706,13 @@ create_refund(InvoiceID, PaymentID, RefundParams0, Context, BenderPrefix) ->
ExternalID = maps:get(<<"externalID">>, RefundParams, undefined),
IdempotentKey = {BenderPrefix, PartyID, ExternalID},
Identity = capi_bender:make_identity(refund, RefundParams),
Identity = capi_bender:make_identity(capi_feature_schemas:refund(), RefundParams),
SequenceID = create_sequence_id([InvoiceID, PaymentID], BenderPrefix),
SequenceParams = #{minimum => 100},
#{woody_context := WoodyCtx} = Context,
%% We put `invoice_id` and `payment_id` in a context here because `get_refund_by_external_id/2` needs it to work
CtxData = #{<<"invoice_id">> => InvoiceID, <<"payment_id">> => PaymentID},
RefundID = capi_bender:try_gen_sequence(IdempotentKey, Identity, SequenceID, SequenceParams, WoodyCtx, CtxData),
RefundID = capi_bender:gen_sequence(IdempotentKey, Identity, SequenceID, SequenceParams, WoodyCtx, CtxData),
refund_payment(RefundID, InvoiceID, PaymentID, RefundParams, Context).
refund_payment(RefundID, InvoiceID, PaymentID, RefundParams, Context) ->

View File

@ -222,7 +222,6 @@ decode_stat_payer(
) ->
#{
<<"payerType">> => <<"CustomerPayer">>,
<<"paymentToolToken">> => decode_stat_payment_tool_token(PaymentTool),
<<"paymentToolDetails">> => decode_stat_payment_tool_details(PaymentTool),
<<"customerID">> => ID
};
@ -236,7 +235,6 @@ decode_stat_payer(
) ->
#{
<<"payerType">> => <<"RecurrentPayer">>,
<<"paymentToolToken">> => decode_stat_payment_tool_token(PaymentTool),
<<"paymentToolDetails">> => decode_stat_payment_tool_details(PaymentTool),
<<"contactInfo">> => genlib_map:compact(#{
<<"phoneNumber">> => PhoneNumber,
@ -256,7 +254,6 @@ decode_stat_payer(
) ->
genlib_map:compact(#{
<<"payerType">> => <<"PaymentResourcePayer">>,
<<"paymentToolToken">> => decode_stat_payment_tool_token(PaymentTool),
<<"paymentToolDetails">> => decode_stat_payment_tool_details(PaymentTool),
<<"paymentSession">> => PaymentSession,
<<"clientInfo">> => genlib_map:compact(#{
@ -296,80 +293,6 @@ decode_stat_payment_status({Status, StatusInfo}, Context) ->
<<"error">> => Error
}.
decode_stat_payment_tool_token({bank_card, BankCard}) ->
decode_bank_card(BankCard);
decode_stat_payment_tool_token({payment_terminal, PaymentTerminal}) ->
decode_payment_terminal(PaymentTerminal);
decode_stat_payment_tool_token({digital_wallet, DigitalWallet}) ->
decode_digital_wallet(DigitalWallet);
decode_stat_payment_tool_token({crypto_currency, CryptoCurrency}) ->
decode_crypto_wallet(CryptoCurrency);
decode_stat_payment_tool_token({mobile_commerce, MobileCommerce}) ->
decode_mobile_commerce(MobileCommerce).
decode_bank_card(#merchstat_BankCard{
'token' = Token,
'payment_system' = PaymentSystem,
'bin' = Bin,
'masked_pan' = MaskedPan,
'payment_token' = BankCardTokenServiceRef
}) ->
capi_utils:map_to_base64url(
genlib_map:compact(#{
<<"type">> => <<"bank_card">>,
<<"token">> => Token,
<<"payment_system">> => capi_handler_decoder_utils:decode_payment_system_ref(PaymentSystem),
<<"bin">> => Bin,
<<"masked_pan">> => MaskedPan,
<<"token_provider">> => capi_utils:maybe(
BankCardTokenServiceRef,
fun capi_handler_decoder_utils:decode_bank_card_token_service_ref/1
),
<<"issuer_country">> => undefined,
<<"bank_name">> => undefined,
<<"metadata">> => undefined
})
).
decode_payment_terminal(#merchstat_PaymentTerminal{
terminal_type = Type
}) ->
capi_utils:map_to_base64url(#{
<<"type">> => <<"payment_terminal">>,
<<"terminal_type">> => Type
}).
decode_digital_wallet(#merchstat_DigitalWallet{
provider = Provider,
id = ID
}) ->
capi_utils:map_to_base64url(#{
<<"type">> => <<"digital_wallet">>,
<<"provider">> => atom_to_binary(Provider, utf8),
<<"id">> => ID
}).
decode_crypto_wallet(CryptoCurrency) ->
capi_utils:map_to_base64url(#{
<<"type">> => <<"crypto_wallet">>,
<<"crypto_currency">> => capi_handler_decoder_utils:convert_crypto_currency_to_swag(CryptoCurrency)
}).
decode_mobile_commerce(MobileCommerce) ->
#merchstat_MobileCommerce{
operator = Operator,
phone = #merchstat_MobilePhone{
cc = Cc,
ctn = Ctn
}
} = MobileCommerce,
Phone = #{<<"cc">> => Cc, <<"ctn">> => Ctn},
capi_utils:map_to_base64url(#{
<<"type">> => <<"mobile_commerce">>,
<<"phone">> => Phone,
<<"operator">> => atom_to_binary(Operator, utf8)
}).
decode_stat_payment_tool_details({bank_card, V}) ->
decode_bank_card_details(V, #{<<"detailsType">> => <<"PaymentToolDetailsBankCard">>});
decode_stat_payment_tool_details({payment_terminal, V}) ->

View File

@ -3,6 +3,7 @@
-include_lib("damsel/include/dmsl_payment_processing_thrift.hrl").
-include_lib("damsel/include/dmsl_domain_thrift.hrl").
-export([conflict_error/1]).
-export([general_error/2]).
-export([logic_error/1]).
-export([logic_error/2]).
@ -49,6 +50,21 @@
| dmsl_domain_thrift:'InvoiceTemplate'().
-type token_source() :: capi_auth:token_spec() | entity().
-spec conflict_error(binary() | {binary(), binary()}) -> response().
conflict_error({ID, ExternalID}) ->
Data = #{
<<"externalID">> => ExternalID,
<<"id">> => ID,
<<"message">> => <<"This 'externalID' has been used by another request">>
},
create_error_resp(409, Data);
conflict_error(ExternalID) ->
Data = #{
<<"externalID">> => ExternalID,
<<"message">> => <<"This 'externalID' has been used by another request">>
},
create_error_resp(409, Data).
-spec general_error(cowboy:http_status(), binary()) -> response().
general_error(Code, Message) ->
create_error_resp(Code, #{<<"message">> => genlib:to_binary(Message)}).
@ -57,24 +73,7 @@ general_error(Code, Message) ->
logic_error('invalidPaymentToolToken') ->
logic_error('invalidPaymentToolToken', <<"Specified payment tool token is invalid">>).
-spec logic_error
(term(), io_lib:chars() | binary()) -> response();
(term(), {binary(), binary() | undefined}) -> response().
logic_error('externalIDConflict', {ID, undefined}) ->
logic_error('externalIDConflict', {ID, <<"undefined">>});
logic_error('externalIDConflict', {ID, ExternalID}) ->
Data = #{
<<"externalID">> => ExternalID,
<<"id">> => ID,
<<"message">> => <<"This 'externalID' has been used by another request">>
},
create_error_resp(409, Data);
logic_error('externalIDConflict', ExternalID) ->
Data = #{
<<"externalID">> => ExternalID,
<<"message">> => <<"This 'externalID' has been used by another request">>
},
create_error_resp(409, Data);
-spec logic_error(term(), iodata()) -> response().
logic_error(Code, Message) ->
Data = #{<<"code">> => genlib:to_binary(Code), <<"message">> => genlib:to_binary(Message)},
create_error_resp(400, Data).

View File

@ -1,247 +0,0 @@
-module(capi_idemp_features_legacy).
-include("capi_feature_schemas_legacy.hrl").
-type request_key() :: binary().
-type request_value() :: integer() | binary() | request() | [request()].
-type request() :: #{request_key() := request_value()}.
-type feature_name() :: integer().
-type feature_value() :: integer() | features() | [feature_value()] | undefined.
-type features() :: #{feature_name() := feature_value()}.
-type schema() ::
#{
feature_name() := [request_key() | schema() | {set, schema()}]
}
| #{
?discriminator := [request_key()],
feature_name() := schema()
}.
-type difference() :: features().
-type event() ::
{invalid_schema_fragment, feature_name(), request()}
| {request_visited, {request, request()}}
| {request_key_index_visit, integer()}
| {request_key_index_visited, integer()}
| {request_key_visit, {key, integer(), request()}}
| {request_key_visited, {key, integer()}}.
-type event_handler() :: {module(), options()} | undefined.
-type options() :: term().
-export_type([event_handler/0]).
-export_type([event/0]).
-export_type([schema/0]).
-export_type([request/0]).
-export_type([difference/0]).
-export_type([features/0]).
-export_type([feature_name/0]).
-export_type([feature_value/0]).
-export_type([options/0]).
-export([read/2, read/3]).
-export([compare/2]).
-export([hash/1]).
-export([list_diff_fields/2]).
-callback handle_event(event(), options()) -> ok.
-spec read(schema(), request()) -> features().
read(Schema, Request) ->
read(get_event_handler(), Schema, Request).
-spec read(event_handler(), schema(), request()) -> features().
read(Handler, Schema, Request) ->
handle_event(get_event_handler(Handler), {request_visited, {request, Request}}),
read_(Schema, Request, Handler).
read_(Schema, Request, Handler) ->
Result = maps:fold(
fun
(Name, Fs, Acc) when is_map(Fs) ->
Value = read_(Fs, Request, Handler),
Acc#{Name => Value};
(Name, Accessor, Acc) when is_list(Accessor) ->
FeatureValue = read_request_value(Accessor, Request, Handler),
Acc#{Name => FeatureValue};
(_Name, 'reserved', Acc) ->
Acc
end,
#{},
Schema
),
Result.
read_request_value([], undefined, _) ->
undefined;
read_request_value([], Value, _) ->
hash(Value);
read_request_value([Schema = #{}], Request = #{}, Handler) ->
read_(Schema, Request, Handler);
read_request_value([{set, Schema = #{}}], List, Handler) when is_list(List) ->
{_, ListIndex} = lists:foldl(fun(Item, {N, Acc}) -> {N + 1, [{N, Item} | Acc]} end, {0, []}, List),
ListSorted = lists:keysort(2, ListIndex),
lists:foldl(
fun({Index, Req}, Acc) ->
handle_event(get_event_handler(Handler), {request_key_index_visit, Index}),
Value = read_(Schema, Req, Handler),
handle_event(get_event_handler(Handler), {request_key_index_visited, Index}),
[[Index, Value] | Acc]
end,
[],
ListSorted
);
read_request_value([Key | Rest], Request = #{}, Handler) when is_binary(Key) ->
SubRequest = maps:get(Key, Request, undefined),
handle_event(get_event_handler(Handler), {request_key_visit, {key, Key, SubRequest}}),
Value = read_request_value(Rest, SubRequest, Handler),
handle_event(get_event_handler(Handler), {request_key_visited, {key, Key}}),
Value;
read_request_value(_, undefined, _) ->
undefined;
read_request_value(Key, Request, Handler) ->
handle_event(get_event_handler(Handler), {invalid_schema_fragment, Key, Request}).
handle_event(undefined, {invalid_schema_fragment, Key, Request}) ->
logger:warning("Unable to extract idemp feature with schema: ~p from client request subset: ~p", [Key, Request]),
undefined;
handle_event(undefined, _Event) ->
ok;
handle_event({Mod, Opts}, Event) ->
Mod:handle_event(Event, Opts).
get_event_handler() ->
genlib_app:env(capi, idempotence_event_handler).
get_event_handler({Mod, Options}) ->
{Mod, Options};
get_event_handler(undefined) ->
undefined.
-spec hash(term()) -> integer().
hash(V) ->
erlang:phash2(V).
-spec list_diff_fields(schema(), difference()) -> [binary()].
list_diff_fields(Schema, Diff) ->
{ConvertedDiff, _} = list_diff_fields_(Diff, Schema, {[], []}),
lists:foldl(
fun(Keys, AccIn) ->
KeysBin = lists:map(fun genlib:to_binary/1, Keys),
Item = list_to_binary(lists:join(<<".">>, KeysBin)),
case lists:member(Item, AccIn) of
false ->
[Item | AccIn];
_ ->
AccIn
end
end,
[],
ConvertedDiff
).
list_diff_fields_(Diffs, {set, Schema}, Acc) when is_map(Schema) ->
maps:fold(
fun(I, Diff, {PathsAcc, PathRev}) ->
list_diff_fields_(Diff, Schema, {PathsAcc, [I | PathRev]})
end,
Acc,
Diffs
);
list_diff_fields_(Diff, Schema, Acc) when is_map(Schema) ->
zipfold(
fun
(_Feature, ?difference, [Key | _SchemaPart], {PathsAcc, PathRev}) ->
Path = lists:reverse([Key | PathRev]),
{[Path | PathsAcc], PathRev};
(_Feature, DiffPart, SchemaPart, {_PathsAcc, PathRev} = AccIn) ->
{NewPathsAcc, _NewPathRev} = list_diff_fields_(DiffPart, SchemaPart, AccIn),
{NewPathsAcc, PathRev}
end,
Acc,
Diff,
Schema
);
list_diff_fields_(Diff, [Schema], Acc) ->
list_diff_fields_(Diff, Schema, Acc);
list_diff_fields_(Diff, [Key | Schema], {PathsAcc, PathRev}) ->
list_diff_fields_(Diff, Schema, {PathsAcc, [Key | PathRev]}).
-spec compare(features(), features()) -> true | {false, difference()}.
compare(Features, FeaturesWith) ->
case compare_features(Features, FeaturesWith) of
Diff when map_size(Diff) > 0 ->
{false, Diff};
_ ->
true
end.
compare_features(Fs, FsWith) ->
zipfold(
fun
(Key, Values, ValuesWith, Diff) when is_list(ValuesWith), is_list(Values) ->
compare_list_features(Key, Values, ValuesWith, Diff);
(Key, Value, ValueWith, Diff) when is_map(ValueWith) and is_map(Value) ->
compare_features_(Key, Value, ValueWith, Diff);
%% We expect that clients may _at any time_ change their implementation and start
%% sending information they were not sending beforehand, so this is not considered a
%% conflict. Yet, we DO NOT expect them to do the opposite, to stop sending
%% information they were sending, this is still a conflict.
(_Key, _Value, undefined, Diff) ->
Diff;
(_Key, Value, Value, Diff) ->
Diff;
(Key, Value, ValueWith, Diff) when Value =/= ValueWith ->
Diff#{Key => ?difference}
end,
#{},
Fs,
FsWith
).
compare_list_features(Key, L1, L2, Diff) when length(L1) =/= length(L2) ->
Diff#{Key => ?difference};
compare_list_features(Key, L1, L2, Acc) ->
case compare_list_features_(L1, L2, #{}) of
Diff when map_size(Diff) > 0 ->
Acc#{Key => Diff};
#{} ->
Acc
end.
compare_list_features_([], [], Diff) ->
Diff;
compare_list_features_([[Index, V1] | Values], [[_, V2] | ValuesWith], Acc) ->
Diff = compare_features_(Index, V1, V2, Acc),
compare_list_features_(Values, ValuesWith, Diff).
compare_features_(Key, Value, ValueWith, Diff) when is_map(Value) and is_map(ValueWith) ->
case compare_features(Value, ValueWith) of
ValueWith ->
% different everywhere
Diff#{Key => ?difference};
#{?discriminator := _} ->
% Different with regard to discriminator, semantically same as different everywhere.
Diff#{Key => ?difference};
Diff1 when map_size(Diff1) > 0 ->
Diff#{Key => Diff1};
#{} ->
% no notable differences
Diff
end.
zipfold(Fun, Acc, M1, M2) ->
maps:fold(
fun(Key, V1, AccIn) ->
case maps:find(Key, M2) of
{ok, V2} ->
Fun(Key, V1, V2, AccIn);
error ->
AccIn
end
end,
Acc,
M1
).

View File

@ -52,3 +52,53 @@ unmarshal({obj, Object}) ->
maps:fold(fun(K, V, Acc) -> maps:put(unmarshal(K), unmarshal(V), Acc) end, #{}, Object);
unmarshal({arr, Array}) ->
lists:map(fun unmarshal/1, Array).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
-spec test() -> _.
-define(INSTANCES, [
{undefined, {nl, #msgpack_Nil{}}},
{42, {i, 42}},
{false, {b, false}},
{
#{
3.1415 => 1.337,
<<"there">> => [<<"be">>, {bin, <<"🐲"/utf8>>}, <<"dragons">>],
false => #{<<"is">> => true}
},
{obj, #{
{flt, 3.1415} => {flt, 1.337},
{str, <<"there">>} => {arr, [{str, <<"be">>}, {bin, <<"🐲"/utf8>>}, {str, <<"dragons">>}]},
{b, false} => {obj, #{{str, <<"is">>} => {b, true}}}
}}
}
]).
-spec marshalling_test_() -> _.
marshalling_test_() ->
[?_assertEqual(marshal(Instance), Marshalled) || {Instance, Marshalled} <- ?INSTANCES].
-spec unmarshalling_test_() -> _.
unmarshalling_test_() ->
[?_assertEqual(Instance, unmarshal(Marshalled)) || {Instance, Marshalled} <- ?INSTANCES].
-spec symmetric_marshalling_test_() -> _.
symmetric_marshalling_test_() ->
[?_assertEqual(Instance, unmarshal(marshal(Instance))) || {Instance, _} <- ?INSTANCES].
-spec symmetric_unmarshalling_test_() -> _.
symmetric_unmarshalling_test_() ->
[?_assertEqual(Marshalled, marshal(unmarshal(Marshalled))) || {_, Marshalled} <- ?INSTANCES].
-spec thrift_serialize_test_() -> _.
thrift_serialize_test_() ->
[?_test(serialize_thrift(marshal(Instance))) || {Instance, _} <- ?INSTANCES].
serialize_thrift(Term) ->
C1 = thrift_strict_binary_codec:new(),
{ok, C2} = thrift_strict_binary_codec:write(C1, {struct, union, {dmsl_msgpack_thrift, 'Value'}}, Term),
thrift_strict_binary_codec:close(C2).
-endif.

View File

@ -2075,7 +2075,7 @@ get_payment_revenue_stats_ok_test(Config) ->
{offset, 2},
{from_time, {{2015, 08, 11}, {19, 42, 36}}},
{to_time, {{2020, 08, 11}, {19, 42, 36}}},
{split_unit, minute},
{split_unit, hour},
{split_size, 1}
],
{ok, _} = capi_client_analytics:get_payment_revenue_stats(?config(context, Config), ?STRING, Query).
@ -2094,7 +2094,7 @@ get_payment_geo_stats_ok_test(Config) ->
{offset, 0},
{from_time, {{2015, 08, 11}, {19, 42, 37}}},
{to_time, {{2020, 08, 11}, {19, 42, 37}}},
{split_unit, minute},
{split_unit, day},
{split_size, 1}
],
{ok, _} = capi_client_analytics:get_payment_geo_stats(?config(context, Config), ?STRING, Query).
@ -2113,7 +2113,7 @@ get_payment_rate_stats_ok_test(Config) ->
{offset, 0},
{from_time, {{2015, 08, 11}, {19, 42, 38}}},
{to_time, {{2020, 08, 11}, {19, 42, 38}}},
{split_unit, minute},
{split_unit, week},
{split_size, 1}
],
{ok, _} = capi_client_analytics:get_payment_rate_stats(?config(context, Config), ?STRING, Query).
@ -2132,7 +2132,7 @@ get_payment_method_stats_ok_test(Config) ->
{offset, 0},
{from_time, {{2015, 08, 11}, {19, 42, 35}}},
{to_time, {{2020, 08, 11}, {19, 42, 35}}},
{split_unit, minute},
{split_unit, month},
{split_size, 1},
{'paymentMethod', <<"bankCard">>}
],

View File

@ -16,6 +16,7 @@
-define(TEST_USER_REALM, <<"external">>).
-define(TEST_RULESET_ID, <<"test/api">>).
-define(API_TOKEN, <<"letmein">>).
-define(EMAIL, <<"test@test.ru">>).
-define(RATIONAL, #'Rational'{p = ?INTEGER, q = ?INTEGER}).
@ -198,6 +199,7 @@
last_digits = <<"411111******1111">>
}).
-define(BANK_CARD(PS, ExpDate), ?BANK_CARD(PS, ExpDate, <<"CARD HODLER">>)).
-define(BANK_CARD(PS, ExpDate, CardHolder), ?BANK_CARD(PS, ExpDate, CardHolder, undefined)).
-define(BANK_CARD(PS, ExpDate, CardHolder, Category), #domain_BankCard{
token = PS,
@ -215,9 +217,19 @@
token = Token
}).
-define(MOBILE_COMMERCE(Operator, CC, CTN), #domain_MobileCommerce{
operator = #domain_MobileOperatorRef{id = Operator},
phone = #domain_MobilePhone{
cc = CC,
ctn = CTN
}
}).
-define(CRYPTO_CURRENCY_BTC, #domain_CryptoCurrencyRef{id = <<"bitcoin">>}).
-define(CONTACT_INFO, #domain_ContactInfo{
phone_number = ?STRING,
email = <<"test@test.ru">>
email = ?EMAIL
}).
-define(EXP_DATE(Month, Year), #domain_BankCardExpDate{
@ -1220,7 +1232,7 @@
}}
},
#domain_PaymentMethodRef{
id = {crypto_currency, #domain_CryptoCurrencyRef{id = <<"bitcoin">>}}
id = {crypto_currency, ?CRYPTO_CURRENCY_BTC}
},
#domain_PaymentMethodRef{
id = {crypto_currency, #domain_CryptoCurrencyRef{id = <<"bitcoin_cash">>}}

View File

@ -23,8 +23,6 @@
-export([second_request_with_idempotent_feature_test/1]).
-export([second_request_without_idempotent_feature_test/1]).
-export([create_invoice_ok_test/1]).
-export([create_invoice_legacy_fail_test/1]).
-export([create_invoice_legacy_ok_test/1]).
-export([create_invoice_fail_test/1]).
-export([create_invoice_idemp_cart_ok_test/1]).
-export([create_invoice_idemp_cart_fail_test/1]).
@ -44,8 +42,6 @@
-type config() :: [{atom(), any()}].
-type group_name() :: atom().
-define(DIFFERENCE, -1).
-behaviour(supervisor).
-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}.
@ -77,8 +73,6 @@ groups() ->
]},
{invoice_creation, [], [
create_invoice_ok_test,
create_invoice_legacy_fail_test,
create_invoice_legacy_ok_test,
create_invoice_fail_test,
create_invoice_idemp_cart_fail_test,
create_invoice_idemp_cart_ok_test,
@ -216,9 +210,6 @@ create_payment_ok_test(Config) ->
[<<"metadata">>, <<"bla">>, 0],
[<<"payer">>, <<"contactInfo">>],
[<<"payer">>, <<"paymentSession">>],
[<<"payer">>, <<"paymentTool">>, <<"bin">>],
[<<"payer">>, <<"paymentTool">>, <<"cardholder_name">>],
[<<"payer">>, <<"paymentTool">>, <<"masked_pan">>],
[<<"payer">>, <<"paymentTool">>, <<"payment_system">>],
[<<"payer">>, <<"paymentToolToken">>],
[<<"processingDeadline">>]
@ -324,63 +315,6 @@ create_invoice_ok_test(Config) ->
?assertEqual(ExternalID, maps:get(<<"externalID">>, Invoice1)),
?assertEqual(Invoice1, Invoice2).
-spec create_invoice_legacy_ok_test(config()) -> _.
create_invoice_legacy_ok_test(Config) ->
BenderKey = <<"bender_key">>,
ExternalID = <<"ok_merch_id">>,
Req = invoice_params(ExternalID),
Unused = [
[<<"description">>],
[<<"externalID">>],
[<<"metadata">>, <<"invoice_dummy_metadata">>]
],
Ctx = capi_msgp_marshalling:marshal(#{
<<"version">> => 2,
<<"features">> => capi_idemp_features_legacy:read(capi_feature_schemas_legacy:invoice(), Req)
}),
_ = capi_ct_helper:mock_services(
[
{invoicing, fun('Create', {_UserInfo, #payproc_InvoiceParams{id = ID, external_id = EID}}) ->
{ok, ?PAYPROC_INVOICE_WITH_ID(ID, EID)}
end},
{bender, fun('GenerateID', _) -> {ok, capi_ct_helper_bender:get_result(BenderKey, Ctx)} end}
],
Config
),
{{ok, ActualInvoice}, ActualUnused} = create_invoice_(Req, Config),
?assertEqual(Unused, ActualUnused),
{{ok, ActualInvoice}, ActualUnused} = create_invoice_(Req, Config).
-spec create_invoice_legacy_fail_test(config()) -> _.
create_invoice_legacy_fail_test(Config) ->
BenderKey = <<"bender_key">>,
ExternalID = <<"merch_id">>,
Req = invoice_params(ExternalID),
Unused = [
[<<"description">>],
[<<"externalID">>],
[<<"metadata">>, <<"invoice_dummy_metadata">>]
],
Req2 = Req#{<<"product">> => <<"test_product2">>},
Ctx = capi_msgp_marshalling:marshal(#{
<<"version">> => 2,
<<"features">> => capi_idemp_features_legacy:read(capi_feature_schemas_legacy:invoice(), Req)
}),
_ = capi_ct_helper:mock_services(
[
{invoicing, fun('Create', {_UserInfo, #payproc_InvoiceParams{id = ID, external_id = EID}}) ->
{ok, ?PAYPROC_INVOICE_WITH_ID(ID, EID)}
end},
{bender, fun('GenerateID', _) -> {ok, capi_ct_helper_bender:get_result(BenderKey, Ctx)} end}
],
Config
),
{{ok, Invoice1}, _} = create_invoice_(Req, Config),
#{<<"invoice">> := #{<<"id">> := InvoiceID}} = Invoice1,
{Response, Unused2} = create_invoice_(Req2, Config),
?assertEqual(Unused, Unused2),
?assertEqual(response_error(409, ExternalID, InvoiceID), Response).
-spec create_invoice_fail_test(config()) -> _.
create_invoice_fail_test(Config) ->
BenderKey = <<"bender_key">>,
@ -703,8 +637,6 @@ create_customer_binding_ok_test(Config) ->
?assertMatch(
{{ok, _}, [
[<<"externalID">>],
[<<"paymentResource">>, <<"paymentTool">>, <<"bin">>],
[<<"paymentResource">>, <<"paymentTool">>, <<"masked_pan">>],
[<<"paymentResource">>, <<"paymentTool">>, <<"payment_system">>]
]},
BindingResult1

View File

@ -25,8 +25,9 @@
create_payment_ok_test/1,
create_payment_expired_test/1,
create_payment_qiwi_access_token_ok_test/1,
create_payment_crypto_ok_test/1,
create_payment_mobile_commerce_ok_test/1,
create_payment_with_empty_cvv_ok_test/1,
create_payment_with_googlepay_encrypt_ok_test/1,
get_payments_ok_test/1,
get_payment_by_id_ok_test/1,
get_payment_by_id_trx_ok_test/1,
@ -73,9 +74,10 @@ invoice_access_token_tests() ->
create_payment_ok_test,
create_payment_expired_test,
create_payment_with_empty_cvv_ok_test,
create_payment_with_googlepay_encrypt_ok_test,
get_payments_ok_test,
create_payment_qiwi_access_token_ok_test,
create_payment_crypto_ok_test,
create_payment_mobile_commerce_ok_test,
create_first_recurrent_payment_ok_test,
create_second_recurrent_payment_ok_test
].
@ -297,7 +299,7 @@ create_payment_ok_test(Config) ->
?STRING,
Config
),
PaymentToolToken = get_encrypted_token(<<"visa">>, ?EXP_DATE(2, 2020)),
PaymentToolToken = encrypt_payment_tool({bank_card, ?BANK_CARD(<<"visa">>, ?EXP_DATE(2, 2020))}),
Req = ?PAYMENT_PARAMS(ExternalID, PaymentToolToken),
{ok, #{
<<"id">> := BenderKey,
@ -347,61 +349,30 @@ create_payment_expired_test(Config) ->
-spec create_payment_with_empty_cvv_ok_test(config()) -> _.
create_payment_with_empty_cvv_ok_test(Config) ->
_ = capi_ct_helper:mock_services(
[
{invoicing, fun
('Get', _) ->
{ok, ?PAYPROC_INVOICE};
(
'StartPayment',
{
_UserInfo,
_InvoiceID,
#payproc_InvoicePaymentParams{
payer =
{payment_resource, #payproc_PaymentResourcePayerParams{
resource = #domain_DisposablePaymentResource{
payment_tool = {
bank_card,
#domain_BankCard{is_cvv_empty = true}
}
}
}}
}
}
) ->
{ok, ?PAYPROC_PAYMENT}
end},
{generator, fun('GenerateID', _) -> capi_ct_helper_bender:generate_id(<<"bender_key">>) end}
],
Config
),
_ = capi_ct_helper_bouncer:mock_assert_invoice_op_ctx(
<<"CreatePayment">>,
?STRING,
?STRING,
?STRING,
Config
),
PaymentToolToken = get_encrypted_token(<<"visa">>, ?EXP_DATE(1, 2020), true),
Req2 = #{
<<"flow">> => #{<<"type">> => <<"PaymentFlowInstant">>},
<<"payer">> => #{
<<"payerType">> => <<"PaymentResourcePayer">>,
<<"paymentSession">> => ?TEST_PAYMENT_SESSION,
<<"paymentToolToken">> => PaymentToolToken,
<<"contactInfo">> => #{
<<"email">> => <<"bla@bla.ru">>
}
}
},
{ok, _} = capi_client_payments:create_payment(?config(context, Config), Req2, ?STRING).
BankCard = ?BANK_CARD(<<"visa">>, ?EXP_DATE(1, 2020), <<"CARD HODLER">>),
BankCardNoCVV = BankCard#domain_BankCard{is_cvv_empty = true},
{ok, _} = create_payment_w_payment_tool({bank_card, BankCardNoCVV}, Config).
-spec create_payment_qiwi_access_token_ok_test(_) -> _.
create_payment_qiwi_access_token_ok_test(Config) ->
Provider = <<"qiwi">>,
WalletID = <<"+79876543210">>,
Token = <<"blarg">>,
DigitalWallet = ?DIGITAL_WALLET(Provider, WalletID, Token),
{ok, _} = create_payment_w_payment_tool({digital_wallet, DigitalWallet}, Config).
-spec create_payment_crypto_ok_test(_) -> _.
create_payment_crypto_ok_test(Config) ->
{ok, _} = create_payment_w_payment_tool({crypto_currency, ?CRYPTO_CURRENCY_BTC}, Config).
-spec create_payment_mobile_commerce_ok_test(_) -> _.
create_payment_mobile_commerce_ok_test(Config) ->
MobileCommerce = ?MOBILE_COMMERCE(<<"mts">>, <<"123">>, <<"4567890">>),
{ok, _} = create_payment_w_payment_tool({mobile_commerce, MobileCommerce}, Config).
-spec create_payment_w_payment_tool(PaymentTool, config()) -> _ when
PaymentTool :: dmsl_domain_thrift:'PaymentTool'().
create_payment_w_payment_tool(PaymentTool, Config) ->
_ = capi_ct_helper:mock_services(
[
{invoicing, fun
@ -411,93 +382,30 @@ create_payment_qiwi_access_token_ok_test(Config) ->
?assertMatch(
{payment_resource, #payproc_PaymentResourcePayerParams{
resource = #domain_DisposablePaymentResource{
payment_tool = {
digital_wallet,
?DIGITAL_WALLET(Provider, WalletID, Token)
}
payment_tool = PaymentTool
}
}},
Params#payproc_InvoicePaymentParams.payer
),
{ok, ?PAYPROC_PAYMENT}
end},
{generator, fun('GenerateID', _) -> capi_ct_helper_bender:generate_id(<<"bender_key">>) end}
{generator, fun('GenerateID', _) ->
capi_ct_helper_bender:generate_id(?STRING)
end}
],
Config
),
_ = capi_ct_helper_bouncer:mock_assert_invoice_op_ctx(
<<"CreatePayment">>,
?STRING,
?STRING,
?STRING,
Config
),
PaymentToolToken = get_encrypted_token({Provider, WalletID, Token}),
_ = capi_ct_helper_bouncer:mock_assert_invoice_op_ctx(<<"CreatePayment">>, ?STRING, ?STRING, ?STRING, Config),
Req = #{
<<"flow">> => #{<<"type">> => <<"PaymentFlowInstant">>},
<<"payer">> => #{
<<"payerType">> => <<"PaymentResourcePayer">>,
<<"paymentSession">> => ?TEST_PAYMENT_SESSION,
<<"paymentToolToken">> => PaymentToolToken,
<<"contactInfo">> => #{<<"email">> => <<"bla@bla.ru">>}
<<"paymentToolToken">> => encrypt_payment_tool(PaymentTool),
<<"contactInfo">> => #{<<"email">> => ?EMAIL}
}
},
{ok, _} = capi_client_payments:create_payment(?config(context, Config), Req, ?STRING).
-spec create_payment_with_googlepay_encrypt_ok_test(_) -> _.
create_payment_with_googlepay_encrypt_ok_test(Config) ->
_ = capi_ct_helper:mock_services(
[
{invoicing, fun
('Get', _) ->
{ok, ?PAYPROC_INVOICE};
(
'StartPayment',
{
_UserInfo,
_InvoiceID,
#payproc_InvoicePaymentParams{
payer =
{payment_resource, #payproc_PaymentResourcePayerParams{
resource = #domain_DisposablePaymentResource{
payment_tool = {
bank_card,
#domain_BankCard{
is_cvv_empty = undefined,
payment_system = #domain_PaymentSystemRef{id = <<"mastercard">>}
}
}
}
}}
}
}
) ->
{ok, ?PAYPROC_PAYMENT}
end},
{generator, fun('GenerateID', _) -> capi_ct_helper_bender:generate_id(<<"bender_key">>) end}
],
Config
),
_ = capi_ct_helper_bouncer:mock_assert_invoice_op_ctx(
<<"CreatePayment">>,
?STRING,
?STRING,
?STRING,
Config
),
PaymentToolToken = get_encrypted_token(<<"mastercard">>, ?EXP_DATE(1, 2020)),
Req2 = #{
<<"flow">> => #{<<"type">> => <<"PaymentFlowInstant">>},
<<"payer">> => #{
<<"payerType">> => <<"PaymentResourcePayer">>,
<<"paymentSession">> => ?TEST_PAYMENT_SESSION,
<<"paymentToolToken">> => PaymentToolToken,
<<"contactInfo">> => #{
<<"email">> => <<"bla@bla.ru">>
}
}
},
{ok, _} = capi_client_payments:create_payment(?config(context, Config), Req2, ?STRING).
capi_client_payments:create_payment(?config(context, Config), Req, ?STRING).
-spec get_payments_ok_test(config()) -> _.
get_payments_ok_test(Config) ->
@ -665,7 +573,7 @@ create_first_recurrent_payment_ok_test(Config) ->
],
Config
),
PaymentToolToken = get_encrypted_token(<<"visa">>, ?EXP_DATE(1, 2020)),
PaymentToolToken = encrypt_payment_tool({bank_card, ?BANK_CARD(<<"visa">>, ?EXP_DATE(1, 2020))}),
Req2 = #{
<<"flow">> => #{<<"type">> => <<"PaymentFlowInstant">>},
<<"makeRecurrent">> => true,
@ -768,31 +676,6 @@ get_failed_payment_with_invalid_cvv(Config) ->
% mock_services([{invoicing, fun('GetPayment', _) -> {ok, ?PAYPROC_PAYMENT} end}], Config),
capi_client_payments:get_payment_by_id(?config(context, Config), ?STRING, ?STRING).
get_encrypted_token({Provider, ID, TokenID}) ->
PaymentTool =
{digital_wallet, #domain_DigitalWallet{
payment_service = #domain_PaymentServiceRef{id = Provider},
id = ID,
token = TokenID
}},
encrypt_payment_tool(PaymentTool).
get_encrypted_token(PS, ExpDate) ->
get_encrypted_token(PS, ExpDate, undefined).
get_encrypted_token(PS, ExpDate, IsCvvEmpty) ->
PaymentTool =
{bank_card, #domain_BankCard{
token = ?TEST_PAYMENT_TOKEN,
payment_system = #domain_PaymentSystemRef{id = PS},
bin = <<"411111">>,
last_digits = <<"1111">>,
exp_date = ExpDate,
cardholder_name = <<"Degus Degusovich">>,
is_cvv_empty = IsCvvEmpty
}},
encrypt_payment_tool(PaymentTool).
encrypt_payment_tool(PaymentTool) ->
encrypt_payment_tool(PaymentTool, undefined).

View File

@ -37,8 +37,7 @@
{elvis_style, macro_names, #{
ignore => [
% Abuses lowercase macros too much
capi_feature_schemas,
capi_feature_schemas_legacy
capi_feature_schemas
]
}}
]

View File

@ -119,11 +119,11 @@
{<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.6">>},2},
{<<"swag_client">>,
{git,"https://github.com/valitydev/swag-payments",
{ref,"03520bda80db540cfe2f431bc035e9c9c165fb99"}},
{ref,"187da507554108df60a317582ec8a8d40433c9e5"}},
0},
{<<"swag_server">>,
{git,"https://github.com/valitydev/swag-payments",
{ref,"347f081b2823c17c390fcedefb58bad9de35034f"}},
{ref,"04149a7aad1b9fad8d1992bc29bffa0de5fbd498"}},
0},
{<<"thrift">>,
{git,"https://github.com/valitydev/thrift_erlang.git",