diff --git a/apps/capi/src/capi_auth.erl b/apps/capi/src/capi_auth.erl index f26e0ba..f44d837 100644 --- a/apps/capi/src/capi_auth.erl +++ b/apps/capi/src/capi_auth.erl @@ -27,7 +27,7 @@ -type consumer() :: client | merchant | provider. -type token_spec() :: #{ party := binary(), - scope := {invoice | invoice_template, binary()}, + scope := {invoice | invoice_template | customer, binary()}, shop => binary(), lifetime => pos_integer() | unlimited, metadata => token_keeper_client:metadata() @@ -168,7 +168,8 @@ resolve_auth_scope(TokenSpec) -> ). resolve_auth_method(#{scope := {invoice, _}}) -> ?CTX_V1_AUTHMETHOD_INVOICEACCESSTOKEN; -resolve_auth_method(#{scope := {invoice_template, _}}) -> ?CTX_V1_AUTHMETHOD_INVOICETEMPLATEACCESSTOKEN. +resolve_auth_method(#{scope := {invoice_template, _}}) -> ?CTX_V1_AUTHMETHOD_INVOICETEMPLATEACCESSTOKEN; +resolve_auth_method(#{scope := {customer, _}}) -> ?CTX_V1_AUTHMETHOD_CUSTOMERACCESSTOKEN. resolve_auth_expiration(TokenSpec) -> case get_token_lifetime(TokenSpec) of @@ -185,10 +186,13 @@ get_token_lifetime(#{lifetime := LifeTime} = TokenSpec) when LifeTime =/= undefi get_token_lifetime(#{scope := {invoice, _}}) -> ?DEFAULT_INVOICE_ACCESS_TOKEN_LIFETIME; get_token_lifetime(#{scope := {invoice_template, _}}) -> + unlimited; +get_token_lifetime(#{scope := {customer, _}}) -> unlimited. verify_token_lifetime(#{scope := {invoice, _}}, LifeTime) when LifeTime =/= unlimited -> ok; -verify_token_lifetime(#{scope := {invoice_template, _}}, _LifeTime) -> ok. +verify_token_lifetime(#{scope := {invoice_template, _}}, _LifeTime) -> ok; +verify_token_lifetime(#{scope := {customer, _}}, _LifeTime) -> ok. %% diff --git a/apps/capi/src/capi_bouncer_context.erl b/apps/capi/src/capi_bouncer_context.erl index 81de8f9..c94dc1e 100644 --- a/apps/capi/src/capi_bouncer_context.erl +++ b/apps/capi/src/capi_bouncer_context.erl @@ -30,7 +30,8 @@ payment => entity_id(), refund => entity_id(), invoice_template => entity_id(), - webhook => entity_id() + webhook => entity_id(), + customer => entity_id() }. -type prototype_payproc() :: #{ diff --git a/apps/capi/src/capi_feature_schemas.erl b/apps/capi/src/capi_feature_schemas.erl index fe3e63d..ba55898 100644 --- a/apps/capi/src/capi_feature_schemas.erl +++ b/apps/capi/src/capi_feature_schemas.erl @@ -76,11 +76,13 @@ -define(postal_code, 71). -define(date_of_birth, 72). -define(document_id, 73). +-define(metadata, 74). -export([payment/0]). -export([invoice/0]). -export([invoice_template/0]). -export([refund/0]). +-export([customer/0]). -spec payment() -> schema(). payment() -> @@ -166,6 +168,28 @@ refund() -> ?allocation => {<<"allocation">>, {set, allocation_transaction()}} }. +-spec customer() -> schema(). +customer() -> + #{ + ?contact_info => {<<"contactInfo">>, contact_info_schema()}, + ?metadata => <<"metadata">> + }. + +contact_info_schema() -> + #{ + ?email => <<"email">>, + ?phone_number => <<"phoneNumber">>, + ?first_name => <<"firstName">>, + ?last_name => <<"lastName">>, + ?country => <<"country">>, + ?state => <<"state">>, + ?city => <<"city">>, + ?address => <<"address">>, + ?postal_code => <<"postalCode">>, + ?date_of_birth => <<"dateOfBirth">>, + ?document_id => <<"documentId">> + }. + -spec payment_tool_schema() -> schema(). payment_tool_schema() -> {union, <<"type">>, #{ diff --git a/apps/capi/src/capi_handler.erl b/apps/capi/src/capi_handler.erl index 0418a0f..0bf4f86 100644 --- a/apps/capi/src/capi_handler.erl +++ b/apps/capi/src/capi_handler.erl @@ -103,6 +103,7 @@ get_handlers() -> [ capi_handler_categories, capi_handler_countries, + capi_handler_customers, capi_handler_invoice_templates, capi_handler_invoices, capi_handler_parties, @@ -308,6 +309,7 @@ set_context_meta(Context) -> -spec set_request_meta(operation_id(), request_data()) -> ok. set_request_meta(OperationID, Req) -> InterestParams = [ + 'customerID', 'invoiceID', 'invoiceTemplateID', 'webhookID', diff --git a/apps/capi/src/capi_handler_customers.erl b/apps/capi/src/capi_handler_customers.erl new file mode 100644 index 0000000..fd2d2d0 --- /dev/null +++ b/apps/capi/src/capi_handler_customers.erl @@ -0,0 +1,298 @@ +-module(capi_handler_customers). + +-include_lib("damsel/include/dmsl_customer_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("damsel/include/dmsl_base_thrift.hrl"). + +-behaviour(capi_handler). + +-export([prepare/3]). + +-import(capi_handler_utils, [general_error/2, logic_error/2, conflict_error/1]). + +-spec prepare( + OperationID :: capi_handler:operation_id(), + Req :: capi_handler:request_data(), + Context :: capi_handler:processing_context() +) -> {ok, capi_handler:request_state()} | {error, noimpl}. +prepare('CreateCustomer' = OperationID, Req, Context) -> + CustomerParams = maps:get('CustomerParams', Req), + PartyID = capi_handler_utils:get_party_id(Context), + Authorize = fun() -> + Prototypes = [ + {operation, #{id => OperationID, party => PartyID}} + ], + Resolution = capi_auth:authorize_operation(Prototypes, Context), + {ok, Resolution} + end, + Process = fun() -> + try + ExternalID = maps:get(<<"externalID">>, CustomerParams, undefined), + ThriftParams = encode_customer_params(PartyID, CustomerParams), + Call = {customer_management, 'Create', {ThriftParams}}, + case capi_handler_utils:service_call(Call, Context) of + {ok, Customer} -> + maybe_register_external_id(ExternalID, PartyID, Customer, CustomerParams, Context), + {ok, {201, #{}, make_customer_and_token(Customer, ExternalID, Context)}}; + {exception, #base_InvalidRequest{errors = Errors}} -> + FormattedErrors = capi_handler_utils:format_request_errors(Errors), + {ok, logic_error('invalidRequest', FormattedErrors)} + end + catch + throw:{external_id_conflict, CustomerID, SourceExternalID, _Schema} -> + {ok, conflict_error({CustomerID, SourceExternalID})} + end + end, + {ok, #{authorize => Authorize, process => Process}}; +prepare('GetCustomerByExternalID' = OperationID, Req, Context) -> + ExternalID = maps:get('externalID', Req), + PartyID = capi_handler_utils:get_party_id(Context), + {CustomerID, ResultCustomerState} = + case get_customer_by_external_id(PartyID, ExternalID, Context) of + {ok, Result} -> + Result; + {error, internal_id_not_found} -> + {undefined, undefined}; + {exception, _Exception} -> + {undefined, undefined} + end, + Authorize = fun() -> + Prototypes = [ + {operation, #{id => OperationID, party => PartyID, customer => CustomerID}} + ], + Resolution = capi_auth:authorize_operation(Prototypes, Context), + {ok, Resolution} + end, + Process = fun() -> + capi_handler:respond_if_undefined(ResultCustomerState, general_error(404, <<"Customer not found">>)), + Customer = ResultCustomerState#customer_CustomerState.customer, + {ok, {200, #{}, decode_customer(Customer, ExternalID)}} + end, + {ok, #{authorize => Authorize, process => Process}}; +prepare('GetCustomerByID' = OperationID, Req, Context) -> + CustomerID = maps:get('customerID', Req), + ResultCustomerState = get_customer_state(CustomerID, Context), + Authorize = fun() -> + Prototypes = [ + {operation, #{ + id => OperationID, party => get_customer_party_id(ResultCustomerState), customer => CustomerID + }} + ], + Resolution = mask_customer_notfound(capi_auth:authorize_operation(Prototypes, Context)), + {ok, Resolution} + end, + Process = fun() -> + capi_handler:respond_if_undefined(ResultCustomerState, general_error(404, <<"Customer not found">>)), + Customer = ResultCustomerState#customer_CustomerState.customer, + {ok, {200, #{}, decode_customer(Customer)}} + end, + {ok, #{authorize => Authorize, process => Process}}; +prepare('DeleteCustomer' = OperationID, Req, Context) -> + CustomerID = maps:get('customerID', Req), + ResultCustomerState = get_customer_state(CustomerID, Context), + Authorize = fun() -> + Prototypes = [ + {operation, #{ + id => OperationID, party => get_customer_party_id(ResultCustomerState), customer => CustomerID + }} + ], + Resolution = mask_customer_notfound(capi_auth:authorize_operation(Prototypes, Context)), + {ok, Resolution} + end, + Process = fun() -> + capi_handler:respond_if_undefined(ResultCustomerState, general_error(404, <<"Customer not found">>)), + Call = {customer_management, 'Delete', {CustomerID}}, + case capi_handler_utils:service_call(Call, Context) of + {ok, _} -> + {ok, {204, #{}, undefined}}; + {exception, #customer_CustomerNotFound{}} -> + {ok, general_error(404, <<"Customer not found">>)} + end + end, + {ok, #{authorize => Authorize, process => Process}}; +prepare('CreateCustomerAccessToken' = OperationID, Req, Context) -> + CustomerID = maps:get('customerID', Req), + ResultCustomerState = get_customer_state(CustomerID, Context), + Authorize = fun() -> + Prototypes = [ + {operation, #{ + id => OperationID, party => get_customer_party_id(ResultCustomerState), customer => CustomerID + }} + ], + Resolution = mask_customer_notfound(capi_auth:authorize_operation(Prototypes, Context)), + {ok, Resolution} + end, + Process = fun() -> + capi_handler:respond_if_undefined(ResultCustomerState, general_error(404, <<"Customer not found">>)), + Customer = ResultCustomerState#customer_CustomerState.customer, + Response = capi_handler_utils:issue_access_token(Customer, Context), + {ok, {201, #{}, Response}} + end, + {ok, #{authorize => Authorize, process => Process}}; +prepare('GetCustomerPayments' = OperationID, Req, Context) -> + CustomerID = maps:get('customerID', Req), + ResultCustomerState = get_customer_state(CustomerID, Context), + Authorize = fun() -> + Prototypes = [ + {operation, #{ + id => OperationID, party => get_customer_party_id(ResultCustomerState), customer => CustomerID + }} + ], + Resolution = mask_customer_notfound(capi_auth:authorize_operation(Prototypes, Context)), + {ok, Resolution} + end, + Process = fun() -> + capi_handler:respond_if_undefined(ResultCustomerState, general_error(404, <<"Customer not found">>)), + Limit = maps:get('limit', Req), + ContinuationToken = maps:get('continuationToken', Req, undefined), + Call = {customer_management, 'GetPayments', {CustomerID, Limit, ContinuationToken}}, + case capi_handler_utils:service_call(Call, Context) of + {ok, #customer_CustomerPaymentsResponse{payments = Payments, continuation_token = CT}} -> + Response = genlib_map:compact(#{ + <<"result">> => [decode_customer_payment(P) || P <- Payments], + <<"continuationToken">> => CT + }), + {ok, {200, #{}, Response}}; + {exception, #customer_CustomerNotFound{}} -> + {ok, general_error(404, <<"Customer not found">>)} + end + end, + {ok, #{authorize => Authorize, process => Process}}; +prepare('GetCustomerBankCards' = OperationID, Req, Context) -> + CustomerID = maps:get('customerID', Req), + ResultCustomerState = get_customer_state(CustomerID, Context), + Authorize = fun() -> + Prototypes = [ + {operation, #{ + id => OperationID, party => get_customer_party_id(ResultCustomerState), customer => CustomerID + }} + ], + Resolution = mask_customer_notfound(capi_auth:authorize_operation(Prototypes, Context)), + {ok, Resolution} + end, + Process = fun() -> + capi_handler:respond_if_undefined(ResultCustomerState, general_error(404, <<"Customer not found">>)), + Limit = maps:get('limit', Req), + ContinuationToken = maps:get('continuationToken', Req, undefined), + Call = {customer_management, 'GetBankCards', {CustomerID, Limit, ContinuationToken}}, + case capi_handler_utils:service_call(Call, Context) of + {ok, #customer_CustomerBankCardsResponse{bank_cards = BankCards, continuation_token = CT}} -> + Response = genlib_map:compact(#{ + <<"result">> => [decode_customer_bank_card(BC) || BC <- BankCards], + <<"continuationToken">> => CT + }), + {ok, {200, #{}, Response}}; + {exception, #customer_CustomerNotFound{}} -> + {ok, general_error(404, <<"Customer not found">>)} + end + end, + {ok, #{authorize => Authorize, process => Process}}; +prepare(_OperationID, _Req, _Context) -> + {error, noimpl}. + +%% + +get_customer_state(CustomerID, Context) -> + Call = {customer_management, 'Get', {CustomerID}}, + case capi_handler_utils:service_call(Call, Context) of + {ok, CustomerState} -> CustomerState; + {exception, #customer_CustomerNotFound{}} -> undefined + end. + +get_customer_party_id(undefined) -> + undefined; +get_customer_party_id(#customer_CustomerState{customer = Customer}) -> + Customer#customer_Customer.party_ref#domain_PartyConfigRef.id. + +mask_customer_notfound(Resolution) -> + capi_handler:respond_if_forbidden(Resolution, general_error(404, <<"Customer not found">>)). + +encode_customer_params(PartyID, Params) -> + ContactInfo = maps:get(<<"contactInfo">>, Params, undefined), + Metadata = maps:get(<<"metadata">>, Params, undefined), + #customer_CustomerParams{ + party_ref = #domain_PartyConfigRef{id = PartyID}, + contact_info = encode_contact_info(ContactInfo), + metadata = encode_metadata(Metadata) + }. + +encode_contact_info(undefined) -> + undefined; +encode_contact_info(ContactInfo) -> + capi_handler_encoder:encode_contact_info(ContactInfo). + +encode_metadata(undefined) -> + undefined; +encode_metadata(Metadata) -> + capi_json_marshalling:marshal(Metadata). + +%% + +maybe_register_external_id(undefined, _PartyID, _Customer, _CustomerParams, _Context) -> + ok; +maybe_register_external_id(ExternalID, PartyID, Customer, CustomerParams, Context) -> + #{woody_context := WoodyCtx} = Context, + CustomerID = Customer#customer_Customer.id, + IdempotentKey = {'CreateCustomer', PartyID, ExternalID}, + Identity = capi_bender:make_identity(capi_feature_schemas:customer(), CustomerParams), + _ = capi_bender:gen_constant(IdempotentKey, Identity, CustomerID, WoodyCtx), + ok. + +get_customer_by_external_id(PartyID, ExternalID, #{woody_context := WoodyContext} = Context) -> + CustomerKey = {'CreateCustomer', PartyID, ExternalID}, + case capi_bender:get_internal_id(CustomerKey, WoodyContext) of + {ok, CustomerID, _CtxData} -> + Call = {customer_management, 'Get', {CustomerID}}, + case capi_handler_utils:service_call(Call, Context) of + {ok, CustomerState} -> + {ok, {CustomerID, CustomerState}}; + Exception -> + Exception + end; + Error -> + Error + end. + +%% + +make_customer_and_token(Customer, ExternalID, Context) -> + #{ + <<"customer">> => decode_customer(Customer, ExternalID), + <<"customerAccessToken">> => capi_handler_utils:issue_access_token(Customer, Context) + }. + +decode_customer(Customer) -> + decode_customer(Customer, undefined). + +decode_customer(Customer, ExternalID) -> + genlib_map:compact(#{ + <<"id">> => Customer#customer_Customer.id, + <<"externalID">> => ExternalID, + <<"createdAt">> => Customer#customer_Customer.created_at, + <<"contactInfo">> => decode_contact_info(Customer#customer_Customer.contact_info), + <<"metadata">> => decode_metadata(Customer#customer_Customer.metadata) + }). + +decode_contact_info(undefined) -> + undefined; +decode_contact_info(ContactInfo) -> + capi_handler_decoder_party:decode_contact_info(ContactInfo). + +decode_metadata(undefined) -> + undefined; +decode_metadata(Metadata) -> + capi_handler_decoder_utils:decode_metadata(Metadata). + +decode_customer_payment(Payment) -> + #{ + <<"invoiceID">> => Payment#customer_CustomerPayment.invoice_id, + <<"paymentID">> => Payment#customer_CustomerPayment.payment_id, + <<"createdAt">> => Payment#customer_CustomerPayment.created_at + }. + +decode_customer_bank_card(BankCardInfo) -> + genlib_map:compact(#{ + <<"id">> => BankCardInfo#customer_BankCardInfo.id, + <<"cardMask">> => BankCardInfo#customer_BankCardInfo.card_mask, + <<"createdAt">> => BankCardInfo#customer_BankCardInfo.created_at + }). diff --git a/apps/capi/src/capi_handler_utils.erl b/apps/capi/src/capi_handler_utils.erl index 5f15b5c..50c965e 100644 --- a/apps/capi/src/capi_handler_utils.erl +++ b/apps/capi/src/capi_handler_utils.erl @@ -2,6 +2,7 @@ -include_lib("damsel/include/dmsl_payproc_thrift.hrl"). -include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("damsel/include/dmsl_customer_thrift.hrl"). -export([conflict_error/1]). -export([general_error/2]). @@ -39,7 +40,8 @@ -type response() :: capi_handler:response(). -type entity() :: dmsl_domain_thrift:'Invoice'() - | dmsl_domain_thrift:'InvoiceTemplate'(). + | dmsl_domain_thrift:'InvoiceTemplate'() + | dmsl_customer_thrift:'Customer'(). -type token_source() :: capi_auth:token_spec() | entity(). -spec conflict_error(binary() | {binary(), binary()}) -> response(). @@ -133,6 +135,12 @@ issue_access_token(#domain_InvoiceTemplate{} = InvoiceTpl, ProcessingContext) -> shop => InvoiceTpl#domain_InvoiceTemplate.shop_ref#domain_ShopConfigRef.id }, issue_access_token(TokenSpec, ProcessingContext); +issue_access_token(#customer_Customer{} = Customer, ProcessingContext) -> + TokenSpec = #{ + party => Customer#customer_Customer.party_ref#domain_PartyConfigRef.id, + scope => {customer, Customer#customer_Customer.id} + }, + issue_access_token(TokenSpec, ProcessingContext); issue_access_token(TokenSpec, ProcessingContext) -> #{ <<"payload">> => diff --git a/apps/capi/test/capi_base_customer_tests_SUITE.erl b/apps/capi/test/capi_base_customer_tests_SUITE.erl new file mode 100644 index 0000000..ab1a294 --- /dev/null +++ b/apps/capi/test/capi_base_customer_tests_SUITE.erl @@ -0,0 +1,262 @@ +-module(capi_base_customer_tests_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +-include_lib("damsel/include/dmsl_customer_thrift.hrl"). +-include_lib("damsel/include/dmsl_base_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). +-include_lib("bender_proto/include/bender_bender_thrift.hrl"). +-include_lib("capi_dummy_data.hrl"). +-include_lib("capi_bouncer_data.hrl"). + +-export([all/0]). +-export([groups/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). +-export([init_per_group/2]). +-export([end_per_group/2]). +-export([init_per_testcase/2]). +-export([end_per_testcase/2]). + +-export([init/1]). + +-export([ + create_customer_ok_test/1, + create_customer_authorization_error_test/1, + get_customer_by_id_ok_test/1, + get_customer_by_id_not_found_test/1, + get_customer_by_external_id_ok_test/1, + get_customer_by_external_id_not_found_test/1, + delete_customer_ok_test/1, + create_customer_access_token_ok_test/1, + get_customer_payments_ok_test/1, + get_customer_bank_cards_ok_test/1 +]). + +-type test_case_name() :: atom(). +-type config() :: [{atom(), any()}]. +-type group_name() :: atom(). + +-behaviour(supervisor). + +-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init([]) -> + {ok, {#{strategy => one_for_all, intensity => 1, period => 1}, []}}. + +-spec all() -> [{group, test_case_name()}]. +all() -> + [ + {group, operations_by_api_key_token}, + {group, operations_by_user_session_token} + ]. + +customer_tests() -> + [ + create_customer_ok_test, + create_customer_authorization_error_test, + get_customer_by_id_ok_test, + get_customer_by_id_not_found_test, + get_customer_by_external_id_ok_test, + get_customer_by_external_id_not_found_test, + delete_customer_ok_test, + create_customer_access_token_ok_test, + get_customer_payments_ok_test, + get_customer_bank_cards_ok_test + ]. + +-spec groups() -> [{group_name(), list(), [test_case_name()]}]. +groups() -> + [ + {operations_by_api_key_token, [], [ + {group, operations_by_any_token} + ]}, + {operations_by_user_session_token, [], [ + {group, operations_by_any_token} + ]}, + {operations_by_any_token, [], customer_tests()} + ]. + +%% +%% starting/stopping +%% +-spec init_per_suite(config()) -> config(). +init_per_suite(Config) -> + capi_ct_helper:init_suite(?MODULE, Config). + +-spec end_per_suite(config()) -> _. +end_per_suite(C) -> + _ = capi_ct_helper:stop_mocked_service_sup(?config(suite_test_sup, C)), + _ = [application:stop(App) || App <- proplists:get_value(apps, C)], + ok. + +-spec init_per_group(group_name(), config()) -> config(). +init_per_group(operations_by_api_key_token, Config) -> + MockServiceSup = capi_ct_helper:start_mocked_service_sup(?MODULE), + _ = capi_ct_helper_token_keeper:mock_api_key_token(?STRING, MockServiceSup), + _ = capi_ct_helper_bouncer:mock_client(MockServiceSup), + [{context, capi_ct_helper:get_context(?API_TOKEN)}, {group_test_sup, MockServiceSup} | Config]; +init_per_group(operations_by_user_session_token, Config) -> + MockServiceSup = capi_ct_helper:start_mocked_service_sup(?MODULE), + _ = capi_ct_helper_token_keeper:mock_user_session_token(MockServiceSup), + _ = capi_ct_helper_bouncer:mock_client(MockServiceSup), + [{context, capi_ct_helper:get_context(?API_TOKEN)}, {group_test_sup, MockServiceSup} | Config]; +init_per_group(_, Config) -> + Config. + +-spec end_per_group(group_name(), config()) -> _. +end_per_group(_Group, C) -> + _ = capi_utils:'maybe'(?config(group_test_sup, C), fun capi_ct_helper:stop_mocked_service_sup/1), + ok. + +-spec init_per_testcase(test_case_name(), config()) -> config(). +init_per_testcase(_Name, C) -> + MockServiceSup = capi_ct_helper:start_mocked_service_sup(?MODULE), + [{test_sup, MockServiceSup} | C]. + +-spec end_per_testcase(test_case_name(), config()) -> _. +end_per_testcase(_Name, C) -> + capi_ct_helper:stop_mocked_service_sup(?config(test_sup, C)), + ok. + +%%% Tests + +-spec create_customer_ok_test(config()) -> _. +create_customer_ok_test(Config) -> + _ = capi_ct_helper:mock_services( + [ + {customer_management, fun('Create', _) -> {ok, ?CUSTOMER} end} + ], + Config + ), + _ = capi_ct_helper_bouncer:mock_assert_party_op_ctx(<<"CreateCustomer">>, ?STRING, Config), + Req = #{ + <<"contactInfo">> => #{<<"email">> => <<"test@test.ru">>}, + <<"metadata">> => #{<<"key">> => <<"value">>} + }, + {ok, #{ + <<"customer">> := #{ + <<"id">> := ?STRING, + <<"createdAt">> := ?TIMESTAMP + }, + <<"customerAccessToken">> := #{<<"payload">> := _} + }} = capi_client_customers:create_customer(?config(context, Config), Req). + +-spec create_customer_authorization_error_test(config()) -> _. +create_customer_authorization_error_test(Config) -> + _ = capi_ct_helper:mock_services([], Config), + _ = capi_ct_helper_bouncer:mock_arbiter(capi_ct_helper_bouncer:judge_always_forbidden(), Config), + Req = #{ + <<"contactInfo">> => #{<<"email">> => <<"test@test.ru">>} + }, + {error, {401, _}} = capi_client_customers:create_customer(?config(context, Config), Req). + +-spec get_customer_by_id_ok_test(config()) -> _. +get_customer_by_id_ok_test(Config) -> + _ = capi_ct_helper:mock_services( + [{customer_management, fun('Get', _) -> {ok, ?CUSTOMER_STATE} end}], + Config + ), + _ = capi_ct_helper_bouncer:mock_assert_party_op_ctx(<<"GetCustomerByID">>, ?STRING, Config), + {ok, #{ + <<"id">> := ?STRING, + <<"createdAt">> := ?TIMESTAMP + }} = capi_client_customers:get_customer_by_id(?config(context, Config), ?STRING). + +-spec get_customer_by_id_not_found_test(config()) -> _. +get_customer_by_id_not_found_test(Config) -> + _ = capi_ct_helper:mock_services( + [{customer_management, fun('Get', _) -> {throwing, #customer_CustomerNotFound{}} end}], + Config + ), + _ = capi_ct_helper_bouncer:mock_arbiter(capi_ct_helper_bouncer:judge_always_forbidden(), Config), + {error, {404, _}} = capi_client_customers:get_customer_by_id(?config(context, Config), ?STRING). + +-spec get_customer_by_external_id_ok_test(config()) -> _. +get_customer_by_external_id_ok_test(Config) -> + _ = capi_ct_helper:mock_services( + [ + {bender, fun('GetInternalID', _) -> + BenderCtx = capi_msgp_marshalling:marshal(#{<<"context_data">> => #{}}), + {ok, capi_ct_helper_bender:get_internal_id_result(?STRING, BenderCtx)} + end}, + {customer_management, fun('Get', _) -> {ok, ?CUSTOMER_STATE} end} + ], + Config + ), + _ = capi_ct_helper_bouncer:mock_assert_party_op_ctx(<<"GetCustomerByExternalID">>, ?STRING, Config), + {ok, #{ + <<"id">> := ?STRING, + <<"createdAt">> := ?TIMESTAMP + }} = capi_client_customers:get_customer_by_external_id(?config(context, Config), ?STRING). + +-spec get_customer_by_external_id_not_found_test(config()) -> _. +get_customer_by_external_id_not_found_test(Config) -> + _ = capi_ct_helper:mock_services( + [{bender, fun('GetInternalID', _) -> {throwing, #bender_InternalIDNotFound{}} end}], + Config + ), + _ = capi_ct_helper_bouncer:mock_assert_party_op_ctx(<<"GetCustomerByExternalID">>, ?STRING, Config), + {error, {404, _}} = capi_client_customers:get_customer_by_external_id(?config(context, Config), ?STRING). + +-spec delete_customer_ok_test(config()) -> _. +delete_customer_ok_test(Config) -> + _ = capi_ct_helper:mock_services( + [ + {customer_management, fun + ('Get', _) -> {ok, ?CUSTOMER_STATE}; + ('Delete', _) -> {ok, ok} + end} + ], + Config + ), + _ = capi_ct_helper_bouncer:mock_assert_party_op_ctx(<<"DeleteCustomer">>, ?STRING, Config), + {ok, _} = capi_client_customers:delete_customer(?config(context, Config), ?STRING). + +-spec create_customer_access_token_ok_test(config()) -> _. +create_customer_access_token_ok_test(Config) -> + _ = capi_ct_helper:mock_services( + [{customer_management, fun('Get', _) -> {ok, ?CUSTOMER_STATE} end}], + Config + ), + _ = capi_ct_helper_bouncer:mock_assert_party_op_ctx(<<"CreateCustomerAccessToken">>, ?STRING, Config), + {ok, #{<<"payload">> := _}} = + capi_client_customers:create_customer_access_token(?config(context, Config), ?STRING). + +-spec get_customer_payments_ok_test(config()) -> _. +get_customer_payments_ok_test(Config) -> + _ = capi_ct_helper:mock_services( + [ + {customer_management, fun + ('Get', _) -> {ok, ?CUSTOMER_STATE}; + ('GetPayments', _) -> {ok, ?CUSTOMER_PAYMENTS_RESPONSE} + end} + ], + Config + ), + _ = capi_ct_helper_bouncer:mock_assert_party_op_ctx(<<"GetCustomerPayments">>, ?STRING, Config), + {ok, #{<<"result">> := [#{<<"invoiceID">> := ?STRING}]}} = + capi_client_customers:get_customer_payments( + ?config(context, Config), + ?STRING, + #{<<"limit">> => <<"10">>} + ). + +-spec get_customer_bank_cards_ok_test(config()) -> _. +get_customer_bank_cards_ok_test(Config) -> + _ = capi_ct_helper:mock_services( + [ + {customer_management, fun + ('Get', _) -> {ok, ?CUSTOMER_STATE}; + ('GetBankCards', _) -> {ok, ?CUSTOMER_BANK_CARDS_RESPONSE} + end} + ], + Config + ), + _ = capi_ct_helper_bouncer:mock_assert_party_op_ctx(<<"GetCustomerBankCards">>, ?STRING, Config), + {ok, #{<<"result">> := [#{<<"id">> := ?STRING}]}} = + capi_client_customers:get_customer_bank_cards( + ?config(context, Config), + ?STRING, + #{<<"limit">> => <<"10">>} + ). diff --git a/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/dummy.pem b/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/dummy.pem new file mode 100644 index 0000000..059582b --- /dev/null +++ b/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/dummy.pem @@ -0,0 +1,13 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp +wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5 +1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh +3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2 +pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX +GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il +AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF +L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k +X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl +U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ +37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/jwk.json b/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/jwk.json new file mode 100644 index 0000000..1b908b2 --- /dev/null +++ b/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/jwk.json @@ -0,0 +1,7 @@ +{ + "use": "enc", + "kty": "oct", + "kid": "1111", + "alg": "dir", + "k": "d3JPWmpORzVqbGRrZ2s0aUdjQnJ6ZTh1OW1pdk1kR2Y" +} \ No newline at end of file diff --git a/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/jwk.priv.json b/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/jwk.priv.json new file mode 100644 index 0000000..e7d6557 --- /dev/null +++ b/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/jwk.priv.json @@ -0,0 +1,10 @@ +{ + "use": "enc", + "kty": "EC", + "kid": "kxdD0orVPGoAxWrqAMTeQ0U5MRoK47uZxWiSJdgo0t0", + "crv": "P-256", + "alg": "ECDH-ES", + "x": "nHi7TCgBwfrPuNTf49bGvJMczk6WZOI-mCKAghbrOlM", + "y": "_8kiXGOIWkfz57m8K5dmTfbYzCJVYHZZZisCfbYicr0", + "d": "i45qDiARZ5qbS_uzeT-CiKnPUe64qHitKaVdAvcN6TI" +} \ No newline at end of file diff --git a/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/jwk.publ.json b/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/jwk.publ.json new file mode 100644 index 0000000..00b7002 --- /dev/null +++ b/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/jwk.publ.json @@ -0,0 +1,9 @@ +{ + "use": "enc", + "kty": "EC", + "kid": "kxdD0orVPGoAxWrqAMTeQ0U5MRoK47uZxWiSJdgo0t0", + "crv": "P-256", + "alg": "ECDH-ES", + "x": "nHi7TCgBwfrPuNTf49bGvJMczk6WZOI-mCKAghbrOlM", + "y": "_8kiXGOIWkfz57m8K5dmTfbYzCJVYHZZZisCfbYicr0" +} \ No newline at end of file diff --git a/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/private.pem b/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/private.pem new file mode 100644 index 0000000..4e6d12c --- /dev/null +++ b/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/private.pem @@ -0,0 +1,9 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF +B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T +9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+ +gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX +37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX +BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM +GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ== +-----END RSA PRIVATE KEY----- diff --git a/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/public.pem b/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/public.pem new file mode 100644 index 0000000..c2f50d4 --- /dev/null +++ b/apps/capi/test/capi_base_customer_tests_SUITE_data/keys/local/public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg +7F/ZMtGbPFikJnnvRWvFB5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/apps/capi/test/capi_ct_helper_token_keeper.erl b/apps/capi/test/capi_ct_helper_token_keeper.erl index 0bfb896..ed45005 100644 --- a/apps/capi/test/capi_ct_helper_token_keeper.erl +++ b/apps/capi/test/capi_ct_helper_token_keeper.erl @@ -20,6 +20,7 @@ -export([mock_api_key_token/2]). -export([mock_invoice_access_token/3]). -export([mock_invoice_template_access_token/3]). +-export([mock_customer_access_token/3]). -spec mock_token(token_handler(), sup_or_config()) -> list(app_name()). mock_token(HandlerFun, SupOrConfig) -> @@ -121,6 +122,21 @@ mock_invoice_template_access_token(PartyID, InvoiceTemplateID, SupOrConfig) -> end), mock_token(Handler, SupOrConfig). +-spec mock_customer_access_token(binary(), binary(), sup_or_config()) -> list(app_name()). +mock_customer_access_token(PartyID, CustomerID, SupOrConfig) -> + Handler = make_authenticator_handler(fun() -> + AuthParams = #{ + method => <<"CustomerAccessToken">>, + expiration => posix_to_rfc3339(lifetime_to_expiration(?TOKEN_LIFETIME)), + token => #{id => ?STRING}, + scope => [#{party => #{id => PartyID}, customer => #{id => CustomerID}}] + }, + {<<"dev.vality.capi">>, create_bouncer_context(AuthParams), [ + api_key_metadata(), consumer_metadata(<<"client">>) + ]} + end), + mock_token(Handler, SupOrConfig). + %% -spec make_authenticator_handler(function()) -> token_handler(). diff --git a/apps/capi/test/capi_customer_access_token_tests_SUITE.erl b/apps/capi/test/capi_customer_access_token_tests_SUITE.erl new file mode 100644 index 0000000..d5c5dc0 --- /dev/null +++ b/apps/capi/test/capi_customer_access_token_tests_SUITE.erl @@ -0,0 +1,174 @@ +-module(capi_customer_access_token_tests_SUITE). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("stdlib/include/assert.hrl"). + +-include_lib("damsel/include/dmsl_customer_thrift.hrl"). +-include_lib("damsel/include/dmsl_base_thrift.hrl"). +-include_lib("damsel/include/dmsl_domain_thrift.hrl"). + +-include_lib("capi_dummy_data.hrl"). + +-export([all/0]). +-export([groups/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). +-export([init_per_group/2]). +-export([end_per_group/2]). +-export([init_per_testcase/2]). +-export([end_per_testcase/2]). + +-export([init/1]). + +-export([ + get_customer_ok_test/1, + get_customer_payments_ok_test/1, + get_customer_bank_cards_ok_test/1 +]). + +-type test_case_name() :: atom(). +-type config() :: [{atom(), any()}]. +-type group_name() :: atom(). + +-behaviour(supervisor). + +-spec init([]) -> {ok, {supervisor:sup_flags(), [supervisor:child_spec()]}}. +init([]) -> + {ok, {#{strategy => one_for_all, intensity => 1, period => 1}, []}}. + +-spec all() -> [{group, test_case_name()}]. +all() -> + [ + {group, operations_by_customer_access_token_after_customer_creation}, + {group, operations_by_customer_access_token_after_token_creation} + ]. + +customer_access_token_tests() -> + [ + get_customer_ok_test, + get_customer_payments_ok_test, + get_customer_bank_cards_ok_test + ]. + +-spec groups() -> [{group_name(), list(), [test_case_name()]}]. +groups() -> + [ + {operations_by_customer_access_token_after_customer_creation, [], customer_access_token_tests()}, + {operations_by_customer_access_token_after_token_creation, [], customer_access_token_tests()} + ]. + +%% +%% starting/stopping +%% +-spec init_per_suite(config()) -> config(). +init_per_suite(Config) -> + capi_ct_helper:init_suite(?MODULE, Config). + +-spec end_per_suite(config()) -> _. +end_per_suite(C) -> + _ = capi_ct_helper:stop_mocked_service_sup(?config(suite_test_sup, C)), + _ = [application:stop(App) || App <- proplists:get_value(apps, C)], + ok. + +-spec init_per_group(group_name(), config()) -> config(). +init_per_group(operations_by_customer_access_token_after_customer_creation, Config) -> + MockServiceSup = capi_ct_helper:start_mocked_service_sup(?MODULE), + Context = capi_ct_helper:get_context(?API_TOKEN), + _ = capi_ct_helper:mock_services( + [{customer_management, fun('Create', _) -> {ok, ?CUSTOMER} end}], + MockServiceSup + ), + _ = capi_ct_helper_token_keeper:mock_api_key_token(?STRING, MockServiceSup), + _ = capi_ct_helper_bouncer:mock_assert_party_op_ctx(<<"CreateCustomer">>, ?STRING, MockServiceSup), + Req = #{ + <<"contactInfo">> => #{<<"email">> => <<"test@test.ru">>} + }, + {ok, #{ + <<"customer">> := _, + <<"customerAccessToken">> := #{<<"payload">> := CustAccToken} + }} = capi_client_customers:create_customer(Context, Req), + capi_ct_helper:stop_mocked_service_sup(MockServiceSup), + [{context, capi_ct_helper:get_context(CustAccToken)} | Config]; +init_per_group(operations_by_customer_access_token_after_token_creation, Config) -> + MockServiceSup = capi_ct_helper:start_mocked_service_sup(?MODULE), + Context = capi_ct_helper:get_context(?API_TOKEN), + _ = capi_ct_helper:mock_services( + [{customer_management, fun('Get', _) -> {ok, ?CUSTOMER_STATE} end}], + MockServiceSup + ), + _ = capi_ct_helper_token_keeper:mock_api_key_token(?STRING, MockServiceSup), + _ = capi_ct_helper_bouncer:mock_assert_party_op_ctx(<<"CreateCustomerAccessToken">>, ?STRING, MockServiceSup), + {ok, #{<<"payload">> := CustAccToken}} = + capi_client_customers:create_customer_access_token(Context, ?STRING), + capi_ct_helper:stop_mocked_service_sup(MockServiceSup), + [{context, capi_ct_helper:get_context(CustAccToken)} | Config]; +init_per_group(_, Config) -> + Config. + +-spec end_per_group(group_name(), config()) -> _. +end_per_group(_Group, C) -> + _ = capi_utils:'maybe'(?config(group_test_sup, C), fun capi_ct_helper:stop_mocked_service_sup/1), + ok. + +-spec init_per_testcase(test_case_name(), config()) -> config(). +init_per_testcase(_Name, C) -> + MockServiceSup = capi_ct_helper:start_mocked_service_sup(?MODULE), + _ = capi_ct_helper_token_keeper:mock_customer_access_token(?STRING, ?STRING, MockServiceSup), + [{test_sup, MockServiceSup} | C]. + +-spec end_per_testcase(test_case_name(), config()) -> _. +end_per_testcase(_Name, C) -> + capi_ct_helper:stop_mocked_service_sup(?config(test_sup, C)), + ok. + +%%% Tests + +-spec get_customer_ok_test(config()) -> _. +get_customer_ok_test(Config) -> + _ = capi_ct_helper:mock_services( + [{customer_management, fun('Get', _) -> {ok, ?CUSTOMER_STATE} end}], + Config + ), + _ = capi_ct_helper_bouncer:mock_assert_party_op_ctx(<<"GetCustomerByID">>, ?STRING, Config), + {ok, #{ + <<"id">> := ?STRING, + <<"createdAt">> := ?TIMESTAMP + }} = capi_client_customers:get_customer_by_id(?config(context, Config), ?STRING). + +-spec get_customer_payments_ok_test(config()) -> _. +get_customer_payments_ok_test(Config) -> + _ = capi_ct_helper:mock_services( + [ + {customer_management, fun + ('Get', _) -> {ok, ?CUSTOMER_STATE}; + ('GetPayments', _) -> {ok, ?CUSTOMER_PAYMENTS_RESPONSE} + end} + ], + Config + ), + _ = capi_ct_helper_bouncer:mock_assert_party_op_ctx(<<"GetCustomerPayments">>, ?STRING, Config), + {ok, #{<<"result">> := [#{<<"invoiceID">> := ?STRING}]}} = + capi_client_customers:get_customer_payments( + ?config(context, Config), + ?STRING, + #{<<"limit">> => <<"10">>} + ). + +-spec get_customer_bank_cards_ok_test(config()) -> _. +get_customer_bank_cards_ok_test(Config) -> + _ = capi_ct_helper:mock_services( + [ + {customer_management, fun + ('Get', _) -> {ok, ?CUSTOMER_STATE}; + ('GetBankCards', _) -> {ok, ?CUSTOMER_BANK_CARDS_RESPONSE} + end} + ], + Config + ), + _ = capi_ct_helper_bouncer:mock_assert_party_op_ctx(<<"GetCustomerBankCards">>, ?STRING, Config), + {ok, #{<<"result">> := [#{<<"id">> := ?STRING}]}} = + capi_client_customers:get_customer_bank_cards( + ?config(context, Config), + ?STRING, + #{<<"limit">> => <<"10">>} + ). diff --git a/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/dummy.pem b/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/dummy.pem new file mode 100644 index 0000000..059582b --- /dev/null +++ b/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/dummy.pem @@ -0,0 +1,13 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICXAIBAAKBgQCqGKukO1De7zhZj6+H0qtjTkVxwTCpvKe4eCZ0FPqri0cb2JZfXJ/DgYSF6vUp +wmJG8wVQZKjeGcjDOL5UlsuusFncCzWBQ7RKNUSesmQRMSGkVb1/3j+skZ6UtW+5u09lHNsj6tQ5 +1s1SPrCBkedbNf0Tp0GbMJDyR4e9T04ZZwIDAQABAoGAFijko56+qGyN8M0RVyaRAXz++xTqHBLh +3tx4VgMtrQ+WEgCjhoTwo23KMBAuJGSYnRmoBZM3lMfTKevIkAidPExvYCdm5dYq3XToLkkLv5L2 +pIIVOFMDG+KESnAFV7l2c+cnzRMW0+b6f8mR1CJzZuxVLL6Q02fvLi55/mbSYxECQQDeAw6fiIQX +GukBI4eMZZt4nscy2o12KyYner3VpoeE+Np2q+Z3pvAMd/aNzQ/W9WaI+NRfcxUJrmfPwIGm63il +AkEAxCL5HQb2bQr4ByorcMWm/hEP2MZzROV73yF41hPsRC9m66KrheO9HPTJuo3/9s5p+sqGxOlF +L0NDt4SkosjgGwJAFklyR1uZ/wPJjj611cdBcztlPdqoxssQGnh85BzCj/u3WqBpE2vjvyyvyI5k +X6zk7S0ljKtt2jny2+00VsBerQJBAJGC1Mg5Oydo5NwD6BiROrPxGo2bpTbu/fhrT8ebHkTz2epl +U9VQQSQzY1oZMVX8i1m5WUTLPz2yLJIBQVdXqhMCQBGoiuSoSjafUhV7i1cEGpb88h5NBYZzWXGZ +37sJ5QsW+sJyoNde3xH8vdXhzU7eT82D6X/scw9RZz+/6rCJ4p0= +-----END RSA PRIVATE KEY----- \ No newline at end of file diff --git a/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/jwk.json b/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/jwk.json new file mode 100644 index 0000000..1b908b2 --- /dev/null +++ b/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/jwk.json @@ -0,0 +1,7 @@ +{ + "use": "enc", + "kty": "oct", + "kid": "1111", + "alg": "dir", + "k": "d3JPWmpORzVqbGRrZ2s0aUdjQnJ6ZTh1OW1pdk1kR2Y" +} \ No newline at end of file diff --git a/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/jwk.priv.json b/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/jwk.priv.json new file mode 100644 index 0000000..e7d6557 --- /dev/null +++ b/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/jwk.priv.json @@ -0,0 +1,10 @@ +{ + "use": "enc", + "kty": "EC", + "kid": "kxdD0orVPGoAxWrqAMTeQ0U5MRoK47uZxWiSJdgo0t0", + "crv": "P-256", + "alg": "ECDH-ES", + "x": "nHi7TCgBwfrPuNTf49bGvJMczk6WZOI-mCKAghbrOlM", + "y": "_8kiXGOIWkfz57m8K5dmTfbYzCJVYHZZZisCfbYicr0", + "d": "i45qDiARZ5qbS_uzeT-CiKnPUe64qHitKaVdAvcN6TI" +} \ No newline at end of file diff --git a/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/jwk.publ.json b/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/jwk.publ.json new file mode 100644 index 0000000..00b7002 --- /dev/null +++ b/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/jwk.publ.json @@ -0,0 +1,9 @@ +{ + "use": "enc", + "kty": "EC", + "kid": "kxdD0orVPGoAxWrqAMTeQ0U5MRoK47uZxWiSJdgo0t0", + "crv": "P-256", + "alg": "ECDH-ES", + "x": "nHi7TCgBwfrPuNTf49bGvJMczk6WZOI-mCKAghbrOlM", + "y": "_8kiXGOIWkfz57m8K5dmTfbYzCJVYHZZZisCfbYicr0" +} \ No newline at end of file diff --git a/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/private.pem b/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/private.pem new file mode 100644 index 0000000..4e6d12c --- /dev/null +++ b/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/private.pem @@ -0,0 +1,9 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIBOwIBAAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg7F/ZMtGbPFikJnnvRWvF +B5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQJABUY5KIgr4JZEjwLYxQ9T +9uIbLP1Xe/E7yqoqmBk2GGhSrPY0OeRkYnUVLcP96UPQhF63iuG8VF6uZ7oAPsq+ +gQIhANZy3jSCzPjXYHRU1kRqQzpt2S+OqoEiqQ6YG1HrC/VxAiEA0Vq6JlQK2tOX +37SS00dK0Qog4Qi8dN73GliFQNP18EkCIQC4epSA48zkfJMzQBAbRraSuxDNApPX +BzQbo+pMrEDbYQIgY4AncQgIkLB4Qk5kah48JNYXglzQlQtTjiX8Ty9ueGECIQCM +GD3UbQKiA0gf5plBA24I4wFVKxxa4wXbW/7SfP6XmQ== +-----END RSA PRIVATE KEY----- diff --git a/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/public.pem b/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/public.pem new file mode 100644 index 0000000..c2f50d4 --- /dev/null +++ b/apps/capi/test/capi_customer_access_token_tests_SUITE_data/keys/local/public.pem @@ -0,0 +1,4 @@ +-----BEGIN PUBLIC KEY----- +MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAK9fx7qOJT7Aoseu7KKgaLagBh3wvDzg +7F/ZMtGbPFikJnnvRWvFB5oEGbMPblvtF0/fjqfu+eqjP3Z1tUSn7TkCAwEAAQ== +-----END PUBLIC KEY----- diff --git a/apps/capi/test/capi_dummy_data.hrl b/apps/capi/test/capi_dummy_data.hrl index 8009d72..d415146 100644 --- a/apps/capi/test/capi_dummy_data.hrl +++ b/apps/capi/test/capi_dummy_data.hrl @@ -6,6 +6,7 @@ -include_lib("damsel/include/dmsl_payproc_thrift.hrl"). -include_lib("damsel/include/dmsl_webhooker_thrift.hrl"). -include_lib("damsel/include/dmsl_user_interaction_thrift.hrl"). +-include_lib("damsel/include/dmsl_customer_thrift.hrl"). -define(RECORD_UPDATE(FieldIndex, Value, Record), erlang:setelement(FieldIndex, Record, Value)). @@ -130,6 +131,7 @@ -define(PAYPROC_INVOICE(Payments), ?PAYPROC_INVOICE(?INVOICE, Payments)). -define(PAYPROC_INVOICE(Invoice, Payments), #payproc_Invoice{ + latest_event_id = ?INTEGER, invoice = Invoice, payments = Payments }). @@ -141,6 +143,7 @@ -define(PAYPROC_INVOICE_WITH_ID(ID, EID), ?PAYPROC_INVOICE_WITH_ID(ID, EID, ?STRING)). -define(PAYPROC_INVOICE_WITH_ID(ID, EID, OwnerID), #payproc_Invoice{ + latest_event_id = ?INTEGER, invoice = ?INVOICE(ID, EID, OwnerID), payments = [] }). @@ -1369,4 +1372,44 @@ ] }). +%% Customer + +-define(CUSTOMER, #customer_Customer{ + id = ?STRING, + party_ref = #domain_PartyConfigRef{id = ?STRING}, + created_at = ?TIMESTAMP, + status = {active, #customer_CustomerActive{}}, + contact_info = ?CONTACT_INFO, + metadata = {obj, #{<<"key">> => {str, <<"value">>}}} +}). + +-define(CUSTOMER_STATE, #customer_CustomerState{ + customer = ?CUSTOMER, + bank_card_refs = [], + payment_refs = [] +}). + +-define(CUSTOMER_PAYMENT, #customer_CustomerPayment{ + invoice_id = ?STRING, + payment_id = ?STRING, + created_at = ?TIMESTAMP +}). + +-define(CUSTOMER_PAYMENTS_RESPONSE, #customer_CustomerPaymentsResponse{ + payments = [?CUSTOMER_PAYMENT], + continuation_token = undefined +}). + +-define(BANK_CARD_INFO, #customer_BankCardInfo{ + id = ?STRING, + card_mask = <<"424242******4242">>, + created_at = ?TIMESTAMP, + recurrent_providers = [] +}). + +-define(CUSTOMER_BANK_CARDS_RESPONSE, #customer_CustomerBankCardsResponse{ + bank_cards = [?BANK_CARD_INFO], + continuation_token = undefined +}). + -endif. diff --git a/apps/capi_client/src/capi_client_customers.erl b/apps/capi_client/src/capi_client_customers.erl new file mode 100644 index 0000000..3005640 --- /dev/null +++ b/apps/capi_client/src/capi_client_customers.erl @@ -0,0 +1,66 @@ +-module(capi_client_customers). + +-export([create_customer/2]). +-export([get_customer_by_id/2]). +-export([get_customer_by_external_id/2]). +-export([delete_customer/2]). +-export([create_customer_access_token/2]). +-export([get_customer_payments/3]). +-export([get_customer_bank_cards/3]). + +-type context() :: capi_client_lib:context(). + +-spec create_customer(context(), map()) -> {ok, term()} | {error, term()}. +create_customer(Context, Request) -> + Params = #{body => Request}, + {Url, PreparedParams, Opts} = capi_client_lib:make_request(Context, Params), + Response = swag_client_customers_api:create_customer(Url, PreparedParams, Opts), + capi_client_lib:handle_response(Response). + +-spec get_customer_by_id(context(), binary()) -> {ok, term()} | {error, term()}. +get_customer_by_id(Context, CustomerID) -> + Params = #{binding => #{<<"customerID">> => CustomerID}}, + {Url, PreparedParams, Opts} = capi_client_lib:make_request(Context, Params), + Response = swag_client_customers_api:get_customer_by_id(Url, PreparedParams, Opts), + capi_client_lib:handle_response(Response). + +-spec get_customer_by_external_id(context(), binary()) -> {ok, term()} | {error, term()}. +get_customer_by_external_id(Context, ExternalID) -> + Params = #{qs_val => #{<<"externalID">> => ExternalID}}, + {Url, PreparedParams, Opts} = capi_client_lib:make_request(Context, Params), + Response = swag_client_customers_api:get_customer_by_external_id(Url, PreparedParams, Opts), + capi_client_lib:handle_response(Response). + +-spec delete_customer(context(), binary()) -> {ok, term()} | {error, term()}. +delete_customer(Context, CustomerID) -> + Params = #{binding => #{<<"customerID">> => CustomerID}}, + {Url, PreparedParams, Opts} = capi_client_lib:make_request(Context, Params), + Response = swag_client_customers_api:delete_customer(Url, PreparedParams, Opts), + capi_client_lib:handle_response(Response). + +-spec create_customer_access_token(context(), binary()) -> {ok, term()} | {error, term()}. +create_customer_access_token(Context, CustomerID) -> + Params = #{binding => #{<<"customerID">> => CustomerID}}, + {Url, PreparedParams, Opts} = capi_client_lib:make_request(Context, Params), + Response = swag_client_customers_api:create_customer_access_token(Url, PreparedParams, Opts), + capi_client_lib:handle_response(Response). + +-spec get_customer_payments(context(), binary(), map()) -> {ok, term()} | {error, term()}. +get_customer_payments(Context, CustomerID, Qs) -> + Params = #{ + binding => #{<<"customerID">> => CustomerID}, + qs_val => Qs + }, + {Url, PreparedParams, Opts} = capi_client_lib:make_request(Context, Params), + Response = swag_client_customers_api:get_customer_payments(Url, PreparedParams, Opts), + capi_client_lib:handle_response(Response). + +-spec get_customer_bank_cards(context(), binary(), map()) -> {ok, term()} | {error, term()}. +get_customer_bank_cards(Context, CustomerID, Qs) -> + Params = #{ + binding => #{<<"customerID">> => CustomerID}, + qs_val => Qs + }, + {Url, PreparedParams, Opts} = capi_client_lib:make_request(Context, Params), + Response = swag_client_customers_api:get_customer_bank_cards(Url, PreparedParams, Opts), + capi_client_lib:handle_response(Response). diff --git a/apps/capi_woody_client/src/capi_woody_client.erl b/apps/capi_woody_client/src/capi_woody_client.erl index 2a14143..34e3e1c 100644 --- a/apps/capi_woody_client/src/capi_woody_client.erl +++ b/apps/capi_woody_client/src/capi_woody_client.erl @@ -78,7 +78,9 @@ get_service_modname(invoicing) -> get_service_modname(invoice_templating) -> {dmsl_payproc_thrift, 'InvoiceTemplating'}; get_service_modname(webhook_manager) -> - {dmsl_webhooker_thrift, 'WebhookManager'}. + {dmsl_webhooker_thrift, 'WebhookManager'}; +get_service_modname(customer_management) -> + {dmsl_customer_thrift, 'CustomerManagement'}. get_service_deadline(ServiceName) -> ServiceDeadlines = genlib_app:env(?MODULE, service_deadlines, #{}), diff --git a/config/sys.config b/config/sys.config index c39d1a1..b48e31e 100644 --- a/config/sys.config +++ b/config/sys.config @@ -96,7 +96,8 @@ party_management => <<"http://hellgate:8022/v1/processing/partymgmt">>, invoicing => <<"http://hellgate:8022/v1/processing/invoicing">>, invoice_templating => <<"http://hellgate:8022/v1/processing/invoice_templating">>, - webhook_manager => <<"http://hooker:8022/hook">> + webhook_manager => <<"http://hooker:8022/hook">>, + customer_management => <<"http://cubasty:8023/v1/customer/management">> }}, {service_deadlines, #{ % milliseconds diff --git a/rebar.config b/rebar.config index 6d1c3df..61d2480 100644 --- a/rebar.config +++ b/rebar.config @@ -36,7 +36,7 @@ {cowboy_draining_server, {git, "https://github.com/valitydev/cowboy_draining_server.git", {branch, "master"}}}, {woody, {git, "https://github.com/valitydev/woody_erlang.git", {tag, "v1.1.0"}}}, {woody_user_identity, {git, "https://github.com/valitydev/woody_erlang_user_identity.git", {tag, "v1.1.0"}}}, - {damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.21"}}}, + {damsel, {git, "https://github.com/valitydev/damsel.git", {tag, "v2.2.28"}}}, {bender_proto, {git, "https://github.com/valitydev/bender-proto.git", {branch, "master"}}}, {bender_client, {git, "https://github.com/valitydev/bender-client-erlang.git", {tag, "v1.1.0"}}}, {dmt_client, {git, "https://github.com/valitydev/dmt_client.git", {tag, "v2.0.3"}}}, @@ -46,14 +46,16 @@ {scoper, {git, "https://github.com/valitydev/scoper.git", {tag, "v1.1.0"}}}, {erl_health, {git, "https://github.com/valitydev/erlang-health.git", {branch, master}}}, {lechiffre, {git, "https://github.com/valitydev/lechiffre.git", {tag, "v0.1.0"}}}, - {bouncer_proto, {git, "https://github.com/valitydev/bouncer-proto.git", {branch, master}}}, + {bouncer_proto, {git, "https://github.com/valitydev/bouncer-proto.git", {branch, "BG-676/customer"}}}, {bouncer_client, {git, "https://github.com/valitydev/bouncer-client-erlang.git", {tag, "v1.1.0"}}}, {token_keeper_client, {git, "https://github.com/valitydev/token-keeper-client.git", {tag, "v1.1.0"}}}, {party_client, {git, "https://github.com/valitydev/party-client-erlang.git", {tag, "v2.0.2"}}}, {feat, {git, "https://github.com/valitydev/feat.git", {branch, master}}}, %% Libraries generated with swagger-codegen-erlang from valitydev/swag-payments - {swag_server, {git, "https://github.com/valitydev/swag-payments", {branch, "release/erlang/server/v3"}}}, - {swag_client, {git, "https://github.com/valitydev/swag-payments", {branch, "release/erlang/client/v3"}}}, + {swag_server, + {git, "https://github.com/valitydev/swag-payments", {branch, "release/erlang/server/epic-customers"}}}, + {swag_client, + {git, "https://github.com/valitydev/swag-payments", {branch, "release/erlang/client/epic-customers"}}}, %% OpenTelemetry deps {opentelemetry_api, "1.4.0"}, {opentelemetry, "1.5.0"}, diff --git a/rebar.lock b/rebar.lock index 7d1af3e..99eccdc 100644 --- a/rebar.lock +++ b/rebar.lock @@ -15,7 +15,7 @@ 0}, {<<"bouncer_proto">>, {git,"https://github.com/valitydev/bouncer-proto.git", - {ref,"31866c36c049dc568d4bc795a641690db3cb20ab"}}, + {ref,"356ba6aa29fe4df9eb134b69ddea006b209ee714"}}, 0}, {<<"cache">>,{pkg,<<"cache">>,<<"2.3.3">>},1}, {<<"certifi">>,{pkg,<<"certifi">>,<<"2.6.1">>},2}, @@ -41,7 +41,7 @@ {<<"ctx">>,{pkg,<<"ctx">>,<<"0.6.0">>},2}, {<<"damsel">>, {git,"https://github.com/valitydev/damsel.git", - {ref,"0ac1d9c85e61a3f7428e1a6bc56330116e5a8129"}}, + {ref,"bedca9501f9cd00c14c561eeb7dd706016b756dd"}}, 0}, {<<"dmt_client">>, {git,"https://github.com/valitydev/dmt_client.git", @@ -122,11 +122,11 @@ {<<"ssl_verify_fun">>,{pkg,<<"ssl_verify_fun">>,<<"1.1.7">>},2}, {<<"swag_client">>, {git,"https://github.com/valitydev/swag-payments", - {ref,"24d442d4e14eb8ccb7a960fcebdf9a388c2d4700"}}, + {ref,"28d9461c2d691e5f1e30351c2a843013d5193352"}}, 0}, {<<"swag_server">>, {git,"https://github.com/valitydev/swag-payments", - {ref,"fddf13c86f824c75d9750cec90c9c742c7a377e7"}}, + {ref,"116043fe83236644c71c854ccf4437943665bd96"}}, 0}, {<<"thrift">>, {git,"https://github.com/valitydev/thrift_erlang.git",