CP-14: add validation step (#84)

* CP-14: add validation step

* CP-14: fix issues

* CP-14: fix spec

---------

Co-authored-by: ttt161 <losto@nix>
This commit is contained in:
ttt161 2024-06-25 13:00:40 +03:00 committed by GitHub
parent a701267af5
commit 8460d6e691
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 314 additions and 11 deletions

View File

@ -11,6 +11,7 @@
scoper,
party_client,
fistful_proto,
ff_validator,
fistful,
ff_transfer,
w2w,

View File

@ -79,7 +79,8 @@ marshal_withdrawal_state(WithdrawalState, Context) ->
adjustments = [ff_withdrawal_adjustment_codec:marshal(adjustment_state, A) || A <- Adjustments],
context = marshal(ctx, Context),
metadata = marshal(ctx, ff_withdrawal:metadata(WithdrawalState)),
quote = maybe_marshal(quote_state, ff_withdrawal:quote(WithdrawalState))
quote = maybe_marshal(quote_state, ff_withdrawal:quote(WithdrawalState)),
withdrawal_validation = maybe_marshal(withdrawal_validation, ff_withdrawal:validation(WithdrawalState))
}.
-spec marshal_event(ff_withdrawal_machine:event()) -> fistful_wthd_thrift:'Event'().
@ -119,6 +120,19 @@ marshal(change, {adjustment, #{id := ID, payload := Payload}}) ->
id = marshal(id, ID),
payload = ff_withdrawal_adjustment_codec:marshal(change, Payload)
}};
marshal(change, {validation, {Part, ValidationResult}}) when Part =:= sender; Part =:= receiver ->
{validation, {Part, marshal(validation_result, ValidationResult)}};
marshal(validation_result, {personal, #{validation_id := ValidationID, token := Token, validation_status := Status}}) ->
{
personal,
#wthd_PersonalDataValidationResult{
validation_id = marshal(id, ValidationID),
token = marshal(string, Token),
validation_status = marshal(validation_status, Status)
}
};
marshal(validation_status, V) when V =:= valid; V =:= invalid ->
V;
marshal(withdrawal, Withdrawal) ->
#wthd_Withdrawal{
id = marshal(id, ff_withdrawal:id(Withdrawal)),
@ -190,6 +204,11 @@ marshal(quote, Quote) ->
domain_revision = maybe_marshal(domain_revision, genlib_map:get(domain_revision, Quote)),
operation_timestamp = maybe_marshal(timestamp_ms, genlib_map:get(operation_timestamp, Quote))
};
marshal(withdrawal_validation, WithdrawalValidation) ->
#wthd_WithdrawalValidation{
sender = maybe_marshal({list, validation_result}, maps:get(sender, WithdrawalValidation, undefined)),
receiver = maybe_marshal({list, validation_result}, maps:get(receiver, WithdrawalValidation, undefined))
};
marshal(ctx, Ctx) ->
maybe_marshal(context, Ctx);
marshal(T, V) ->
@ -227,6 +246,19 @@ unmarshal(change, {adjustment, Change}) ->
id => unmarshal(id, Change#wthd_AdjustmentChange.id),
payload => ff_withdrawal_adjustment_codec:unmarshal(change, Change#wthd_AdjustmentChange.payload)
}};
unmarshal(change, {validation, {Part, ValidationResult}}) when Part =:= sender; Part =:= receiver ->
{validation, {Part, unmarshal(validation_result, ValidationResult)}};
unmarshal(validation_result, {personal, Validation}) ->
{personal, #{
validation_id => unmarshal(id, Validation#wthd_PersonalDataValidationResult.validation_id),
token => unmarshal(string, Validation#wthd_PersonalDataValidationResult.token),
validation_status => unmarshal(
validation_status,
Validation#wthd_PersonalDataValidationResult.validation_status
)
}};
unmarshal(validation_status, V) when V =:= valid; V =:= invalid ->
V;
unmarshal(withdrawal, Withdrawal = #wthd_Withdrawal{}) ->
ff_withdrawal:gen(#{
id => unmarshal(id, Withdrawal#wthd_Withdrawal.id),

View File

@ -8,6 +8,7 @@
-include_lib("fistful_proto/include/fistful_fistful_thrift.hrl").
-include_lib("fistful_proto/include/fistful_fistful_base_thrift.hrl").
-include_lib("fistful_proto/include/fistful_cashflow_thrift.hrl").
-include_lib("validator_personal_data_proto/include/validator_personal_data_validator_personal_data_thrift.hrl").
-export([all/0]).
-export([groups/0]).
@ -31,6 +32,8 @@
-export([create_destination_resource_no_bindata_fail_test/1]).
-export([create_destination_notfound_test/1]).
-export([create_destination_generic_ok_test/1]).
-export([create_destination_auth_data_valid_test/1]).
-export([create_destination_auth_data_invalid_test/1]).
-export([create_wallet_notfound_test/1]).
-export([unknown_test/1]).
-export([get_context_test/1]).
@ -48,7 +51,10 @@
-spec all() -> [test_case_name() | {group, group_name()}].
all() ->
[{group, default}].
[
{group, default},
{group, validator}
].
-spec groups() -> [{group_name(), list(), [test_case_name()]}].
groups() ->
@ -75,6 +81,10 @@ groups() ->
create_adjustment_already_has_status_error_test,
create_adjustment_already_has_data_revision_error_test,
withdrawal_state_content_test
]},
{validator, [], [
create_destination_auth_data_valid_test,
create_destination_auth_data_invalid_test
]}
].
@ -389,6 +399,114 @@ create_destination_generic_ok_test(C) ->
FinalWithdrawalState#wthd_WithdrawalState.status
).
-spec create_destination_auth_data_valid_test(config()) -> test_return().
create_destination_auth_data_valid_test(C) ->
%% mock validator
ok = meck:expect(ff_woody_client, call, fun
(validator, {_, _, {Token}}) ->
{ok, #validator_personal_data_ValidationResponse{
validation_id = <<"ID">>,
token = Token,
validation_status = valid
}};
(Service, Request) ->
meck:passthrough([Service, Request])
end),
Cash = make_cash({424242, <<"RUB">>}),
AuthData = #{
auth_data => #{
sender => <<"SenderPersonalDataToken">>,
receiver => <<"ReceiverPersonalDataToken">>
}
},
#{
wallet_id := WalletID,
destination_id := DestinationID
} = prepare_standard_environment(Cash, undefined, AuthData, C),
WithdrawalID = generate_id(),
Params = #wthd_WithdrawalParams{
id = WithdrawalID,
wallet_id = WalletID,
destination_id = DestinationID,
body = Cash
},
Result = call_withdrawal('Create', {Params, #{}}),
?assertMatch({ok, _}, Result),
succeeded = await_final_withdrawal_status(WithdrawalID),
ExpectedValidation = #wthd_WithdrawalValidation{
sender = [
{personal, #wthd_PersonalDataValidationResult{
validation_id = <<"ID">>,
token = <<"SenderPersonalDataToken">>,
validation_status = valid
}}
],
receiver = [
{personal, #wthd_PersonalDataValidationResult{
validation_id = <<"ID">>,
token = <<"ReceiverPersonalDataToken">>,
validation_status = valid
}}
]
},
{ok, WithdrawalState} = call_withdrawal('Get', {WithdrawalID, #'fistful_base_EventRange'{}}),
?assertEqual(ExpectedValidation, WithdrawalState#wthd_WithdrawalState.withdrawal_validation),
meck:unload(ff_woody_client).
-spec create_destination_auth_data_invalid_test(config()) -> test_return().
create_destination_auth_data_invalid_test(C) ->
%% mock validator
ok = meck:expect(ff_woody_client, call, fun
(validator, {_, _, {Token}}) ->
{ok, #validator_personal_data_ValidationResponse{
validation_id = <<"ID">>,
token = Token,
validation_status = invalid
}};
(Service, Request) ->
meck:passthrough([Service, Request])
end),
Cash = make_cash({424242, <<"RUB">>}),
AuthData = #{
auth_data => #{
sender => <<"SenderPersonalDataToken">>,
receiver => <<"ReceiverPersonalDataToken">>
}
},
#{
wallet_id := WalletID,
destination_id := DestinationID
} = prepare_standard_environment(Cash, undefined, AuthData, C),
WithdrawalID = generate_id(),
Params = #wthd_WithdrawalParams{
id = WithdrawalID,
wallet_id = WalletID,
destination_id = DestinationID,
body = Cash
},
Result = call_withdrawal('Create', {Params, #{}}),
?assertMatch({ok, _}, Result),
{failed, #{code := <<"invalid_personal_data">>}} = await_final_withdrawal_status(WithdrawalID),
ExpectedValidation = #wthd_WithdrawalValidation{
sender = [
{personal, #wthd_PersonalDataValidationResult{
validation_id = <<"ID">>,
token = <<"SenderPersonalDataToken">>,
validation_status = invalid
}}
],
receiver = [
{personal, #wthd_PersonalDataValidationResult{
validation_id = <<"ID">>,
token = <<"ReceiverPersonalDataToken">>,
validation_status = invalid
}}
]
},
{ok, WithdrawalState} = call_withdrawal('Get', {WithdrawalID, #'fistful_base_EventRange'{}}),
?assertEqual(ExpectedValidation, WithdrawalState#wthd_WithdrawalState.withdrawal_validation),
meck:unload(ff_woody_client).
-spec create_wallet_notfound_test(config()) -> test_return().
create_wallet_notfound_test(C) ->
Cash = make_cash({100, <<"RUB">>}),
@ -573,6 +691,9 @@ prepare_standard_environment(Body, C) ->
prepare_standard_environment(Body, undefined, C).
prepare_standard_environment(Body, Token, C) ->
prepare_standard_environment(Body, Token, #{}, C).
prepare_standard_environment(Body, Token, AuthData, C) ->
#'fistful_base_Cash'{
amount = Amount,
currency = #'fistful_base_CurrencyRef'{symbolic_code = Currency}
@ -581,7 +702,7 @@ prepare_standard_environment(Body, Token, C) ->
IdentityID = create_identity(Party, C),
WalletID = create_wallet(IdentityID, <<"My wallet">>, Currency, C),
ok = await_wallet_balance({0, Currency}, WalletID),
DestinationID = create_destination(IdentityID, Token, C),
DestinationID = create_destination(IdentityID, Token, AuthData, C),
ok = set_wallet_balance({Amount, Currency}, WalletID),
#{
identity_id => IdentityID,
@ -694,12 +815,12 @@ get_account_balance(Account) ->
generate_id() ->
ff_id:generate_snowflake_id().
create_destination(IID, <<"USD_CURRENCY">>, C) ->
create_destination(IID, <<"USD">>, undefined, C);
create_destination(IID, Token, C) ->
create_destination(IID, <<"RUB">>, Token, C).
create_destination(IID, <<"USD_CURRENCY">>, AuthData, C) ->
create_destination(IID, <<"USD">>, undefined, AuthData, C);
create_destination(IID, Token, AuthData, C) ->
create_destination(IID, <<"RUB">>, Token, AuthData, C).
create_destination(IID, Currency, Token, C) ->
create_destination(IID, Currency, Token, AuthData, C) ->
ID = generate_id(),
StoreSource = ct_cardstore:bank_card(<<"4150399999000900">>, {12, 2025}, C),
NewStoreResource =
@ -710,7 +831,8 @@ create_destination(IID, Currency, Token, C) ->
StoreSource#{token => Token}
end,
Resource = {bank_card, #{bank_card => NewStoreResource}},
Params = #{id => ID, identity => IID, name => <<"XDesination">>, currency => Currency, resource => Resource},
Params0 = #{id => ID, identity => IID, name => <<"XDesination">>, currency => Currency, resource => Resource},
Params = maps:merge(AuthData, Params0),
ok = ff_destination_machine:create(Params, ff_entity_context:new()),
authorized = ct_helper:await(
authorized,

View File

@ -24,7 +24,8 @@
adjustments => adjustments_index(),
status => status(),
metadata => metadata(),
external_id => id()
external_id => id(),
validation => withdrawal_validation()
}.
-opaque withdrawal() :: #{
@ -56,12 +57,29 @@
| succeeded
| {failed, failure()}.
-type withdrawal_validation() :: #{
sender => validation_result(),
receiver => validation_result()
}.
-type validation_result() ::
{personal, personal_data_validation()}.
-type personal_data_validation() :: #{
validation_id := binary(),
token := binary(),
validation_status := validation_status()
}.
-type validation_status() :: valid | invalid.
-type event() ::
{created, withdrawal()}
| {resource_got, destination_resource()}
| {route_changed, route()}
| {p_transfer, ff_postings_transfer:event()}
| {limit_check, limit_check_details()}
| {validation, {sender | receiver, validation_result()}}
| {session_started, session_id()}
| {session_finished, {session_id(), session_result()}}
| {status_changed, status()}
@ -217,6 +235,7 @@
-export([metadata/1]).
-export([params/1]).
-export([activity/1]).
-export([validation/1]).
%% API
@ -300,6 +319,7 @@
routing
| p_transfer_start
| p_transfer_prepare
| validating
| session_starting
| session_sleeping
| p_transfer_commit
@ -315,6 +335,7 @@
limit_check
| ff_withdrawal_routing:route_not_found()
| {inconsistent_quote_route, {provider_id, provider_id()} | {terminal_id, terminal_id()}}
| {validation_personal_data, sender | receiver}
| session.
-type session_processing_status() :: undefined | pending | succeeded | failed.
@ -395,6 +416,12 @@ params(#{params := V}) ->
activity(Withdrawal) ->
deduce_activity(Withdrawal).
-spec validation(withdrawal_state()) -> withdrawal_validation() | undefined.
validation(#{validation := WithdrawalValidation}) ->
WithdrawalValidation;
validation(_) ->
undefined.
%% API
-spec gen(gen_args()) -> withdrawal().
@ -697,6 +724,7 @@ deduce_activity(Withdrawal) ->
Params = #{
route => route_selection_status(Withdrawal),
p_transfer => p_transfer_status(Withdrawal),
validation => withdrawal_validation_status(Withdrawal),
session => get_current_session_status(Withdrawal),
status => status(Withdrawal),
limit_check => limit_check_processing_status(Withdrawal),
@ -719,6 +747,8 @@ do_pending_activity(#{p_transfer := created}) ->
p_transfer_prepare;
do_pending_activity(#{p_transfer := prepared, limit_check := unknown}) ->
limit_check;
do_pending_activity(#{p_transfer := prepared, limit_check := ok, validation := undefined}) ->
validating;
do_pending_activity(#{p_transfer := prepared, limit_check := ok, session := undefined}) ->
session_starting;
do_pending_activity(#{p_transfer := prepared, limit_check := failed}) ->
@ -764,6 +794,8 @@ do_process_transfer(p_transfer_cancel, Withdrawal) ->
{continue, [{p_transfer, Ev} || Ev <- Events]};
do_process_transfer(limit_check, Withdrawal) ->
process_limit_check(Withdrawal);
do_process_transfer(validating, Withdrawal) ->
process_withdrawal_validation(Withdrawal);
do_process_transfer(session_starting, Withdrawal) ->
process_session_creation(Withdrawal);
do_process_transfer(session_sleeping, Withdrawal) ->
@ -932,6 +964,25 @@ process_p_transfer_creation(Withdrawal) ->
{ok, PostingsTransferEvents} = ff_postings_transfer:create(PTransferID, FinalCashFlow),
{continue, [{p_transfer, Ev} || Ev <- PostingsTransferEvents]}.
-spec process_withdrawal_validation(withdrawal_state()) -> process_result().
process_withdrawal_validation(Withdrawal) ->
DestinationID = destination_id(Withdrawal),
{ok, Destination} = get_destination(DestinationID),
#{
auth_data := #{
sender := SenderToken,
receiver := ReceiverToken
}
} = Destination,
SenderValidationPDResult = unwrap(ff_validator:validate_personal_data(SenderToken)),
ReceiverValidationPDResult = unwrap(ff_validator:validate_personal_data(ReceiverToken)),
Events = [
{validation, {sender, {personal, SenderValidationPDResult}}},
{validation, {receiver, {personal, ReceiverValidationPDResult}}}
],
MaybeFailEvent = maybe_fail_validation(Events, Withdrawal),
{continue, Events ++ MaybeFailEvent}.
-spec process_session_creation(withdrawal_state()) -> process_result().
process_session_creation(Withdrawal) ->
ID = construct_session_id(Withdrawal),
@ -1368,6 +1419,28 @@ quote_domain_revision(undefined) ->
quote_domain_revision(Quote) ->
maps:get(domain_revision, Quote, undefined).
%% Validation
-spec withdrawal_validation_status(withdrawal_state()) -> validated | skipped | undefined.
withdrawal_validation_status(#{validation := _Validation}) ->
validated;
withdrawal_validation_status(#{params := #{destination_id := DestinationID}}) ->
case get_destination(DestinationID) of
{ok, #{auth_data := _AuthData}} ->
undefined;
_ ->
skipped
end.
maybe_fail_validation([], _Withdrawal) ->
[];
maybe_fail_validation(
[{validation, {Part, {personal, #{validation_status := invalid}}}} | _Tail],
Withdrawal
) when Part =:= sender; Part =:= receiver ->
process_transfer_fail({validation_personal_data, Part}, Withdrawal);
maybe_fail_validation([_Valid | Tail], Withdrawal) ->
maybe_fail_validation(Tail, Withdrawal).
%% Session management
-spec session_id(withdrawal_state()) -> session_id() | undefined.
@ -1873,6 +1946,11 @@ build_failure({inconsistent_quote_route, {Type, FoundID}}, Withdrawal) ->
code => <<"unknown">>,
reason => genlib:format(Details)
};
build_failure({validation_personal_data, Part}, _Withdrawal) ->
#{
code => <<"invalid_personal_data">>,
reason => genlib:format(Part)
};
build_failure(session, Withdrawal) ->
Result = get_session_result(Withdrawal),
{failed, Failure} = Result,
@ -1923,6 +2001,10 @@ apply_event_({route_changed, Route}, T) ->
route => Route,
attempts => R
};
apply_event_({validation, {Part, ValidationResult}}, T) ->
Validates = maps:get(validation, T, #{}),
PartValidations = maps:get(Part, Validates, []),
T#{validation => Validates#{Part => [ValidationResult | PartValidations]}};
apply_event_({adjustment, _Ev} = Event, T) ->
apply_adjustment_event(Event, T).

View File

@ -0,0 +1,2 @@
{erl_opts, [debug_info]}.
{deps, []}.

View File

@ -0,0 +1,14 @@
{application, ff_validator, [
{description, "An OTP library"},
{vsn, "0.1.0"},
{registered, []},
{applications, [
kernel,
stdlib,
validator_personal_data_proto
]},
{env, []},
{modules, []},
{links, []}
]}.

View File

@ -0,0 +1,42 @@
-module(ff_validator).
-include_lib("validator_personal_data_proto/include/validator_personal_data_validator_personal_data_thrift.hrl").
-include_lib("damsel/include/dmsl_base_thrift.hrl").
-define(SERVICE, {validator_personal_data_validator_personal_data_thrift, 'ValidatorPersonalDataService'}).
%% API
-export([validate_personal_data/1]).
-type personal_data_validation_response() :: #{
validation_id := binary(),
token := binary(),
validation_status := valid | invalid
}.
-type personal_data_token() :: binary().
-spec validate_personal_data(personal_data_token()) -> {ok, personal_data_validation_response()} | {error, _Reason}.
validate_personal_data(PersonalToken) ->
Args = {PersonalToken},
Request = {?SERVICE, 'ValidatePersonalData', Args},
case ff_woody_client:call(validator, Request) of
{ok, Result} ->
{ok, unmarshal(personal_data_validation, Result)};
{exception, #validator_personal_data_PersonalDataTokenNotFound{}} ->
{error, not_found};
{exception, #base_InvalidRequest{}} ->
{error, invalid_request}
end.
%% Internal functions
unmarshal(personal_data_validation, #validator_personal_data_ValidationResponse{
validation_id = ID,
token = Token,
validation_status = Status
}) ->
#{
validation_id => ID,
token => Token,
validation_status => Status
}.

View File

@ -91,7 +91,8 @@
{services, #{
'automaton' => "http://machinegun:8022/v1/automaton",
'accounter' => "http://shumway:8022/accounter",
'limiter' => "http://limiter:8022/v1/limiter"
'limiter' => "http://limiter:8022/v1/limiter",
'validator' => "http://validator:8022/v1/validator_personal_data"
}}
]},

View File

@ -42,6 +42,8 @@
{party_client, {git, "https://github.com/valitydev/party-client-erlang.git", {branch, "master"}}},
{bender_client, {git, "https://github.com/valitydev/bender-client-erlang.git", {branch, "master"}}},
{limiter_proto, {git, "https://github.com/valitydev/limiter-proto.git", {branch, "master"}}},
{validator_personal_data_proto,
{git, "https://github.com/valitydev/validator-personal-data-proto.git", {branch, "master"}}},
{opentelemetry_api, "1.2.1"},
{opentelemetry, "1.3.0"},
{opentelemetry_exporter, "1.3.0"}
@ -70,6 +72,7 @@
"apps/ff_core",
"apps/ff_server",
"apps/ff_transfer",
"apps/ff_validator",
"apps/fistful",
"apps/machinery_extra",
"apps/w2w"

View File

@ -114,6 +114,10 @@
{git,"https://github.com/okeuday/uuid.git",
{ref,"965c76b7343530cf940a808f497eef37d0a332e6"}},
0},
{<<"validator_personal_data_proto">>,
{git,"https://github.com/valitydev/validator-personal-data-proto.git",
{ref,"adad5026cbac206718271d8f540edccd44f56256"}},
0},
{<<"woody">>,
{git,"https://github.com/valitydev/woody_erlang.git",
{ref,"81219ba5408e1c67f5eaed3c7e566ede42da88d4"}},