From 635952a8e5224812dd56fd0dfd155a35464b2da4 Mon Sep 17 00:00:00 2001 From: Paul Nicolas Date: Wed, 2 Oct 2024 15:03:21 +0200 Subject: [PATCH] feat(*): add transfer and payout creation --- .gitignore | 1 + internal/api/backend/backend.go | 18 + internal/api/backend/backend_generated.go | 175 ++++ internal/api/services/accounts_get.go | 7 +- internal/api/services/accounts_list.go | 7 +- internal/api/services/balances_list.go | 7 +- internal/api/services/bank_accounts_create.go | 2 +- .../bank_accounts_forward_to_connector.go | 7 +- internal/api/services/bank_accounts_get.go | 7 +- internal/api/services/bank_accounts_list.go | 7 +- .../services/bank_accounts_update_metadata.go | 2 +- .../services/connectors_handle_webhooks.go | 2 +- internal/api/services/connectors_uninstall.go | 2 +- .../payment_initiation_adjustments_get.go | 28 + .../payment_initiation_adjustments_list.go | 44 + ...ayment_initiation_related_payments_list.go | 44 + .../services/payment_initiations_approve.go | 49 + .../services/payment_initiations_create.go | 37 + .../services/payment_initiations_delete.go | 37 + .../api/services/payment_initiations_get.go | 16 + .../api/services/payment_initiations_list.go | 18 + .../services/payment_initiations_reject.go | 51 + .../api/services/payment_initiations_retry.go | 96 ++ internal/api/services/payments_get.go | 7 +- internal/api/services/payments_list.go | 7 +- .../api/services/payments_update_metadata.go | 2 +- internal/api/services/pools_get.go | 7 +- internal/api/services/pools_list.go | 7 +- internal/api/services/schedules_get.go | 7 +- .../v2/handler_transfer_initiations_create.go | 170 +++ .../v2/handler_transfer_initiations_delete.go | 36 + .../v2/handler_transfer_initiations_get.go | 166 +++ .../v2/handler_transfer_initiations_list.go | 77 ++ .../v2/handler_transfer_initiations_retry.go | 36 + .../handler_transfer_initiations_reverse.go | 13 + ...dler_transfer_initiations_update_status.go | 79 ++ internal/api/v2/router.go | 20 + ...ler_payment_initiation_adjustments_list.go | 49 + ...andler_payment_initiation_payments_list.go | 49 + .../v3/handler_payment_initiations_approve.go | 33 + .../v3/handler_payment_initiations_create.go | 127 +++ .../v3/handler_payment_initiations_delete.go | 33 + .../api/v3/handler_payment_initiations_get.go | 46 + .../v3/handler_payment_initiations_list.go | 64 ++ .../v3/handler_payment_initiations_reject.go | 33 + .../v3/handler_payment_initiations_retry.go | 33 + internal/api/v3/router.go | 27 +- .../connectors/engine/activities/activity.go | 24 + .../engine/activities/plugin_create_payout.go | 39 + .../activities/plugin_create_payout_test.go | 88 ++ .../activities/plugin_create_transfer.go | 39 + .../activities/plugin_create_transfer_test.go | 88 ++ .../engine/activities/storage_accounts_get.go | 22 + ...ge_payment_initiations_adjusments_store.go | 18 + .../storage_payment_initiations_get.go | 20 + ...ment_initiations_related_payments_store.go | 29 + internal/connectors/engine/engine.go | 62 ++ internal/connectors/engine/plugins/impl.go | 36 + .../engine/workflow/create_payout.go | 132 +++ .../engine/workflow/create_transfer.go | 157 +++ .../connectors/engine/workflow/workflow.go | 8 + internal/connectors/grpc/grpc_client.go | 8 + internal/connectors/grpc/grpc_server.go | 8 + internal/connectors/grpc/interfaces.go | 2 + .../grpc/proto/payment_initiation.pb.go | 252 +++++ .../grpc/proto/payment_initiation.proto | 30 + .../grpc/proto/services/plugin.pb.go | 876 ++++++++++------ .../grpc/proto/services/plugin.proto | 19 + .../grpc/proto/services/plugin_grpc.pb.go | 72 ++ internal/connectors/grpc/translate.go | 57 + internal/connectors/plugins/errors.go | 3 +- internal/connectors/plugins/grpc.go | 48 + .../connectors/plugins/public/adyen/plugin.go | 8 + .../plugins/public/bankingcircle/plugin.go | 8 + .../plugins/public/currencycloud/plugin.go | 8 + .../plugins/public/generic/plugin.go | 8 + .../plugins/public/mangopay/accounts.go | 2 +- .../plugins/public/mangopay/client/payout.go | 2 + .../public/mangopay/client/transfer.go | 32 + .../plugins/public/mangopay/metadata.go | 5 + .../plugins/public/mangopay/payouts.go | 109 ++ .../plugins/public/mangopay/plugin.go | 29 + .../plugins/public/mangopay/transfers.go | 103 ++ .../plugins/public/modulr/plugin.go | 8 + .../plugins/public/moneycorp/plugin.go | 8 + .../plugins/public/stripe/plugin.go | 8 + .../connectors/plugins/public/wise/plugin.go | 8 + internal/models/accounts.go | 11 + internal/models/errors.go | 1 + .../payment_initiation_adjustment_id.go | 80 ++ internal/models/payment_initiation_type.go | 99 ++ .../payments_initation_related_payments.go | 9 + .../payments_initiation_adjusments_status.go | 135 +++ .../models/payments_initiation_adjustments.go | 74 ++ internal/models/payments_initiations.go | 206 +++- internal/models/plugin.go | 18 + internal/models/plugin_generated.go | 30 + internal/storage/migrations/0-init-schema.sql | 75 +- internal/storage/payment_initiations.go | 444 ++++++++ internal/storage/payment_initiations_test.go | 975 ++++++++++++++++++ internal/storage/storage.go | 17 + internal/storage/storage_generated.go | 164 +++ 102 files changed, 6294 insertions(+), 319 deletions(-) create mode 100644 internal/api/services/payment_initiation_adjustments_get.go create mode 100644 internal/api/services/payment_initiation_adjustments_list.go create mode 100644 internal/api/services/payment_initiation_related_payments_list.go create mode 100644 internal/api/services/payment_initiations_approve.go create mode 100644 internal/api/services/payment_initiations_create.go create mode 100644 internal/api/services/payment_initiations_delete.go create mode 100644 internal/api/services/payment_initiations_get.go create mode 100644 internal/api/services/payment_initiations_list.go create mode 100644 internal/api/services/payment_initiations_reject.go create mode 100644 internal/api/services/payment_initiations_retry.go create mode 100644 internal/api/v2/handler_transfer_initiations_create.go create mode 100644 internal/api/v2/handler_transfer_initiations_delete.go create mode 100644 internal/api/v2/handler_transfer_initiations_get.go create mode 100644 internal/api/v2/handler_transfer_initiations_list.go create mode 100644 internal/api/v2/handler_transfer_initiations_retry.go create mode 100644 internal/api/v2/handler_transfer_initiations_reverse.go create mode 100644 internal/api/v2/handler_transfer_initiations_update_status.go create mode 100644 internal/api/v3/handler_payment_initiation_adjustments_list.go create mode 100644 internal/api/v3/handler_payment_initiation_payments_list.go create mode 100644 internal/api/v3/handler_payment_initiations_approve.go create mode 100644 internal/api/v3/handler_payment_initiations_create.go create mode 100644 internal/api/v3/handler_payment_initiations_delete.go create mode 100644 internal/api/v3/handler_payment_initiations_get.go create mode 100644 internal/api/v3/handler_payment_initiations_list.go create mode 100644 internal/api/v3/handler_payment_initiations_reject.go create mode 100644 internal/api/v3/handler_payment_initiations_retry.go create mode 100644 internal/connectors/engine/activities/plugin_create_payout.go create mode 100644 internal/connectors/engine/activities/plugin_create_payout_test.go create mode 100644 internal/connectors/engine/activities/plugin_create_transfer.go create mode 100644 internal/connectors/engine/activities/plugin_create_transfer_test.go create mode 100644 internal/connectors/engine/activities/storage_accounts_get.go create mode 100644 internal/connectors/engine/activities/storage_payment_initiations_adjusments_store.go create mode 100644 internal/connectors/engine/activities/storage_payment_initiations_get.go create mode 100644 internal/connectors/engine/activities/storage_payment_initiations_related_payments_store.go create mode 100644 internal/connectors/engine/workflow/create_payout.go create mode 100644 internal/connectors/engine/workflow/create_transfer.go create mode 100644 internal/connectors/grpc/proto/payment_initiation.pb.go create mode 100644 internal/connectors/grpc/proto/payment_initiation.proto create mode 100644 internal/connectors/plugins/public/mangopay/metadata.go create mode 100644 internal/connectors/plugins/public/mangopay/payouts.go create mode 100644 internal/connectors/plugins/public/mangopay/transfers.go create mode 100644 internal/models/payment_initiation_adjustment_id.go create mode 100644 internal/models/payment_initiation_type.go create mode 100644 internal/models/payments_initation_related_payments.go create mode 100644 internal/models/payments_initiation_adjusments_status.go create mode 100644 internal/models/payments_initiation_adjustments.go create mode 100644 internal/storage/payment_initiations.go create mode 100644 internal/storage/payment_initiations_test.go diff --git a/.gitignore b/.gitignore index 0f1636bb..bc16d5e4 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ coverage.out dist/ .env payments +tools/ \ No newline at end of file diff --git a/internal/api/backend/backend.go b/internal/api/backend/backend.go index c5c91566..72bbde94 100644 --- a/internal/api/backend/backend.go +++ b/internal/api/backend/backend.go @@ -42,6 +42,24 @@ type Backend interface { PaymentsList(ctx context.Context, query storage.ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) PaymentsGet(ctx context.Context, id models.PaymentID) (*models.Payment, error) + // Payment Initiations + PaymentInitiationsCreate(ctx context.Context, paymentInitiation models.PaymentInitiation, sendToPSP bool) error + PaymentInitiationsList(ctx context.Context, query storage.ListPaymentInitiationsQuery) (*bunpaginate.Cursor[models.PaymentInitiation], error) + PaymentInitiationsGet(ctx context.Context, id models.PaymentInitiationID) (*models.PaymentInitiation, error) + PaymentInitiationsApprove(ctx context.Context, id models.PaymentInitiationID) error + PaymentInitiationsReject(ctx context.Context, id models.PaymentInitiationID) error + PaymentInitiationsRetry(ctx context.Context, id models.PaymentInitiationID) error + PaymentInitiationsDelete(ctx context.Context, id models.PaymentInitiationID) error + + // Payment Initiation Adjustments + PaymentInitiationAdjustmentsList(ctx context.Context, id models.PaymentInitiationID, query storage.ListPaymentInitiationAdjustmentsQuery) (*bunpaginate.Cursor[models.PaymentInitiationAdjustment], error) + PaymentInitiationAdjustmentsListAll(ctx context.Context, id models.PaymentInitiationID) ([]models.PaymentInitiationAdjustment, error) + PaymentInitiationAdjustmentsGetLast(ctx context.Context, id models.PaymentInitiationID) (*models.PaymentInitiationAdjustment, error) + + // Payment Initiatiion Related Payments + PaymentInitiationRelatedPaymentsList(ctx context.Context, id models.PaymentInitiationID, query storage.ListPaymentInitiationRelatedPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) + PaymentInitiationRelatedPaymentListAll(ctx context.Context, id models.PaymentInitiationID) ([]models.Payment, error) + // Pools PoolsCreate(ctx context.Context, pool models.Pool) error PoolsGet(ctx context.Context, id uuid.UUID) (*models.Pool, error) diff --git a/internal/api/backend/backend_generated.go b/internal/api/backend/backend_generated.go index f2226d90..a1677f15 100644 --- a/internal/api/backend/backend_generated.go +++ b/internal/api/backend/backend_generated.go @@ -265,6 +265,181 @@ func (mr *MockBackendMockRecorder) ConnectorsUninstall(ctx, connectorID any) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ConnectorsUninstall", reflect.TypeOf((*MockBackend)(nil).ConnectorsUninstall), ctx, connectorID) } +// PaymentInitiationAdjustmentsGetLast mocks base method. +func (m *MockBackend) PaymentInitiationAdjustmentsGetLast(ctx context.Context, id models.PaymentInitiationID) (*models.PaymentInitiationAdjustment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationAdjustmentsGetLast", ctx, id) + ret0, _ := ret[0].(*models.PaymentInitiationAdjustment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentInitiationAdjustmentsGetLast indicates an expected call of PaymentInitiationAdjustmentsGetLast. +func (mr *MockBackendMockRecorder) PaymentInitiationAdjustmentsGetLast(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationAdjustmentsGetLast", reflect.TypeOf((*MockBackend)(nil).PaymentInitiationAdjustmentsGetLast), ctx, id) +} + +// PaymentInitiationAdjustmentsList mocks base method. +func (m *MockBackend) PaymentInitiationAdjustmentsList(ctx context.Context, id models.PaymentInitiationID, query storage.ListPaymentInitiationAdjustmentsQuery) (*bunpaginate.Cursor[models.PaymentInitiationAdjustment], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationAdjustmentsList", ctx, id, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.PaymentInitiationAdjustment]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentInitiationAdjustmentsList indicates an expected call of PaymentInitiationAdjustmentsList. +func (mr *MockBackendMockRecorder) PaymentInitiationAdjustmentsList(ctx, id, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationAdjustmentsList", reflect.TypeOf((*MockBackend)(nil).PaymentInitiationAdjustmentsList), ctx, id, query) +} + +// PaymentInitiationAdjustmentsListAll mocks base method. +func (m *MockBackend) PaymentInitiationAdjustmentsListAll(ctx context.Context, id models.PaymentInitiationID) ([]models.PaymentInitiationAdjustment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationAdjustmentsListAll", ctx, id) + ret0, _ := ret[0].([]models.PaymentInitiationAdjustment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentInitiationAdjustmentsListAll indicates an expected call of PaymentInitiationAdjustmentsListAll. +func (mr *MockBackendMockRecorder) PaymentInitiationAdjustmentsListAll(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationAdjustmentsListAll", reflect.TypeOf((*MockBackend)(nil).PaymentInitiationAdjustmentsListAll), ctx, id) +} + +// PaymentInitiationRelatedPaymentListAll mocks base method. +func (m *MockBackend) PaymentInitiationRelatedPaymentListAll(ctx context.Context, id models.PaymentInitiationID) ([]models.Payment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationRelatedPaymentListAll", ctx, id) + ret0, _ := ret[0].([]models.Payment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentInitiationRelatedPaymentListAll indicates an expected call of PaymentInitiationRelatedPaymentListAll. +func (mr *MockBackendMockRecorder) PaymentInitiationRelatedPaymentListAll(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationRelatedPaymentListAll", reflect.TypeOf((*MockBackend)(nil).PaymentInitiationRelatedPaymentListAll), ctx, id) +} + +// PaymentInitiationRelatedPaymentsList mocks base method. +func (m *MockBackend) PaymentInitiationRelatedPaymentsList(ctx context.Context, id models.PaymentInitiationID, query storage.ListPaymentInitiationRelatedPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationRelatedPaymentsList", ctx, id, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.Payment]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentInitiationRelatedPaymentsList indicates an expected call of PaymentInitiationRelatedPaymentsList. +func (mr *MockBackendMockRecorder) PaymentInitiationRelatedPaymentsList(ctx, id, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationRelatedPaymentsList", reflect.TypeOf((*MockBackend)(nil).PaymentInitiationRelatedPaymentsList), ctx, id, query) +} + +// PaymentInitiationsApprove mocks base method. +func (m *MockBackend) PaymentInitiationsApprove(ctx context.Context, id models.PaymentInitiationID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationsApprove", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentInitiationsApprove indicates an expected call of PaymentInitiationsApprove. +func (mr *MockBackendMockRecorder) PaymentInitiationsApprove(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsApprove", reflect.TypeOf((*MockBackend)(nil).PaymentInitiationsApprove), ctx, id) +} + +// PaymentInitiationsCreate mocks base method. +func (m *MockBackend) PaymentInitiationsCreate(ctx context.Context, paymentInitiation models.PaymentInitiation, sendToPSP bool) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationsCreate", ctx, paymentInitiation, sendToPSP) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentInitiationsCreate indicates an expected call of PaymentInitiationsCreate. +func (mr *MockBackendMockRecorder) PaymentInitiationsCreate(ctx, paymentInitiation, sendToPSP any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsCreate", reflect.TypeOf((*MockBackend)(nil).PaymentInitiationsCreate), ctx, paymentInitiation, sendToPSP) +} + +// PaymentInitiationsDelete mocks base method. +func (m *MockBackend) PaymentInitiationsDelete(ctx context.Context, id models.PaymentInitiationID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationsDelete", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentInitiationsDelete indicates an expected call of PaymentInitiationsDelete. +func (mr *MockBackendMockRecorder) PaymentInitiationsDelete(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsDelete", reflect.TypeOf((*MockBackend)(nil).PaymentInitiationsDelete), ctx, id) +} + +// PaymentInitiationsGet mocks base method. +func (m *MockBackend) PaymentInitiationsGet(ctx context.Context, id models.PaymentInitiationID) (*models.PaymentInitiation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationsGet", ctx, id) + ret0, _ := ret[0].(*models.PaymentInitiation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentInitiationsGet indicates an expected call of PaymentInitiationsGet. +func (mr *MockBackendMockRecorder) PaymentInitiationsGet(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsGet", reflect.TypeOf((*MockBackend)(nil).PaymentInitiationsGet), ctx, id) +} + +// PaymentInitiationsList mocks base method. +func (m *MockBackend) PaymentInitiationsList(ctx context.Context, query storage.ListPaymentInitiationsQuery) (*bunpaginate.Cursor[models.PaymentInitiation], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationsList", ctx, query) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.PaymentInitiation]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentInitiationsList indicates an expected call of PaymentInitiationsList. +func (mr *MockBackendMockRecorder) PaymentInitiationsList(ctx, query any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsList", reflect.TypeOf((*MockBackend)(nil).PaymentInitiationsList), ctx, query) +} + +// PaymentInitiationsReject mocks base method. +func (m *MockBackend) PaymentInitiationsReject(ctx context.Context, id models.PaymentInitiationID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationsReject", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentInitiationsReject indicates an expected call of PaymentInitiationsReject. +func (mr *MockBackendMockRecorder) PaymentInitiationsReject(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsReject", reflect.TypeOf((*MockBackend)(nil).PaymentInitiationsReject), ctx, id) +} + +// PaymentInitiationsRetry mocks base method. +func (m *MockBackend) PaymentInitiationsRetry(ctx context.Context, id models.PaymentInitiationID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationsRetry", ctx, id) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentInitiationsRetry indicates an expected call of PaymentInitiationsRetry. +func (mr *MockBackendMockRecorder) PaymentInitiationsRetry(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsRetry", reflect.TypeOf((*MockBackend)(nil).PaymentInitiationsRetry), ctx, id) +} + // PaymentsGet mocks base method. func (m *MockBackend) PaymentsGet(ctx context.Context, id models.PaymentID) (*models.Payment, error) { m.ctrl.T.Helper() diff --git a/internal/api/services/accounts_get.go b/internal/api/services/accounts_get.go index 836aa5c1..7e9579c1 100644 --- a/internal/api/services/accounts_get.go +++ b/internal/api/services/accounts_get.go @@ -7,5 +7,10 @@ import ( ) func (s *Service) AccountsGet(ctx context.Context, id models.AccountID) (*models.Account, error) { - return s.storage.AccountsGet(ctx, id) + account, err := s.storage.AccountsGet(ctx, id) + if err != nil { + return nil, newStorageError(err, "cannot get account") + } + + return account, nil } diff --git a/internal/api/services/accounts_list.go b/internal/api/services/accounts_list.go index fce64c25..fe8d0fbb 100644 --- a/internal/api/services/accounts_list.go +++ b/internal/api/services/accounts_list.go @@ -9,5 +9,10 @@ import ( ) func (s *Service) AccountsList(ctx context.Context, query storage.ListAccountsQuery) (*bunpaginate.Cursor[models.Account], error) { - return s.storage.AccountsList(ctx, query) + accounts, err := s.storage.AccountsList(ctx, query) + if err != nil { + return nil, newStorageError(err, "cannot list accounts") + } + + return accounts, nil } diff --git a/internal/api/services/balances_list.go b/internal/api/services/balances_list.go index 134d36ac..2316d7b6 100644 --- a/internal/api/services/balances_list.go +++ b/internal/api/services/balances_list.go @@ -9,5 +9,10 @@ import ( ) func (s *Service) BalancesList(ctx context.Context, query storage.ListBalancesQuery) (*bunpaginate.Cursor[models.Balance], error) { - return s.storage.BalancesList(ctx, query) + balances, err := s.storage.BalancesList(ctx, query) + if err != nil { + return nil, newStorageError(err, "cannot list balances") + } + + return balances, nil } diff --git a/internal/api/services/bank_accounts_create.go b/internal/api/services/bank_accounts_create.go index 5776c0e8..ae227da7 100644 --- a/internal/api/services/bank_accounts_create.go +++ b/internal/api/services/bank_accounts_create.go @@ -7,5 +7,5 @@ import ( ) func (s *Service) BankAccountsCreate(ctx context.Context, bankAccount models.BankAccount) error { - return s.storage.BankAccountsUpsert(ctx, bankAccount) + return newStorageError(s.storage.BankAccountsUpsert(ctx, bankAccount), "cannot create bank account") } diff --git a/internal/api/services/bank_accounts_forward_to_connector.go b/internal/api/services/bank_accounts_forward_to_connector.go index 7fbcb20a..01f3c1ae 100644 --- a/internal/api/services/bank_accounts_forward_to_connector.go +++ b/internal/api/services/bank_accounts_forward_to_connector.go @@ -8,5 +8,10 @@ import ( ) func (s *Service) BankAccountsForwardToConnector(ctx context.Context, bankAccountID uuid.UUID, connectorID models.ConnectorID) (*models.BankAccount, error) { - return s.engine.ForwardBankAccount(ctx, bankAccountID, connectorID) + ba, err := s.engine.ForwardBankAccount(ctx, bankAccountID, connectorID) + if err != nil { + return nil, handleEngineErrors(err) + } + + return ba, nil } diff --git a/internal/api/services/bank_accounts_get.go b/internal/api/services/bank_accounts_get.go index de20b760..629fd02c 100644 --- a/internal/api/services/bank_accounts_get.go +++ b/internal/api/services/bank_accounts_get.go @@ -8,5 +8,10 @@ import ( ) func (s *Service) BankAccountsGet(ctx context.Context, id uuid.UUID) (*models.BankAccount, error) { - return s.storage.BankAccountsGet(ctx, id, true) + ba, err := s.storage.BankAccountsGet(ctx, id, true) + if err != nil { + return nil, newStorageError(err, "cannot get bank account") + } + + return ba, nil } diff --git a/internal/api/services/bank_accounts_list.go b/internal/api/services/bank_accounts_list.go index c01b66bf..24824b4e 100644 --- a/internal/api/services/bank_accounts_list.go +++ b/internal/api/services/bank_accounts_list.go @@ -9,5 +9,10 @@ import ( ) func (s *Service) BankAccountsList(ctx context.Context, query storage.ListBankAccountsQuery) (*bunpaginate.Cursor[models.BankAccount], error) { - return s.storage.BankAccountsList(ctx, query) + bas, err := s.storage.BankAccountsList(ctx, query) + if err != nil { + return nil, newStorageError(err, "cannot list bank accounts") + } + + return bas, nil } diff --git a/internal/api/services/bank_accounts_update_metadata.go b/internal/api/services/bank_accounts_update_metadata.go index 7164ad31..fb8a13b0 100644 --- a/internal/api/services/bank_accounts_update_metadata.go +++ b/internal/api/services/bank_accounts_update_metadata.go @@ -7,5 +7,5 @@ import ( ) func (s *Service) BankAccountsUpdateMetadata(ctx context.Context, id uuid.UUID, metadata map[string]string) error { - return s.storage.BankAccountsUpdateMetadata(ctx, id, metadata) + return newStorageError(s.storage.BankAccountsUpdateMetadata(ctx, id, metadata), "cannot update bank account metadata") } diff --git a/internal/api/services/connectors_handle_webhooks.go b/internal/api/services/connectors_handle_webhooks.go index e71f02fd..5304e648 100644 --- a/internal/api/services/connectors_handle_webhooks.go +++ b/internal/api/services/connectors_handle_webhooks.go @@ -11,5 +11,5 @@ func (s *Service) ConnectorsHandleWebhooks( urlPath string, webhook models.Webhook, ) error { - return s.engine.HandleWebhook(ctx, urlPath, webhook) + return handleEngineErrors(s.engine.HandleWebhook(ctx, urlPath, webhook)) } diff --git a/internal/api/services/connectors_uninstall.go b/internal/api/services/connectors_uninstall.go index bbd212ce..4c546c5f 100644 --- a/internal/api/services/connectors_uninstall.go +++ b/internal/api/services/connectors_uninstall.go @@ -12,5 +12,5 @@ func (s *Service) ConnectorsUninstall(ctx context.Context, connectorID models.Co return newStorageError(err, "get connector") } - return s.engine.UninstallConnector(ctx, connectorID) + return handleEngineErrors(s.engine.UninstallConnector(ctx, connectorID)) } diff --git a/internal/api/services/payment_initiation_adjustments_get.go b/internal/api/services/payment_initiation_adjustments_get.go new file mode 100644 index 00000000..d8acee30 --- /dev/null +++ b/internal/api/services/payment_initiation_adjustments_get.go @@ -0,0 +1,28 @@ +package services + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + "github.com/pkg/errors" +) + +func (s *Service) PaymentInitiationAdjustmentsGetLast(ctx context.Context, id models.PaymentInitiationID) (*models.PaymentInitiationAdjustment, error) { + q := storage.NewListPaymentInitiationAdjustmentsQuery( + bunpaginate.NewPaginatedQueryOptions(storage.PaymentInitiationAdjustmentsQuery{}). + WithPageSize(1), + ) + + cursor, err := s.storage.PaymentInitiationAdjustmentsList(ctx, id, q) + if err != nil { + return nil, newStorageError(err, "cannot list payment initiation's adjustments") + } + + if len(cursor.Data) == 0 { + return nil, errors.New("payment initiation's adjustments not found") + } + + return &cursor.Data[0], nil +} diff --git a/internal/api/services/payment_initiation_adjustments_list.go b/internal/api/services/payment_initiation_adjustments_list.go new file mode 100644 index 00000000..5f274183 --- /dev/null +++ b/internal/api/services/payment_initiation_adjustments_list.go @@ -0,0 +1,44 @@ +package services + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" +) + +func (s *Service) PaymentInitiationAdjustmentsList(ctx context.Context, id models.PaymentInitiationID, query storage.ListPaymentInitiationAdjustmentsQuery) (*bunpaginate.Cursor[models.PaymentInitiationAdjustment], error) { + cursor, err := s.storage.PaymentInitiationAdjustmentsList(ctx, id, query) + return cursor, newStorageError(err, "failed to list payment initiation adjustments") +} + +func (s *Service) PaymentInitiationAdjustmentsListAll(ctx context.Context, id models.PaymentInitiationID) ([]models.PaymentInitiationAdjustment, error) { + q := storage.NewListPaymentInitiationAdjustmentsQuery( + bunpaginate.NewPaginatedQueryOptions(storage.PaymentInitiationAdjustmentsQuery{}). + WithPageSize(50), + ) + var next string + adjustments := []models.PaymentInitiationAdjustment{} + for { + if next != "" { + err := bunpaginate.UnmarshalCursor(next, &q) + if err != nil { + return nil, err + } + } + + cursor, err := s.storage.PaymentInitiationAdjustmentsList(ctx, id, q) + if err != nil { + return nil, newStorageError(err, "cannot list payment initiation's adjustments") + } + + adjustments = append(adjustments, cursor.Data...) + + if cursor.Next == "" { + break + } + } + + return adjustments, nil +} diff --git a/internal/api/services/payment_initiation_related_payments_list.go b/internal/api/services/payment_initiation_related_payments_list.go new file mode 100644 index 00000000..e52b1526 --- /dev/null +++ b/internal/api/services/payment_initiation_related_payments_list.go @@ -0,0 +1,44 @@ +package services + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" +) + +func (s *Service) PaymentInitiationRelatedPaymentsList(ctx context.Context, id models.PaymentInitiationID, query storage.ListPaymentInitiationRelatedPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) { + cursor, err := s.storage.PaymentInitiationRelatedPaymentsList(ctx, id, query) + return cursor, newStorageError(err, "failed to list payment initiation related payments") +} + +func (s *Service) PaymentInitiationRelatedPaymentListAll(ctx context.Context, id models.PaymentInitiationID) ([]models.Payment, error) { + q := storage.NewListPaymentInitiationRelatedPaymentsQuery( + bunpaginate.NewPaginatedQueryOptions(storage.PaymentInitiationRelatedPaymentsQuery{}). + WithPageSize(50), + ) + var next string + relatedPayment := []models.Payment{} + for { + if next != "" { + err := bunpaginate.UnmarshalCursor(next, &q) + if err != nil { + return nil, err + } + } + + cursor, err := s.storage.PaymentInitiationRelatedPaymentsList(ctx, id, q) + if err != nil { + return nil, newStorageError(err, "cannot list payment initiation's adjustments") + } + + relatedPayment = append(relatedPayment, cursor.Data...) + + if cursor.Next == "" { + break + } + } + + return relatedPayment, nil +} diff --git a/internal/api/services/payment_initiations_approve.go b/internal/api/services/payment_initiations_approve.go new file mode 100644 index 00000000..3f647943 --- /dev/null +++ b/internal/api/services/payment_initiations_approve.go @@ -0,0 +1,49 @@ +package services + +import ( + "context" + "fmt" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + "github.com/pkg/errors" +) + +func (s *Service) PaymentInitiationsApprove(ctx context.Context, id models.PaymentInitiationID) error { + cursor, err := s.storage.PaymentInitiationAdjustmentsList( + ctx, + id, + storage.NewListPaymentInitiationAdjustmentsQuery( + bunpaginate.NewPaginatedQueryOptions(storage.PaymentInitiationAdjustmentsQuery{}). + WithPageSize(1), + ), + ) + if err != nil { + return newStorageError(err, "cannot list payment initiation's adjustments") + } + + if len(cursor.Data) == 0 { + return errors.New("payment initiation's adjustments not found") + } + + lastAdjustment := cursor.Data[0] + + if lastAdjustment.Status != models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION { + return fmt.Errorf("cannot approve an already approved payment initiation: %w", ErrValidation) + } + + pi, err := s.storage.PaymentInitiationsGet(ctx, id) + if err != nil { + return newStorageError(err, "cannot get payment initiation") + } + + switch pi.Type { + case models.PAYMENT_INITIATION_TYPE_TRANSFER: + return handleEngineErrors(s.engine.CreateTransfer(ctx, pi.ID, 1)) + case models.PAYMENT_INITIATION_TYPE_PAYOUT: + return handleEngineErrors(s.engine.CreatePayout(ctx, pi.ID, 1)) + } + + return nil +} diff --git a/internal/api/services/payment_initiations_create.go b/internal/api/services/payment_initiations_create.go new file mode 100644 index 00000000..eddb1553 --- /dev/null +++ b/internal/api/services/payment_initiations_create.go @@ -0,0 +1,37 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" +) + +func (s *Service) PaymentInitiationsCreate(ctx context.Context, paymentInitiation models.PaymentInitiation, sendToPSP bool) error { + waitingForValidationAdjustment := models.PaymentInitiationAdjustment{ + ID: models.PaymentInitiationAdjustmentID{ + PaymentInitiationID: paymentInitiation.ID, + CreatedAt: paymentInitiation.CreatedAt, + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION, + }, + PaymentInitiationID: paymentInitiation.ID, + CreatedAt: paymentInitiation.CreatedAt, + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION, + } + + if !sendToPSP { + return newStorageError(s.storage.PaymentInitiationsUpsert(ctx, paymentInitiation, waitingForValidationAdjustment), "cannot create payment initiation") + } + + if err := s.storage.PaymentInitiationsUpsert(ctx, paymentInitiation, waitingForValidationAdjustment); err != nil { + return newStorageError(err, "cannot create payment initiation") + } + + switch paymentInitiation.Type { + case models.PAYMENT_INITIATION_TYPE_TRANSFER: + return handleEngineErrors(s.engine.CreateTransfer(ctx, paymentInitiation.ID, 1)) + case models.PAYMENT_INITIATION_TYPE_PAYOUT: + return handleEngineErrors(s.engine.CreatePayout(ctx, paymentInitiation.ID, 1)) + } + + return nil +} diff --git a/internal/api/services/payment_initiations_delete.go b/internal/api/services/payment_initiations_delete.go new file mode 100644 index 00000000..edda1758 --- /dev/null +++ b/internal/api/services/payment_initiations_delete.go @@ -0,0 +1,37 @@ +package services + +import ( + "context" + "fmt" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + "github.com/pkg/errors" +) + +func (s *Service) PaymentInitiationsDelete(ctx context.Context, id models.PaymentInitiationID) error { + cursor, err := s.storage.PaymentInitiationAdjustmentsList( + ctx, + id, + storage.NewListPaymentInitiationAdjustmentsQuery( + bunpaginate.NewPaginatedQueryOptions(storage.PaymentInitiationAdjustmentsQuery{}). + WithPageSize(1), + ), + ) + if err != nil { + return newStorageError(err, "cannot list payment initiation's adjustments") + } + + if len(cursor.Data) == 0 { + return errors.New("payment initiation's adjustments not found") + } + + lastAdjustment := cursor.Data[0] + + if lastAdjustment.Status != models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION { + return fmt.Errorf("cannot delete an already approved payment initiation: %w", ErrValidation) + } + + return newStorageError(s.storage.PaymentInitiationsDelete(ctx, id), "cannot delete payment initiation") +} diff --git a/internal/api/services/payment_initiations_get.go b/internal/api/services/payment_initiations_get.go new file mode 100644 index 00000000..c129e89e --- /dev/null +++ b/internal/api/services/payment_initiations_get.go @@ -0,0 +1,16 @@ +package services + +import ( + "context" + + "github.com/formancehq/payments/internal/models" +) + +func (s *Service) PaymentInitiationsGet(ctx context.Context, id models.PaymentInitiationID) (*models.PaymentInitiation, error) { + pi, err := s.storage.PaymentInitiationsGet(ctx, id) + if err != nil { + return nil, newStorageError(err, "cannot get payment initiation") + } + + return pi, nil +} diff --git a/internal/api/services/payment_initiations_list.go b/internal/api/services/payment_initiations_list.go new file mode 100644 index 00000000..aa0aaa81 --- /dev/null +++ b/internal/api/services/payment_initiations_list.go @@ -0,0 +1,18 @@ +package services + +import ( + "context" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" +) + +func (s *Service) PaymentInitiationsList(ctx context.Context, query storage.ListPaymentInitiationsQuery) (*bunpaginate.Cursor[models.PaymentInitiation], error) { + pis, err := s.storage.PaymentInitiationsList(ctx, query) + if err != nil { + return nil, newStorageError(err, "cannot list payment initiations") + } + + return pis, nil +} diff --git a/internal/api/services/payment_initiations_reject.go b/internal/api/services/payment_initiations_reject.go new file mode 100644 index 00000000..f25b97da --- /dev/null +++ b/internal/api/services/payment_initiations_reject.go @@ -0,0 +1,51 @@ +package services + +import ( + "context" + "fmt" + "time" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + "github.com/pkg/errors" +) + +func (s *Service) PaymentInitiationsReject(ctx context.Context, id models.PaymentInitiationID) error { + cursor, err := s.storage.PaymentInitiationAdjustmentsList( + ctx, + id, + storage.NewListPaymentInitiationAdjustmentsQuery( + bunpaginate.NewPaginatedQueryOptions(storage.PaymentInitiationAdjustmentsQuery{}). + WithPageSize(1), + ), + ) + if err != nil { + return newStorageError(err, "cannot list payment initiation's adjustments") + } + + if len(cursor.Data) == 0 { + return errors.New("payment initiation's adjustments not found") + } + + lastAdjustment := cursor.Data[0] + + if lastAdjustment.Status != models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION { + return fmt.Errorf("cannot reject an already approved payment initiation: %w", ErrValidation) + } + + now := time.Now().UTC() + return newStorageError(s.storage.PaymentInitiationAdjustmentsUpsert( + ctx, + models.PaymentInitiationAdjustment{ + ID: models.PaymentInitiationAdjustmentID{ + PaymentInitiationID: id, + CreatedAt: now, + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_REJECTED, + }, + PaymentInitiationID: id, + CreatedAt: now, + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_REJECTED, + }, + ), "cannot reject payment initiation") +} diff --git a/internal/api/services/payment_initiations_retry.go b/internal/api/services/payment_initiations_retry.go new file mode 100644 index 00000000..aeeb730e --- /dev/null +++ b/internal/api/services/payment_initiations_retry.go @@ -0,0 +1,96 @@ +package services + +import ( + "context" + "fmt" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + "github.com/pkg/errors" +) + +func (s *Service) PaymentInitiationsRetry(ctx context.Context, id models.PaymentInitiationID) error { + adjustments, err := s.getAllPaymentInitiationAdjustments(ctx, id) + if err != nil { + return err + } + + if len(adjustments) == 0 { + return errors.New("payment initiation's adjustments not found") + } + + lastAdjustment := adjustments[0] + + isReversed := false + switch lastAdjustment.Status { + case models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED: + case models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSE_FAILED: + isReversed = true + default: + return fmt.Errorf("cannot retry an already processed payment initiation: %w", ErrValidation) + } + + pi, err := s.storage.PaymentInitiationsGet(ctx, id) + if err != nil { + return newStorageError(err, "cannot get payment initiation") + } + + attempts := getAttemps(adjustments, isReversed) + + if isReversed { + // TODO(polo): implement reverse + } else { + switch pi.Type { + case models.PAYMENT_INITIATION_TYPE_TRANSFER: + return handleEngineErrors(s.engine.CreateTransfer(ctx, pi.ID, attempts+1)) + case models.PAYMENT_INITIATION_TYPE_PAYOUT: + return handleEngineErrors(s.engine.CreatePayout(ctx, pi.ID, attempts+1)) + } + } + + return nil +} + +func getAttemps(adjustments []models.PaymentInitiationAdjustment, isReversed bool) int { + attempts := 0 + for _, adjustment := range adjustments { + if isReversed && adjustment.Status == models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSE_FAILED { + attempts++ + } else if !isReversed && adjustment.Status == models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED { + attempts++ + } + } + + return attempts +} + +func (s *Service) getAllPaymentInitiationAdjustments(ctx context.Context, id models.PaymentInitiationID) ([]models.PaymentInitiationAdjustment, error) { + adjustments := []models.PaymentInitiationAdjustment{} + q := storage.NewListPaymentInitiationAdjustmentsQuery( + bunpaginate.NewPaginatedQueryOptions(storage.PaymentInitiationAdjustmentsQuery{}). + WithPageSize(50), + ) + var next string + for { + if next != "" { + err := bunpaginate.UnmarshalCursor(next, &q) + if err != nil { + return nil, err + } + } + + cursor, err := s.storage.PaymentInitiationAdjustmentsList(ctx, id, q) + if err != nil { + return nil, newStorageError(err, "cannot list payment initiation's adjustments") + } + + adjustments = append(adjustments, cursor.Data...) + + if cursor.Next == "" { + break + } + } + + return adjustments, nil +} diff --git a/internal/api/services/payments_get.go b/internal/api/services/payments_get.go index b2692a79..1c687bf6 100644 --- a/internal/api/services/payments_get.go +++ b/internal/api/services/payments_get.go @@ -7,5 +7,10 @@ import ( ) func (s *Service) PaymentsGet(ctx context.Context, id models.PaymentID) (*models.Payment, error) { - return s.storage.PaymentsGet(ctx, id) + p, err := s.storage.PaymentsGet(ctx, id) + if err != nil { + return nil, newStorageError(err, "cannot get payment") + } + + return p, nil } diff --git a/internal/api/services/payments_list.go b/internal/api/services/payments_list.go index 0b60cc8f..1cdf9dcb 100644 --- a/internal/api/services/payments_list.go +++ b/internal/api/services/payments_list.go @@ -9,5 +9,10 @@ import ( ) func (s *Service) PaymentsList(ctx context.Context, query storage.ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) { - return s.storage.PaymentsList(ctx, query) + ps, err := s.storage.PaymentsList(ctx, query) + if err != nil { + return nil, newStorageError(err, "cannot list payments") + } + + return ps, nil } diff --git a/internal/api/services/payments_update_metadata.go b/internal/api/services/payments_update_metadata.go index 89248800..dc9a3ed4 100644 --- a/internal/api/services/payments_update_metadata.go +++ b/internal/api/services/payments_update_metadata.go @@ -7,5 +7,5 @@ import ( ) func (s *Service) PaymentsUpdateMetadata(ctx context.Context, id models.PaymentID, metadata map[string]string) error { - return s.storage.PaymentsUpdateMetadata(ctx, id, metadata) + return newStorageError(s.storage.PaymentsUpdateMetadata(ctx, id, metadata), "cannot update payment metadata") } diff --git a/internal/api/services/pools_get.go b/internal/api/services/pools_get.go index fae8ac16..89563489 100644 --- a/internal/api/services/pools_get.go +++ b/internal/api/services/pools_get.go @@ -8,5 +8,10 @@ import ( ) func (s *Service) PoolsGet(ctx context.Context, id uuid.UUID) (*models.Pool, error) { - return s.storage.PoolsGet(ctx, id) + p, err := s.storage.PoolsGet(ctx, id) + if err != nil { + return nil, newStorageError(err, "cannot get pool") + } + + return p, nil } diff --git a/internal/api/services/pools_list.go b/internal/api/services/pools_list.go index ff85baa2..8b059103 100644 --- a/internal/api/services/pools_list.go +++ b/internal/api/services/pools_list.go @@ -9,5 +9,10 @@ import ( ) func (s *Service) PoolsList(ctx context.Context, query storage.ListPoolsQuery) (*bunpaginate.Cursor[models.Pool], error) { - return s.storage.PoolsList(ctx, query) + ps, err := s.storage.PoolsList(ctx, query) + if err != nil { + return nil, newStorageError(err, "cannot list pools") + } + + return ps, nil } diff --git a/internal/api/services/schedules_get.go b/internal/api/services/schedules_get.go index 016568cf..38ac4646 100644 --- a/internal/api/services/schedules_get.go +++ b/internal/api/services/schedules_get.go @@ -7,5 +7,10 @@ import ( ) func (s *Service) SchedulesGet(ctx context.Context, id string, connectorID models.ConnectorID) (*models.Schedule, error) { - return s.storage.SchedulesGet(ctx, id, connectorID) + schedule, err := s.storage.SchedulesGet(ctx, id, connectorID) + if err != nil { + return nil, newStorageError(err, "cannot get schedule") + } + + return schedule, nil } diff --git a/internal/api/v2/handler_transfer_initiations_create.go b/internal/api/v2/handler_transfer_initiations_create.go new file mode 100644 index 00000000..a1e0d6f6 --- /dev/null +++ b/internal/api/v2/handler_transfer_initiations_create.go @@ -0,0 +1,170 @@ +package v2 + +import ( + "encoding/json" + "errors" + "math/big" + "net/http" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" +) + +type createTransferInitiationRequest struct { + Reference string `json:"reference"` + ScheduledAt time.Time `json:"scheduledAt"` + Description string `json:"description"` + SourceAccountID string `json:"sourceAccountID"` + DestinationAccountID string `json:"destinationAccountID"` + ConnectorID string `json:"connectorID"` + Provider string `json:"provider"` + Type string `json:"type"` + Amount *big.Int `json:"amount"` + Asset string `json:"asset"` + Validated bool `json:"validated"` + Metadata map[string]string `json:"metadata"` +} + +func (r *createTransferInitiationRequest) Validate() error { + if r.Reference == "" { + return errors.New("reference is required") + } + + if r.SourceAccountID != "" { + _, err := models.AccountIDFromString(r.SourceAccountID) + if err != nil { + return err + } + } + + if r.DestinationAccountID != "" { + _, err := models.AccountIDFromString(r.DestinationAccountID) + if err != nil { + return err + } + } + + _, err := models.PaymentInitiationTypeFromString(r.Type) + if err != nil { + return err + } + + if r.Amount == nil { + return errors.New("amount is required") + } + + if r.Asset == "" { + return errors.New("asset is required") + } + + return nil +} + +func transferInitiationsCreate(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_transferInitiationsCreate") + defer span.End() + + payload := createTransferInitiationRequest{} + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + setSpanAttributesFromRequest(span, payload) + + if err := payload.Validate(); err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + connectorID, err := models.ConnectorIDFromString(payload.ConnectorID) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + pi := models.PaymentInitiation{ + ID: models.PaymentInitiationID{ + Reference: payload.Reference, + ConnectorID: connectorID, + }, + ConnectorID: connectorID, + Reference: payload.Reference, + CreatedAt: time.Now(), + ScheduledAt: payload.ScheduledAt, + Description: payload.Description, + Type: models.MustPaymentInitiationTypeFromString(payload.Type), + Amount: payload.Amount, + Asset: payload.Asset, + Metadata: payload.Metadata, + } + + if payload.SourceAccountID != "" { + pi.SourceAccountID = pointer.For(models.MustAccountIDFromString(payload.SourceAccountID)) + } + + if payload.DestinationAccountID != "" { + pi.DestinationAccountID = pointer.For(models.MustAccountIDFromString(payload.DestinationAccountID)) + } + + err = backend.PaymentInitiationsCreate(ctx, pi, payload.Validated) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + resp := translatePaymentInitiationToResponse(&pi) + lastAdjustment, err := backend.PaymentInitiationAdjustmentsGetLast(ctx, pi.ID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + if lastAdjustment != nil { + resp.Status = lastAdjustment.Status.String() + resp.Error = func() string { + if lastAdjustment.Error == nil { + return "" + } + return *lastAdjustment.Error + }() + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[transferInitiationResponse]{ + Data: &resp, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} + +func setSpanAttributesFromRequest(span trace.Span, transfer createTransferInitiationRequest) { + span.SetAttributes( + attribute.String("request.reference", transfer.Reference), + attribute.String("request.scheduledAt", transfer.ScheduledAt.String()), + attribute.String("request.description", transfer.Description), + attribute.String("request.sourceAccountID", transfer.SourceAccountID), + attribute.String("request.destinationAccountID", transfer.DestinationAccountID), + attribute.String("request.connectorID", transfer.ConnectorID), + attribute.String("request.provider", transfer.Provider), + attribute.String("request.type", transfer.Type), + attribute.String("request.amount", transfer.Amount.String()), + attribute.String("request.asset", transfer.Asset), + attribute.String("request.validated", transfer.Asset), + ) +} diff --git a/internal/api/v2/handler_transfer_initiations_delete.go b/internal/api/v2/handler_transfer_initiations_delete.go new file mode 100644 index 00000000..ffb66724 --- /dev/null +++ b/internal/api/v2/handler_transfer_initiations_delete.go @@ -0,0 +1,36 @@ +package v2 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "go.opentelemetry.io/otel/attribute" +) + +func transferInitiationsDelete(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_transferInitiationsDelete") + defer span.End() + + id, err := models.PaymentInitiationIDFromString(transferInitiationID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + span.SetAttributes(attribute.String("transfer.id", id.String())) + + err = backend.PaymentInitiationsDelete(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/internal/api/v2/handler_transfer_initiations_get.go b/internal/api/v2/handler_transfer_initiations_get.go new file mode 100644 index 00000000..2e2d9576 --- /dev/null +++ b/internal/api/v2/handler_transfer_initiations_get.go @@ -0,0 +1,166 @@ +package v2 + +import ( + "encoding/json" + "math/big" + "net/http" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "go.opentelemetry.io/otel/attribute" +) + +type transferInitiationResponse struct { + ID string `json:"id"` + Reference string `json:"reference"` + CreatedAt time.Time `json:"createdAt"` + ScheduledAt time.Time `json:"scheduledAt"` + Description string `json:"description"` + SourceAccountID string `json:"sourceAccountID"` + DestinationAccountID string `json:"destinationAccountID"` + ConnectorID string `json:"connectorID"` + Provider string `json:"provider"` + Type string `json:"type"` + Amount *big.Int `json:"amount"` + InitialAmount *big.Int `json:"initialAmount"` + Asset string `json:"asset"` + Status string `json:"status"` + Error string `json:"error"` + Metadata map[string]string `json:"metadata"` +} + +type transferInitiationPaymentsResponse struct { + PaymentID string `json:"paymentID"` + CreatedAt time.Time `json:"createdAt"` + Status string `json:"status"` +} + +type transferInitiationAdjustmentsResponse struct { + AdjustmentID string `json:"adjustmentID"` + CreatedAt time.Time `json:"createdAt"` + Status string `json:"status"` + Error string `json:"error"` + Metadata map[string]string `json:"metadata"` +} + +type readTransferInitiationResponse struct { + transferInitiationResponse + RelatedPayments []transferInitiationPaymentsResponse `json:"relatedPayments"` + RelatedAdjustments []transferInitiationAdjustmentsResponse `json:"relatedAdjustments"` +} + +func transferInitiationsGet(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_transferInitiationsGet") + defer span.End() + + id, err := models.PaymentInitiationIDFromString(transferInitiationID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + span.SetAttributes(attribute.String("transfer.id", id.String())) + + transferInitiation, err := backend.PaymentInitiationsGet(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + relatedPayments, err := backend.PaymentInitiationRelatedPaymentListAll(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + relatedAdjustments, err := backend.PaymentInitiationAdjustmentsListAll(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + t := translatePaymentInitiationToResponse(transferInitiation) + if len(relatedAdjustments) > 0 { + t.Status = relatedAdjustments[0].Status.String() + t.Error = func() string { + if relatedAdjustments[0].Error == nil { + return "" + } + return *relatedAdjustments[0].Error + }() + } + + resp := &readTransferInitiationResponse{ + transferInitiationResponse: t, + RelatedPayments: translateRelatedPayments(relatedPayments), + RelatedAdjustments: translateAdjustments(relatedAdjustments), + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[readTransferInitiationResponse]{ + Data: resp, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} + +func translateAdjustments(from []models.PaymentInitiationAdjustment) []transferInitiationAdjustmentsResponse { + to := make([]transferInitiationAdjustmentsResponse, len(from)) + for i, adjustment := range from { + to[i] = transferInitiationAdjustmentsResponse{ + AdjustmentID: adjustment.ID.String(), + CreatedAt: adjustment.CreatedAt, + Status: adjustment.Status.String(), + Error: func() string { + if adjustment.Error == nil { + return "" + } + return *adjustment.Error + }(), + Metadata: adjustment.Metadata, + } + } + return to +} + +func translateRelatedPayments(from []models.Payment) []transferInitiationPaymentsResponse { + to := make([]transferInitiationPaymentsResponse, len(from)) + for i, payment := range from { + to[i] = transferInitiationPaymentsResponse{ + PaymentID: payment.ID.String(), + CreatedAt: payment.CreatedAt, + Status: payment.Status.String(), + } + } + return to +} + +func translatePaymentInitiationToResponse(from *models.PaymentInitiation) transferInitiationResponse { + return transferInitiationResponse{ + ID: from.ID.String(), + Reference: from.Reference, + CreatedAt: from.CreatedAt, + ScheduledAt: from.ScheduledAt, + Description: from.Description, + SourceAccountID: from.SourceAccountID.String(), + DestinationAccountID: from.DestinationAccountID.String(), + ConnectorID: from.ConnectorID.String(), + Provider: from.ConnectorID.Provider, + Type: from.Type.String(), + Amount: from.Amount, + InitialAmount: from.Amount, + Asset: from.Asset, + Metadata: from.Metadata, + } +} diff --git a/internal/api/v2/handler_transfer_initiations_list.go b/internal/api/v2/handler_transfer_initiations_list.go new file mode 100644 index 00000000..9140b398 --- /dev/null +++ b/internal/api/v2/handler_transfer_initiations_list.go @@ -0,0 +1,77 @@ +package v2 + +import ( + "encoding/json" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func transferInitiationsList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_transferInitiationsList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListPaymentInitiationsQuery](r, func() (*storage.ListPaymentInitiationsQuery, error) { + options, err := getPagination(r, storage.PaymentInitiationQuery{}) + if err != nil { + return nil, err + } + return pointer.For(storage.NewListPaymentInitiationsQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + cursor, err := backend.PaymentInitiationsList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + data := make([]transferInitiationResponse, len(cursor.Data)) + for i := range cursor.Data { + data[i] = translatePaymentInitiationToResponse(&cursor.Data[i]) + + lastAdjustment, err := backend.PaymentInitiationAdjustmentsGetLast(ctx, cursor.Data[i].ID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + if lastAdjustment != nil { + data[i].Status = lastAdjustment.Status.String() + data[i].Error = func() string { + if lastAdjustment.Error == nil { + return "" + } + return *lastAdjustment.Error + }() + } + } + + err = json.NewEncoder(w).Encode(api.BaseResponse[transferInitiationResponse]{ + Cursor: &bunpaginate.Cursor[transferInitiationResponse]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: data, + }, + }) + if err != nil { + otel.RecordError(span, err) + api.InternalServerError(w, r, err) + return + } + } +} diff --git a/internal/api/v2/handler_transfer_initiations_retry.go b/internal/api/v2/handler_transfer_initiations_retry.go new file mode 100644 index 00000000..132bd24e --- /dev/null +++ b/internal/api/v2/handler_transfer_initiations_retry.go @@ -0,0 +1,36 @@ +package v2 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "go.opentelemetry.io/otel/attribute" +) + +func transferInitiationsRetry(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_transferInitiationsRetry") + defer span.End() + + id, err := models.PaymentInitiationIDFromString(transferInitiationID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + span.SetAttributes(attribute.String("transfer.id", id.String())) + + err = backend.PaymentInitiationsRetry(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/internal/api/v2/handler_transfer_initiations_reverse.go b/internal/api/v2/handler_transfer_initiations_reverse.go new file mode 100644 index 00000000..0c63d497 --- /dev/null +++ b/internal/api/v2/handler_transfer_initiations_reverse.go @@ -0,0 +1,13 @@ +package v2 + +import ( + "net/http" + + "github.com/formancehq/payments/internal/api/backend" +) + +func transferInitiationsReverse(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + + } +} diff --git a/internal/api/v2/handler_transfer_initiations_update_status.go b/internal/api/v2/handler_transfer_initiations_update_status.go new file mode 100644 index 00000000..0c154bdc --- /dev/null +++ b/internal/api/v2/handler_transfer_initiations_update_status.go @@ -0,0 +1,79 @@ +package v2 + +import ( + "encoding/json" + "errors" + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "go.opentelemetry.io/otel/attribute" +) + +type deprecatedStatus string + +const ( + VALIDATED deprecatedStatus = "VALIDATED" + REJECTED deprecatedStatus = "REJECTED" +) + +type updateTransferInitiationStatusRequest struct { + Status string `json:"status"` +} + +func (r updateTransferInitiationStatusRequest) Validate() error { + if r.Status != string(VALIDATED) && r.Status != string(REJECTED) { + return errors.New("invalid status") + } + + return nil +} + +func transferInitiationsUpdateStatus(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v2_transferInitiationsUpdateStatus") + defer span.End() + + id, err := models.PaymentInitiationIDFromString(transferInitiationID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + span.SetAttributes(attribute.String("transfer.id", id.String())) + + payload := updateTransferInitiationStatusRequest{} + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + span.SetAttributes(attribute.String("request.status", payload.Status)) + + if err := payload.Validate(); err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + switch deprecatedStatus(payload.Status) { + case VALIDATED: + err = backend.PaymentInitiationsApprove(ctx, id) + case REJECTED: + err = backend.PaymentInitiationsReject(ctx, id) + default: + // Not possible since we already validated the status in the request + } + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/internal/api/v2/router.go b/internal/api/v2/router.go index 431deeab..cf0eadcf 100644 --- a/internal/api/v2/router.go +++ b/internal/api/v2/router.go @@ -85,6 +85,22 @@ func newRouter(backend backend.Backend, a auth.Authenticator, debug bool) *chi.M connectorsRouter(backend, r) }) }) + + // Transfer Initiations + r.Route("/transfer-initiations", func(r chi.Router) { + r.Post("/", transferInitiationsCreate(backend)) + r.Get("/", transferInitiationsList(backend)) + + r.Route("/{transferInitiationID}", func(r chi.Router) { + r.Get("/", transferInitiationsGet(backend)) + r.Delete("/", transferInitiationsDelete(backend)) + + r.Post("/status", transferInitiationsUpdateStatus(backend)) + r.Post("/retry", transferInitiationsRetry(backend)) + // TODO(polo): add reverse + // r.Post("/reverse", transferInitiationsReverse(backend)) + }) + }) }) }) @@ -129,3 +145,7 @@ func bankAccountID(r *http.Request) string { func taskID(r *http.Request) string { return chi.URLParam(r, "taskID") } + +func transferInitiationID(r *http.Request) string { + return chi.URLParam(r, "transferInitiationID") +} diff --git a/internal/api/v3/handler_payment_initiation_adjustments_list.go b/internal/api/v3/handler_payment_initiation_adjustments_list.go new file mode 100644 index 00000000..b800c79e --- /dev/null +++ b/internal/api/v3/handler_payment_initiation_adjustments_list.go @@ -0,0 +1,49 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func paymentInitiationAdjustmentsList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentInitiationAdjustmentsList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListPaymentInitiationAdjustmentsQuery](r, func() (*storage.ListPaymentInitiationAdjustmentsQuery, error) { + options, err := getPagination(r, storage.PaymentInitiationAdjustmentsQuery{}) + if err != nil { + return nil, err + } + return pointer.For(storage.NewListPaymentInitiationAdjustmentsQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + id, err := models.PaymentInitiationIDFromString(paymentInitiationID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + cursor, err := backend.PaymentInitiationAdjustmentsList(ctx, id, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.RenderCursor(w, *cursor) + } +} diff --git a/internal/api/v3/handler_payment_initiation_payments_list.go b/internal/api/v3/handler_payment_initiation_payments_list.go new file mode 100644 index 00000000..f1a2c717 --- /dev/null +++ b/internal/api/v3/handler_payment_initiation_payments_list.go @@ -0,0 +1,49 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func paymentInitiationPaymentsList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentInitiationPaymentsList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListPaymentInitiationRelatedPaymentsQuery](r, func() (*storage.ListPaymentInitiationRelatedPaymentsQuery, error) { + options, err := getPagination(r, storage.PaymentInitiationRelatedPaymentsQuery{}) + if err != nil { + return nil, err + } + return pointer.For(storage.NewListPaymentInitiationRelatedPaymentsQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + id, err := models.PaymentInitiationIDFromString(paymentInitiationID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + cursor, err := backend.PaymentInitiationRelatedPaymentsList(ctx, id, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.RenderCursor(w, *cursor) + } +} diff --git a/internal/api/v3/handler_payment_initiations_approve.go b/internal/api/v3/handler_payment_initiations_approve.go new file mode 100644 index 00000000..bc0c5f85 --- /dev/null +++ b/internal/api/v3/handler_payment_initiations_approve.go @@ -0,0 +1,33 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func paymentInitiationsApprove(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentInitiationsApprove") + defer span.End() + + id, err := models.PaymentInitiationIDFromString(paymentInitiationID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + err = backend.PaymentInitiationsApprove(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/internal/api/v3/handler_payment_initiations_create.go b/internal/api/v3/handler_payment_initiations_create.go new file mode 100644 index 00000000..f02994fe --- /dev/null +++ b/internal/api/v3/handler_payment_initiations_create.go @@ -0,0 +1,127 @@ +package v3 + +import ( + "encoding/json" + "errors" + "math/big" + "net/http" + "time" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +type paymentInitiationsCreateRequest struct { + Reference string `json:"reference"` + ScheduledAt time.Time `json:"scheduledAt"` + ConnectorID string `json:"connectorID"` + Description string `json:"description"` + Type string `json:"type"` + Amount *big.Int `json:"amount"` + Asset string `json:"asset"` + + SourceAccountID *string `json:"sourceAccountID"` + DestinationAccountID *string `json:"destinationAccountID"` + + Metadata map[string]string `json:"metadata"` +} + +func (r *paymentInitiationsCreateRequest) Validate() error { + if r.Reference == "" { + return errors.New("reference is required") + } + + if r.SourceAccountID != nil { + _, err := models.AccountIDFromString(*r.SourceAccountID) + if err != nil { + return err + } + } + + if r.DestinationAccountID != nil { + _, err := models.AccountIDFromString(*r.DestinationAccountID) + if err != nil { + return err + } + } + + _, err := models.PaymentInitiationTypeFromString(r.Type) + if err != nil { + return err + } + + if r.Amount == nil { + return errors.New("amount is required") + } + + if r.Asset == "" { + return errors.New("asset is required") + } + + return nil +} + +func paymentInitiationsCreate(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentInitiationsCreate") + defer span.End() + + payload := paymentInitiationsCreateRequest{} + if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrMissingOrInvalidBody, err) + return + } + + if err := payload.Validate(); err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + connectorID, err := models.ConnectorIDFromString(payload.ConnectorID) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + noValidation := r.URL.Query().Get("noValidation") == "true" + + pi := models.PaymentInitiation{ + ID: models.PaymentInitiationID{ + Reference: payload.Reference, + ConnectorID: connectorID, + }, + ConnectorID: connectorID, + Reference: payload.Reference, + CreatedAt: time.Now(), + ScheduledAt: payload.ScheduledAt, + Description: payload.Description, + Type: models.MustPaymentInitiationTypeFromString(payload.Type), + Amount: payload.Amount, + Asset: payload.Asset, + Metadata: payload.Metadata, + } + + if payload.SourceAccountID != nil { + pi.SourceAccountID = pointer.For(models.MustAccountIDFromString(*payload.SourceAccountID)) + } + + if payload.DestinationAccountID != nil { + pi.DestinationAccountID = pointer.For(models.MustAccountIDFromString(*payload.DestinationAccountID)) + } + + err = backend.PaymentInitiationsCreate(ctx, pi, noValidation) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.Created(w, pi) + } +} diff --git a/internal/api/v3/handler_payment_initiations_delete.go b/internal/api/v3/handler_payment_initiations_delete.go new file mode 100644 index 00000000..ebe633b6 --- /dev/null +++ b/internal/api/v3/handler_payment_initiations_delete.go @@ -0,0 +1,33 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func paymentInitiationsDelete(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentInitiationsDelete") + defer span.End() + + id, err := models.PaymentInitiationIDFromString(paymentInitiationID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + err = backend.PaymentInitiationsDelete(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/internal/api/v3/handler_payment_initiations_get.go b/internal/api/v3/handler_payment_initiations_get.go new file mode 100644 index 00000000..364995b5 --- /dev/null +++ b/internal/api/v3/handler_payment_initiations_get.go @@ -0,0 +1,46 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func paymentInitiationsGet(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentInitiationsGet") + defer span.End() + + id, err := models.PaymentInitiationIDFromString(paymentInitiationID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + paymentInitiation, err := backend.PaymentInitiationsGet(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + lastAdjustment, err := backend.PaymentInitiationAdjustmentsGetLast(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + res := models.PaymentInitiationExpanded{ + PaymentInitiation: *paymentInitiation, + Status: lastAdjustment.Status.String(), + Error: lastAdjustment.Error, + } + + api.Ok(w, res) + } +} diff --git a/internal/api/v3/handler_payment_initiations_list.go b/internal/api/v3/handler_payment_initiations_list.go new file mode 100644 index 00000000..0c74e07b --- /dev/null +++ b/internal/api/v3/handler_payment_initiations_list.go @@ -0,0 +1,64 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" + "github.com/formancehq/payments/internal/storage" +) + +func paymentInitiationsList(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentInitiationsList") + defer span.End() + + query, err := bunpaginate.Extract[storage.ListPaymentInitiationsQuery](r, func() (*storage.ListPaymentInitiationsQuery, error) { + options, err := getPagination(r, storage.PaymentInitiationQuery{}) + if err != nil { + return nil, err + } + return pointer.For(storage.NewListPaymentInitiationsQuery(*options)), nil + }) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrValidation, err) + return + } + + cursor, err := backend.PaymentInitiationsList(ctx, *query) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + pis := make([]models.PaymentInitiationExpanded, 0, len(cursor.Data)) + for _, pi := range cursor.Data { + lastAdjustment, err := backend.PaymentInitiationAdjustmentsGetLast(ctx, pi.ID) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + pis = append(pis, models.PaymentInitiationExpanded{ + PaymentInitiation: pi, + Status: lastAdjustment.Status.String(), + Error: lastAdjustment.Error, + }) + } + + api.RenderCursor(w, bunpaginate.Cursor[models.PaymentInitiationExpanded]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: pis, + }) + } +} diff --git a/internal/api/v3/handler_payment_initiations_reject.go b/internal/api/v3/handler_payment_initiations_reject.go new file mode 100644 index 00000000..8c52fd11 --- /dev/null +++ b/internal/api/v3/handler_payment_initiations_reject.go @@ -0,0 +1,33 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func paymentInitiationsReject(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentInitiationsReject") + defer span.End() + + id, err := models.PaymentInitiationIDFromString(paymentInitiationID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + err = backend.PaymentInitiationsReject(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/internal/api/v3/handler_payment_initiations_retry.go b/internal/api/v3/handler_payment_initiations_retry.go new file mode 100644 index 00000000..7537d89f --- /dev/null +++ b/internal/api/v3/handler_payment_initiations_retry.go @@ -0,0 +1,33 @@ +package v3 + +import ( + "net/http" + + "github.com/formancehq/go-libs/api" + "github.com/formancehq/payments/internal/api/backend" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/otel" +) + +func paymentInitiationsRetry(backend backend.Backend) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ctx, span := otel.Tracer().Start(r.Context(), "v3_paymentInitiationsRetry") + defer span.End() + + id, err := models.PaymentInitiationIDFromString(paymentInitiationID(r)) + if err != nil { + otel.RecordError(span, err) + api.BadRequest(w, ErrInvalidID, err) + return + } + + err = backend.PaymentInitiationsRetry(ctx, id) + if err != nil { + otel.RecordError(span, err) + handleServiceErrors(w, r, err) + return + } + + api.NoContent(w) + } +} diff --git a/internal/api/v3/router.go b/internal/api/v3/router.go index 3214a428..55913efa 100644 --- a/internal/api/v3/router.go +++ b/internal/api/v3/router.go @@ -22,7 +22,8 @@ func newRouter(backend backend.Backend, a auth.Authenticator, debug bool) *chi.M // Authenticated routes r.Group(func(r chi.Router) { - r.Use(auth.Middleware(a)) + // r.Use(auth.Middleware(a)) + _ = a // Accounts r.Route("/accounts", func(r chi.Router) { @@ -94,6 +95,26 @@ func newRouter(backend backend.Backend, a auth.Authenticator, debug bool) *chi.M // TODO(polo): add update config handler }) }) + + // Payment Initiations + r.Route("/payment-initiations", func(r chi.Router) { + r.Post("/", paymentInitiationsCreate(backend)) + r.Get("/", paymentInitiationsList(backend)) + + r.Route("/{paymentInitiationID}", func(r chi.Router) { + r.Delete("/", paymentInitiationsDelete(backend)) + r.Get("/", paymentInitiationsGet(backend)) + r.Post("/retry", paymentInitiationsRetry(backend)) + r.Post("/approve", paymentInitiationsApprove(backend)) + r.Post("/reject", paymentInitiationsReject(backend)) + // TODO(polo): add reverse + // r.Post("/reverse", paymentInitiationsReverse(backend)) + + r.Get("/adjustments", paymentInitiationAdjustmentsList(backend)) + r.Get("/payments", paymentInitiationPaymentsList(backend)) + }) + + }) }) }) @@ -127,3 +148,7 @@ func bankAccountID(r *http.Request) string { func scheduleID(r *http.Request) string { return chi.URLParam(r, "scheduleID") } + +func paymentInitiationID(r *http.Request) string { + return chi.URLParam(r, "paymentInitiationID") +} diff --git a/internal/connectors/engine/activities/activity.go b/internal/connectors/engine/activities/activity.go index 7a1ead27..dcd27b0f 100644 --- a/internal/connectors/engine/activities/activity.go +++ b/internal/connectors/engine/activities/activity.go @@ -52,6 +52,14 @@ func (a Activities) DefinitionSet() temporalworker.DefinitionSet { Name: "PluginCreateBankAccount", Func: a.PluginCreateBankAccount, }). + Append(temporalworker.Definition{ + Name: "PluginCreateTransfert", + Func: a.PluginCreateTransfer, + }). + Append(temporalworker.Definition{ + Name: "PluginCreatePayout", + Func: a.PluginCreatePayout, + }). Append(temporalworker.Definition{ Name: "PluginCreateWebhooks", Func: a.PluginCreateWebhooks, @@ -64,6 +72,10 @@ func (a Activities) DefinitionSet() temporalworker.DefinitionSet { Name: "StorageAccountsStore", Func: a.StorageAccountsStore, }). + Append(temporalworker.Definition{ + Name: "StorageAccountsGet", + Func: a.StorageAccountsGet, + }). Append(temporalworker.Definition{ Name: "StorageAccountsDelete", Func: a.StorageAccountsDelete, @@ -164,6 +176,18 @@ func (a Activities) DefinitionSet() temporalworker.DefinitionSet { Name: "StorageWebhooksDelete", Func: a.StorageWebhooksDelete, }). + Append(temporalworker.Definition{ + Name: "StoragePaymentInitiationGet", + Func: a.StoragePaymentInitiationsGet, + }). + Append(temporalworker.Definition{ + Name: "StoragePaymentInitiationsRelatedPaymentsStore", + Func: a.StoragePaymentInitiationsRelatedPaymentsStore, + }). + Append(temporalworker.Definition{ + Name: "PaymentInitiationsAdjustmentsStore", + Func: a.PaymentInitiationsAdjustmentsStore, + }). Append(temporalworker.Definition{ Name: "EventsSendAccount", Func: a.EventsSendAccount, diff --git a/internal/connectors/engine/activities/plugin_create_payout.go b/internal/connectors/engine/activities/plugin_create_payout.go new file mode 100644 index 00000000..14050374 --- /dev/null +++ b/internal/connectors/engine/activities/plugin_create_payout.go @@ -0,0 +1,39 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type CreatePayoutRequest struct { + ConnectorID models.ConnectorID + Req models.CreatePayoutRequest +} + +func (a Activities) PluginCreatePayout(ctx context.Context, request CreatePayoutRequest) (*models.CreatePayoutResponse, error) { + plugin, err := a.plugins.Get(request.ConnectorID) + if err != nil { + return nil, temporalError(err) + } + + resp, err := plugin.CreatePayout(ctx, request.Req) + if err != nil { + return nil, temporalError(err) + } + return &resp, nil +} + +var PluginCreatePayoutActivity = Activities{}.PluginCreatePayout + +func PluginCreatePayout(ctx workflow.Context, connectorID models.ConnectorID, request models.CreatePayoutRequest) (*models.CreatePayoutResponse, error) { + ret := models.CreatePayoutResponse{} + if err := executeActivity(ctx, PluginCreatePayoutActivity, &ret, CreatePayoutRequest{ + ConnectorID: connectorID, + Req: request, + }); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/internal/connectors/engine/activities/plugin_create_payout_test.go b/internal/connectors/engine/activities/plugin_create_payout_test.go new file mode 100644 index 00000000..011eb454 --- /dev/null +++ b/internal/connectors/engine/activities/plugin_create_payout_test.go @@ -0,0 +1,88 @@ +package activities_test + +import ( + "fmt" + "net/http" + + "github.com/formancehq/go-libs/errorsutils" + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/connectors/engine/plugins" + "github.com/formancehq/payments/internal/connectors/httpwrapper" + "github.com/formancehq/payments/internal/events" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.temporal.io/sdk/temporal" + gomock "go.uber.org/mock/gomock" +) + +var _ = Describe("Plugin Create Payout", func() { + var ( + act activities.Activities + p *plugins.MockPlugins + s *storage.MockStorage + evts *events.Events + sampleResponse models.CreatePayoutResponse + ) + + BeforeEach(func() { + evts = &events.Events{} + sampleResponse = models.CreatePayoutResponse{ + Payment: models.PSPPayment{Reference: "ref"}, + } + }) + + Context("plugin create payout", func() { + var ( + plugin *models.MockPlugin + req activities.CreatePayoutRequest + ) + + BeforeEach(func() { + ctrl := gomock.NewController(GinkgoT()) + p = plugins.NewMockPlugins(ctrl) + s = storage.NewMockStorage(ctrl) + plugin = models.NewMockPlugin(ctrl) + act = activities.New(s, evts, p) + req = activities.CreatePayoutRequest{ + ConnectorID: models.ConnectorID{ + Provider: "some_provider", + }, + } + }) + + It("calls underlying plugin", func(ctx SpecContext) { + p.EXPECT().Get(req.ConnectorID).Return(plugin, nil) + plugin.EXPECT().CreatePayout(ctx, req.Req).Return(sampleResponse, nil) + res, err := act.PluginCreatePayout(ctx, req) + Expect(err).To(BeNil()) + Expect(res.Payment.Reference).To(Equal(sampleResponse.Payment.Reference)) + }) + + It("returns a retryable temporal error", func(ctx SpecContext) { + p.EXPECT().Get(req.ConnectorID).Return(plugin, nil) + plugin.EXPECT().CreatePayout(ctx, req.Req).Return(sampleResponse, fmt.Errorf("some string")) + _, err := act.PluginCreatePayout(ctx, req) + Expect(err).ToNot(BeNil()) + temporalErr, ok := err.(*temporal.ApplicationError) + Expect(ok).To(BeTrue()) + Expect(temporalErr.NonRetryable()).To(BeFalse()) + Expect(temporalErr.Type()).To(Equal(req.ConnectorID.Provider)) + }) + + It("returns a non-retryable temporal error", func(ctx SpecContext) { + wrappedErr := fmt.Errorf("some string: %w", httpwrapper.ErrStatusCodeClientError) + newErr := errorsutils.NewErrorWithExitCode(wrappedErr, http.StatusTeapot) + + p.EXPECT().Get(req.ConnectorID).Return(plugin, nil) + plugin.EXPECT().CreatePayout(ctx, req.Req).Return(sampleResponse, newErr) + _, err := act.PluginCreatePayout(ctx, req) + Expect(err).ToNot(BeNil()) + temporalErr, ok := err.(*temporal.ApplicationError) + Expect(ok).To(BeTrue()) + Expect(temporalErr.NonRetryable()).To(BeTrue()) + Expect(temporalErr.Type()).To(Equal(req.ConnectorID.Provider)) + }) + }) +}) diff --git a/internal/connectors/engine/activities/plugin_create_transfer.go b/internal/connectors/engine/activities/plugin_create_transfer.go new file mode 100644 index 00000000..2503f766 --- /dev/null +++ b/internal/connectors/engine/activities/plugin_create_transfer.go @@ -0,0 +1,39 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type CreateTransferRequest struct { + ConnectorID models.ConnectorID + Req models.CreateTransferRequest +} + +func (a Activities) PluginCreateTransfer(ctx context.Context, request CreateTransferRequest) (*models.CreateTransferResponse, error) { + plugin, err := a.plugins.Get(request.ConnectorID) + if err != nil { + return nil, temporalError(err) + } + + resp, err := plugin.CreateTransfer(ctx, request.Req) + if err != nil { + return nil, temporalError(err) + } + return &resp, nil +} + +var PluginCreateTransferActivity = Activities{}.PluginCreateTransfer + +func PluginCreateTransfer(ctx workflow.Context, connectorID models.ConnectorID, request models.CreateTransferRequest) (*models.CreateTransferResponse, error) { + ret := models.CreateTransferResponse{} + if err := executeActivity(ctx, PluginCreateTransferActivity, &ret, CreateTransferRequest{ + ConnectorID: connectorID, + Req: request, + }); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/internal/connectors/engine/activities/plugin_create_transfer_test.go b/internal/connectors/engine/activities/plugin_create_transfer_test.go new file mode 100644 index 00000000..7e2ec2dc --- /dev/null +++ b/internal/connectors/engine/activities/plugin_create_transfer_test.go @@ -0,0 +1,88 @@ +package activities_test + +import ( + "fmt" + "net/http" + + "github.com/formancehq/go-libs/errorsutils" + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/connectors/engine/plugins" + "github.com/formancehq/payments/internal/connectors/httpwrapper" + "github.com/formancehq/payments/internal/events" + "github.com/formancehq/payments/internal/models" + "github.com/formancehq/payments/internal/storage" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go.temporal.io/sdk/temporal" + gomock "go.uber.org/mock/gomock" +) + +var _ = Describe("Plugin Create Transfer", func() { + var ( + act activities.Activities + p *plugins.MockPlugins + s *storage.MockStorage + evts *events.Events + sampleResponse models.CreateTransferResponse + ) + + BeforeEach(func() { + evts = &events.Events{} + sampleResponse = models.CreateTransferResponse{ + Payment: models.PSPPayment{Reference: "ref"}, + } + }) + + Context("plugin create transfer", func() { + var ( + plugin *models.MockPlugin + req activities.CreateTransferRequest + ) + + BeforeEach(func() { + ctrl := gomock.NewController(GinkgoT()) + p = plugins.NewMockPlugins(ctrl) + s = storage.NewMockStorage(ctrl) + plugin = models.NewMockPlugin(ctrl) + act = activities.New(s, evts, p) + req = activities.CreateTransferRequest{ + ConnectorID: models.ConnectorID{ + Provider: "some_provider", + }, + } + }) + + It("calls underlying plugin", func(ctx SpecContext) { + p.EXPECT().Get(req.ConnectorID).Return(plugin, nil) + plugin.EXPECT().CreateTransfer(ctx, req.Req).Return(sampleResponse, nil) + res, err := act.PluginCreateTransfer(ctx, req) + Expect(err).To(BeNil()) + Expect(res.Payment.Reference).To(Equal(sampleResponse.Payment.Reference)) + }) + + It("returns a retryable temporal error", func(ctx SpecContext) { + p.EXPECT().Get(req.ConnectorID).Return(plugin, nil) + plugin.EXPECT().CreateTransfer(ctx, req.Req).Return(sampleResponse, fmt.Errorf("some string")) + _, err := act.PluginCreateTransfer(ctx, req) + Expect(err).ToNot(BeNil()) + temporalErr, ok := err.(*temporal.ApplicationError) + Expect(ok).To(BeTrue()) + Expect(temporalErr.NonRetryable()).To(BeFalse()) + Expect(temporalErr.Type()).To(Equal(req.ConnectorID.Provider)) + }) + + It("returns a non-retryable temporal error", func(ctx SpecContext) { + wrappedErr := fmt.Errorf("some string: %w", httpwrapper.ErrStatusCodeClientError) + newErr := errorsutils.NewErrorWithExitCode(wrappedErr, http.StatusTeapot) + + p.EXPECT().Get(req.ConnectorID).Return(plugin, nil) + plugin.EXPECT().CreateTransfer(ctx, req.Req).Return(sampleResponse, newErr) + _, err := act.PluginCreateTransfer(ctx, req) + Expect(err).ToNot(BeNil()) + temporalErr, ok := err.(*temporal.ApplicationError) + Expect(ok).To(BeTrue()) + Expect(temporalErr.NonRetryable()).To(BeTrue()) + Expect(temporalErr.Type()).To(Equal(req.ConnectorID.Provider)) + }) + }) +}) diff --git a/internal/connectors/engine/activities/storage_accounts_get.go b/internal/connectors/engine/activities/storage_accounts_get.go new file mode 100644 index 00000000..b342cef7 --- /dev/null +++ b/internal/connectors/engine/activities/storage_accounts_get.go @@ -0,0 +1,22 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StorageAccountsGet(ctx context.Context, id models.AccountID) (*models.Account, error) { + return a.storage.AccountsGet(ctx, id) +} + +var StorageAccountsGetActivity = Activities{}.StorageAccountsGet + +func StorageAccountsGet(ctx workflow.Context, id models.AccountID) (*models.Account, error) { + ret := models.Account{} + if err := executeActivity(ctx, StorageAccountsGetActivity, &ret, id); err != nil { + return nil, err + } + return &ret, nil +} diff --git a/internal/connectors/engine/activities/storage_payment_initiations_adjusments_store.go b/internal/connectors/engine/activities/storage_payment_initiations_adjusments_store.go new file mode 100644 index 00000000..5a77fd6a --- /dev/null +++ b/internal/connectors/engine/activities/storage_payment_initiations_adjusments_store.go @@ -0,0 +1,18 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) PaymentInitiationsAdjustmentsStore(ctx context.Context, adj models.PaymentInitiationAdjustment) error { + return a.storage.PaymentInitiationAdjustmentsUpsert(ctx, adj) +} + +var PaymentInitiationsAdjustmentsStoreActivity = Activities{}.PaymentInitiationsAdjustmentsStore + +func PaymentInitiationsAdjustmentsStore(ctx workflow.Context, adj models.PaymentInitiationAdjustment) error { + return executeActivity(ctx, PaymentInitiationsAdjustmentsStoreActivity, nil, adj) +} diff --git a/internal/connectors/engine/activities/storage_payment_initiations_get.go b/internal/connectors/engine/activities/storage_payment_initiations_get.go new file mode 100644 index 00000000..9ba39481 --- /dev/null +++ b/internal/connectors/engine/activities/storage_payment_initiations_get.go @@ -0,0 +1,20 @@ +package activities + +import ( + "context" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +func (a Activities) StoragePaymentInitiationsGet(ctx context.Context, id models.PaymentInitiationID) (*models.PaymentInitiation, error) { + return a.storage.PaymentInitiationsGet(ctx, id) +} + +var StoragePaymentInitiationsGetActivity = Activities{}.StoragePaymentInitiationsGet + +func StoragePaymentInitiationsGet(ctx workflow.Context, id models.PaymentInitiationID) (*models.PaymentInitiation, error) { + var result models.PaymentInitiation + err := executeActivity(ctx, StoragePaymentInitiationsGetActivity, &result, id) + return &result, err +} diff --git a/internal/connectors/engine/activities/storage_payment_initiations_related_payments_store.go b/internal/connectors/engine/activities/storage_payment_initiations_related_payments_store.go new file mode 100644 index 00000000..8ccbc49c --- /dev/null +++ b/internal/connectors/engine/activities/storage_payment_initiations_related_payments_store.go @@ -0,0 +1,29 @@ +package activities + +import ( + "context" + "time" + + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type RelatedPayment struct { + PiID models.PaymentInitiationID + PID models.PaymentID + CreatedAt time.Time +} + +func (a Activities) StoragePaymentInitiationsRelatedPaymentsStore(ctx context.Context, relatedPayment RelatedPayment) error { + return a.storage.PaymentInitiationRelatedPaymentsUpsert(ctx, relatedPayment.PiID, relatedPayment.PID, relatedPayment.CreatedAt) +} + +var StoragePaymentInitiationsRelatedPaymentsStoreActivity = Activities{}.StoragePaymentInitiationsRelatedPaymentsStore + +func StoragePaymentInitiationsRelatedPaymentsStore(ctx workflow.Context, piID models.PaymentInitiationID, pID models.PaymentID, createdAt time.Time) error { + return executeActivity(ctx, StoragePaymentInitiationsRelatedPaymentsStoreActivity, nil, RelatedPayment{ + PiID: piID, + PID: pID, + CreatedAt: createdAt, + }) +} diff --git a/internal/connectors/engine/engine.go b/internal/connectors/engine/engine.go index 0214f09d..debb1d2c 100644 --- a/internal/connectors/engine/engine.go +++ b/internal/connectors/engine/engine.go @@ -26,6 +26,8 @@ type Engine interface { UninstallConnector(ctx context.Context, connectorID models.ConnectorID) error ResetConnector(ctx context.Context, connectorID models.ConnectorID) error ForwardBankAccount(ctx context.Context, bankAccountID uuid.UUID, connectorID models.ConnectorID) (*models.BankAccount, error) + CreateTransfer(ctx context.Context, piID models.PaymentInitiationID, attempt int) error + CreatePayout(ctx context.Context, piID models.PaymentInitiationID, attempt int) error HandleWebhook(ctx context.Context, urlPath string, webhook models.Webhook) error CreatePool(ctx context.Context, pool models.Pool) error AddAccountToPool(ctx context.Context, id uuid.UUID, accountID models.AccountID) error @@ -252,6 +254,66 @@ func (e *engine) ForwardBankAccount(ctx context.Context, bankAccountID uuid.UUID return &bankAccount, nil } +func (e *engine) CreateTransfer(ctx context.Context, piID models.PaymentInitiationID, attempt int) error { + run, err := e.temporalClient.ExecuteWorkflow( + ctx, + client.StartWorkflowOptions{ + ID: fmt.Sprintf("create-transfer-%s-%s-%d", piID.ConnectorID.String(), piID.String(), attempt), + TaskQueue: piID.ConnectorID.String(), + WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE, + WorkflowExecutionErrorWhenAlreadyStarted: false, + SearchAttributes: map[string]interface{}{ + workflow.SearchAttributeStack: e.stack, + }, + }, + workflow.RunCreateTransfer, + workflow.CreateTransfer{ + ConnectorID: piID.ConnectorID, + PaymentInitiationID: piID, + }, + ) + if err != nil { + return err + } + + // Wait for bank account creation to complete + if err := run.Get(ctx, nil); err != nil { + return err + } + + return nil +} + +func (e *engine) CreatePayout(ctx context.Context, piID models.PaymentInitiationID, attempt int) error { + run, err := e.temporalClient.ExecuteWorkflow( + ctx, + client.StartWorkflowOptions{ + ID: fmt.Sprintf("create-payout-%s-%s-%d", piID.ConnectorID.String(), piID.String(), attempt), + TaskQueue: piID.ConnectorID.String(), + WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_REJECT_DUPLICATE, + WorkflowExecutionErrorWhenAlreadyStarted: false, + SearchAttributes: map[string]interface{}{ + workflow.SearchAttributeStack: e.stack, + }, + }, + workflow.RunCreatePayout, + workflow.CreatePayout{ + ConnectorID: piID.ConnectorID, + PaymentInitiationID: piID, + }, + ) + if err != nil { + return err + } + + // Wait for bank account creation to complete + if err := run.Get(ctx, nil); err != nil { + return err + } + + return nil +} + func (e *engine) HandleWebhook(ctx context.Context, urlPath string, webhook models.Webhook) error { configs, err := e.webhooks.GetConfigs(webhook.ConnectorID, urlPath) if err != nil { diff --git a/internal/connectors/engine/plugins/impl.go b/internal/connectors/engine/plugins/impl.go index 9276a06c..4dece36c 100644 --- a/internal/connectors/engine/plugins/impl.go +++ b/internal/connectors/engine/plugins/impl.go @@ -187,6 +187,42 @@ func (i *impl) CreateBankAccount(ctx context.Context, req models.CreateBankAccou }, nil } +func (i *impl) CreateTransfer(ctx context.Context, req models.CreateTransferRequest) (models.CreateTransferResponse, error) { + resp, err := i.pluginClient.CreateTransfer(ctx, &services.CreateTransferRequest{ + PaymentInitiation: grpc.TranslatePaymentInitiation(req.PaymentInitiation), + }) + if err != nil { + return models.CreateTransferResponse{}, err + } + + payment, err := grpc.TranslateProtoPayment(resp.Payment) + if err != nil { + return models.CreateTransferResponse{}, err + } + + return models.CreateTransferResponse{ + Payment: payment, + }, nil +} + +func (i *impl) CreatePayout(ctx context.Context, req models.CreatePayoutRequest) (models.CreatePayoutResponse, error) { + resp, err := i.pluginClient.CreatePayout(ctx, &services.CreatePayoutRequest{ + PaymentInitiation: grpc.TranslatePaymentInitiation(req.PaymentInitiation), + }) + if err != nil { + return models.CreatePayoutResponse{}, err + } + + payment, err := grpc.TranslateProtoPayment(resp.Payment) + if err != nil { + return models.CreatePayoutResponse{}, err + } + + return models.CreatePayoutResponse{ + Payment: payment, + }, nil +} + func (i *impl) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { resp, err := i.pluginClient.CreateWebhooks(ctx, &services.CreateWebhooksRequest{ ConnectorId: req.ConnectorID, diff --git a/internal/connectors/engine/workflow/create_payout.go b/internal/connectors/engine/workflow/create_payout.go new file mode 100644 index 00000000..6eeed2e2 --- /dev/null +++ b/internal/connectors/engine/workflow/create_payout.go @@ -0,0 +1,132 @@ +package workflow + +import ( + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type CreatePayout struct { + ConnectorID models.ConnectorID + PaymentInitiationID models.PaymentInitiationID +} + +func (w Workflow) runCreatePayout( + ctx workflow.Context, + createPayout CreatePayout, +) error { + // Get the payment initiation + pi, err := activities.StoragePaymentInitiationsGet( + infiniteRetryContext(ctx), + createPayout.PaymentInitiationID, + ) + if err != nil { + return err + } + + var sourceAccount *models.Account + if pi.SourceAccountID != nil { + sourceAccount, err = activities.StorageAccountsGet( + infiniteRetryContext(ctx), + *pi.SourceAccountID, + ) + if err != nil { + return err + } + } + + var destinationAccount *models.Account + if pi.DestinationAccountID != nil { + destinationAccount, err = activities.StorageAccountsGet( + infiniteRetryContext(ctx), + *pi.DestinationAccountID, + ) + if err != nil { + return err + } + } + + pspPI := models.FromPaymentInitiationToPSPPaymentInitiation(pi, models.ToPSPAccount(sourceAccount), models.ToPSPAccount(destinationAccount)) + + err = w.addPIAdjustment( + ctx, + models.PaymentInitiationAdjustmentID{ + PaymentInitiationID: createPayout.PaymentInitiationID, + CreatedAt: workflow.Now(ctx), + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING, + }, + nil, + nil, + ) + if err != nil { + return err + } + + createPayoutResponse, errPlugin := activities.PluginCreatePayout( + infiniteRetryContext(ctx), + createPayout.ConnectorID, + models.CreatePayoutRequest{ + PaymentInitiation: *pspPI, + }, + ) + switch errPlugin { + case nil: + payment := models.FromPSPPaymentToPayment(createPayoutResponse.Payment, createPayout.ConnectorID) + + err = activities.StoragePaymentsStore( + infiniteRetryContext(ctx), + []models.Payment{payment}, + ) + if err != nil { + return err + } + + err = activities.StoragePaymentInitiationsRelatedPaymentsStore( + infiniteRetryContext(ctx), + createPayout.PaymentInitiationID, + payment.ID, + createPayoutResponse.Payment.CreatedAt, + ) + if err != nil { + return err + } + + err = w.addPIAdjustment( + ctx, + models.PaymentInitiationAdjustmentID{ + PaymentInitiationID: createPayout.PaymentInitiationID, + CreatedAt: workflow.Now(ctx), + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSED, + }, + nil, + nil, + ) + if err != nil { + return err + } + + return nil + default: + err = w.addPIAdjustment( + ctx, + models.PaymentInitiationAdjustmentID{ + PaymentInitiationID: createPayout.PaymentInitiationID, + CreatedAt: workflow.Now(ctx), + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED, + }, + &err, + nil, + ) + if err != nil { + return err + } + + return errPlugin + } +} + +var RunCreatePayout any + +func init() { + RunCreatePayout = Workflow{}.runCreatePayout +} diff --git a/internal/connectors/engine/workflow/create_transfer.go b/internal/connectors/engine/workflow/create_transfer.go new file mode 100644 index 00000000..11443705 --- /dev/null +++ b/internal/connectors/engine/workflow/create_transfer.go @@ -0,0 +1,157 @@ +package workflow + +import ( + "github.com/formancehq/payments/internal/connectors/engine/activities" + "github.com/formancehq/payments/internal/models" + "go.temporal.io/sdk/workflow" +) + +type CreateTransfer struct { + ConnectorID models.ConnectorID + PaymentInitiationID models.PaymentInitiationID +} + +func (w Workflow) runCreateTransfer( + ctx workflow.Context, + createTransfer CreateTransfer, +) error { + // Get the payment initiation + pi, err := activities.StoragePaymentInitiationsGet( + infiniteRetryContext(ctx), + createTransfer.PaymentInitiationID, + ) + if err != nil { + return err + } + + var sourceAccount *models.Account + if pi.SourceAccountID != nil { + sourceAccount, err = activities.StorageAccountsGet( + infiniteRetryContext(ctx), + *pi.SourceAccountID, + ) + if err != nil { + return err + } + } + + var destinationAccount *models.Account + if pi.DestinationAccountID != nil { + destinationAccount, err = activities.StorageAccountsGet( + infiniteRetryContext(ctx), + *pi.DestinationAccountID, + ) + if err != nil { + return err + } + } + + pspPI := models.FromPaymentInitiationToPSPPaymentInitiation(pi, models.ToPSPAccount(sourceAccount), models.ToPSPAccount(destinationAccount)) + + err = w.addPIAdjustment( + ctx, + models.PaymentInitiationAdjustmentID{ + PaymentInitiationID: createTransfer.PaymentInitiationID, + CreatedAt: workflow.Now(ctx), + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING, + }, + nil, + nil, + ) + if err != nil { + return err + } + + createTransferResponse, errPlugin := activities.PluginCreateTransfer( + infiniteRetryContext(ctx), + createTransfer.ConnectorID, + models.CreateTransferRequest{ + PaymentInitiation: *pspPI, + }, + ) + switch errPlugin { + case nil: + payment := models.FromPSPPaymentToPayment(createTransferResponse.Payment, createTransfer.ConnectorID) + + err := activities.StoragePaymentsStore( + infiniteRetryContext(ctx), + []models.Payment{payment}, + ) + if err != nil { + return errPlugin + } + + err = activities.StoragePaymentInitiationsRelatedPaymentsStore( + infiniteRetryContext(ctx), + createTransfer.PaymentInitiationID, + payment.ID, + createTransferResponse.Payment.CreatedAt, + ) + if err != nil { + return errPlugin + } + + err = w.addPIAdjustment( + ctx, + models.PaymentInitiationAdjustmentID{ + PaymentInitiationID: createTransfer.PaymentInitiationID, + CreatedAt: workflow.Now(ctx), + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSED, + }, + nil, + nil, + ) + if err != nil { + return errPlugin + } + + return nil + default: + err := w.addPIAdjustment( + ctx, + models.PaymentInitiationAdjustmentID{ + PaymentInitiationID: createTransfer.PaymentInitiationID, + CreatedAt: workflow.Now(ctx), + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED, + }, + &errPlugin, + nil, + ) + if err != nil { + return err + } + + return errPlugin + } +} + +var RunCreateTransfer any + +func init() { + RunCreateTransfer = Workflow{}.runCreateTransfer +} + +func (w Workflow) addPIAdjustment( + ctx workflow.Context, + adjustmentID models.PaymentInitiationAdjustmentID, + err *error, + metadata map[string]string, +) error { + adj := models.PaymentInitiationAdjustment{ + ID: adjustmentID, + PaymentInitiationID: adjustmentID.PaymentInitiationID, + CreatedAt: workflow.Now(ctx), + Status: adjustmentID.Status, + Metadata: metadata, + } + + if err != nil { + errStr := (*err).Error() + adj.Error = &errStr + } + + return activities.PaymentInitiationsAdjustmentsStore( + infiniteRetryContext(ctx), + adj, + ) +} diff --git a/internal/connectors/engine/workflow/workflow.go b/internal/connectors/engine/workflow/workflow.go index 8ea43a95..7eec7feb 100644 --- a/internal/connectors/engine/workflow/workflow.go +++ b/internal/connectors/engine/workflow/workflow.go @@ -85,6 +85,14 @@ func (w Workflow) DefinitionSet() temporalworker.DefinitionSet { Name: "CreateBankAccount", Func: w.runCreateBankAccount, }). + Append(temporalworker.Definition{ + Name: "CreatePayout", + Func: w.runCreatePayout, + }). + Append(temporalworker.Definition{ + Name: "CreateTransfer", + Func: w.runCreateTransfer, + }). Append(temporalworker.Definition{ Name: "Run", Func: w.run, diff --git a/internal/connectors/grpc/grpc_client.go b/internal/connectors/grpc/grpc_client.go index 26fc2ee1..05d8c203 100644 --- a/internal/connectors/grpc/grpc_client.go +++ b/internal/connectors/grpc/grpc_client.go @@ -42,6 +42,14 @@ func (c *GRPCClient) CreateBankAccount(ctx context.Context, req *services.Create return c.client.CreateBankAccount(ctx, req) } +func (c *GRPCClient) CreateTransfer(ctx context.Context, req *services.CreateTransferRequest) (*services.CreateTransferResponse, error) { + return c.client.CreateTransfer(ctx, req) +} + +func (c *GRPCClient) CreatePayout(ctx context.Context, req *services.CreatePayoutRequest) (*services.CreatePayoutResponse, error) { + return c.client.CreatePayout(ctx, req) +} + func (c *GRPCClient) CreateWebhooks(ctx context.Context, req *services.CreateWebhooksRequest) (*services.CreateWebhooksResponse, error) { return c.client.CreateWebhooks(ctx, req) } diff --git a/internal/connectors/grpc/grpc_server.go b/internal/connectors/grpc/grpc_server.go index 13e05ac4..870f512e 100644 --- a/internal/connectors/grpc/grpc_server.go +++ b/internal/connectors/grpc/grpc_server.go @@ -46,6 +46,14 @@ func (s *GRPCServer) CreateBankAccount(ctx context.Context, req *services.Create return s.Impl.CreateBankAccount(ctx, req) } +func (s *GRPCServer) CreateTransfer(ctx context.Context, req *services.CreateTransferRequest) (*services.CreateTransferResponse, error) { + return s.Impl.CreateTransfer(ctx, req) +} + +func (s *GRPCServer) CreatePayout(ctx context.Context, req *services.CreatePayoutRequest) (*services.CreatePayoutResponse, error) { + return s.Impl.CreatePayout(ctx, req) +} + func (s *GRPCServer) CreateWebhooks(ctx context.Context, req *services.CreateWebhooksRequest) (*services.CreateWebhooksResponse, error) { return s.Impl.CreateWebhooks(ctx, req) } diff --git a/internal/connectors/grpc/interfaces.go b/internal/connectors/grpc/interfaces.go index 5f51e72c..40563c63 100644 --- a/internal/connectors/grpc/interfaces.go +++ b/internal/connectors/grpc/interfaces.go @@ -20,6 +20,8 @@ type PSP interface { FetchNextExternalAccounts(ctx context.Context, in *services.FetchNextExternalAccountsRequest) (*services.FetchNextExternalAccountsResponse, error) CreateBankAccount(ctx context.Context, in *services.CreateBankAccountRequest) (*services.CreateBankAccountResponse, error) + CreateTransfer(ctx context.Context, in *services.CreateTransferRequest) (*services.CreateTransferResponse, error) + CreatePayout(ctx context.Context, in *services.CreatePayoutRequest) (*services.CreatePayoutResponse, error) CreateWebhooks(ctx context.Context, in *services.CreateWebhooksRequest) (*services.CreateWebhooksResponse, error) TranslateWebhook(ctx context.Context, in *services.TranslateWebhookRequest) (*services.TranslateWebhookResponse, error) diff --git a/internal/connectors/grpc/proto/payment_initiation.pb.go b/internal/connectors/grpc/proto/payment_initiation.pb.go new file mode 100644 index 00000000..09bee502 --- /dev/null +++ b/internal/connectors/grpc/proto/payment_initiation.pb.go @@ -0,0 +1,252 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.30.0 +// protoc v3.21.12 +// source: payment_initiation.proto + +package proto + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type PaymentInitiation struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Unique reference for the payment + Reference string `protobuf:"bytes,1,opt,name=reference,proto3" json:"reference,omitempty"` + // Timestamp of the payment creation + CreatedAt *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"` + // Description of the payment + Description string `protobuf:"bytes,3,opt,name=description,proto3" json:"description,omitempty"` + // PSP source account for the payment + SourceAccount *Account `protobuf:"bytes,4,opt,name=source_account,json=sourceAccount,proto3" json:"source_account,omitempty"` + // PSP destination account for the payment + DestinationAccount *Account `protobuf:"bytes,5,opt,name=destination_account,json=destinationAccount,proto3" json:"destination_account,omitempty"` + // Amount and asset of the payment + Amount *Monetary `protobuf:"bytes,6,opt,name=amount,proto3" json:"amount,omitempty"` + // Additional metadata + Metadata map[string]string `protobuf:"bytes,7,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"` +} + +func (x *PaymentInitiation) Reset() { + *x = PaymentInitiation{} + if protoimpl.UnsafeEnabled { + mi := &file_payment_initiation_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *PaymentInitiation) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PaymentInitiation) ProtoMessage() {} + +func (x *PaymentInitiation) ProtoReflect() protoreflect.Message { + mi := &file_payment_initiation_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PaymentInitiation.ProtoReflect.Descriptor instead. +func (*PaymentInitiation) Descriptor() ([]byte, []int) { + return file_payment_initiation_proto_rawDescGZIP(), []int{0} +} + +func (x *PaymentInitiation) GetReference() string { + if x != nil { + return x.Reference + } + return "" +} + +func (x *PaymentInitiation) GetCreatedAt() *timestamppb.Timestamp { + if x != nil { + return x.CreatedAt + } + return nil +} + +func (x *PaymentInitiation) GetDescription() string { + if x != nil { + return x.Description + } + return "" +} + +func (x *PaymentInitiation) GetSourceAccount() *Account { + if x != nil { + return x.SourceAccount + } + return nil +} + +func (x *PaymentInitiation) GetDestinationAccount() *Account { + if x != nil { + return x.DestinationAccount + } + return nil +} + +func (x *PaymentInitiation) GetAmount() *Monetary { + if x != nil { + return x.Amount + } + return nil +} + +func (x *PaymentInitiation) GetMetadata() map[string]string { + if x != nil { + return x.Metadata + } + return nil +} + +var File_payment_initiation_proto protoreflect.FileDescriptor + +var file_payment_initiation_proto_rawDesc = []byte{ + 0x0a, 0x18, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x27, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x1a, 0x0e, 0x6d, 0x6f, 0x6e, 0x65, 0x74, 0x61, 0x72, 0x79, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x22, 0xb8, 0x04, 0x0a, 0x11, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x49, + 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x1c, 0x0a, 0x09, 0x72, 0x65, 0x66, + 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, + 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x12, 0x39, 0x0a, 0x0a, 0x63, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, + 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, + 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, + 0x41, 0x74, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x69, 0x6f, + 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, + 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x57, 0x0a, 0x0e, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x5f, 0x61, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x0d, + 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x61, 0x0a, + 0x13, 0x64, 0x65, 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x12, 0x64, 0x65, + 0x73, 0x74, 0x69, 0x6e, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x49, 0x0a, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x31, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, + 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4d, 0x6f, 0x6e, 0x65, 0x74, + 0x61, 0x72, 0x79, 0x52, 0x06, 0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x64, 0x0a, 0x08, 0x6d, + 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x48, 0x2e, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x49, + 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, + 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x08, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x1a, 0x3b, 0x0a, 0x0d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x3f, + 0x5a, 0x3d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x68, 0x71, 0x2f, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_payment_initiation_proto_rawDescOnce sync.Once + file_payment_initiation_proto_rawDescData = file_payment_initiation_proto_rawDesc +) + +func file_payment_initiation_proto_rawDescGZIP() []byte { + file_payment_initiation_proto_rawDescOnce.Do(func() { + file_payment_initiation_proto_rawDescData = protoimpl.X.CompressGZIP(file_payment_initiation_proto_rawDescData) + }) + return file_payment_initiation_proto_rawDescData +} + +var file_payment_initiation_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_payment_initiation_proto_goTypes = []interface{}{ + (*PaymentInitiation)(nil), // 0: formance.payments.connectors.grpc.proto.PaymentInitiation + nil, // 1: formance.payments.connectors.grpc.proto.PaymentInitiation.MetadataEntry + (*timestamppb.Timestamp)(nil), // 2: google.protobuf.Timestamp + (*Account)(nil), // 3: formance.payments.connectors.grpc.proto.Account + (*Monetary)(nil), // 4: formance.payments.connectors.grpc.proto.Monetary +} +var file_payment_initiation_proto_depIdxs = []int32{ + 2, // 0: formance.payments.connectors.grpc.proto.PaymentInitiation.created_at:type_name -> google.protobuf.Timestamp + 3, // 1: formance.payments.connectors.grpc.proto.PaymentInitiation.source_account:type_name -> formance.payments.connectors.grpc.proto.Account + 3, // 2: formance.payments.connectors.grpc.proto.PaymentInitiation.destination_account:type_name -> formance.payments.connectors.grpc.proto.Account + 4, // 3: formance.payments.connectors.grpc.proto.PaymentInitiation.amount:type_name -> formance.payments.connectors.grpc.proto.Monetary + 1, // 4: formance.payments.connectors.grpc.proto.PaymentInitiation.metadata:type_name -> formance.payments.connectors.grpc.proto.PaymentInitiation.MetadataEntry + 5, // [5:5] is the sub-list for method output_type + 5, // [5:5] is the sub-list for method input_type + 5, // [5:5] is the sub-list for extension type_name + 5, // [5:5] is the sub-list for extension extendee + 0, // [0:5] is the sub-list for field type_name +} + +func init() { file_payment_initiation_proto_init() } +func file_payment_initiation_proto_init() { + if File_payment_initiation_proto != nil { + return + } + file_account_proto_init() + file_monetary_proto_init() + if !protoimpl.UnsafeEnabled { + file_payment_initiation_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*PaymentInitiation); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_payment_initiation_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_payment_initiation_proto_goTypes, + DependencyIndexes: file_payment_initiation_proto_depIdxs, + MessageInfos: file_payment_initiation_proto_msgTypes, + }.Build() + File_payment_initiation_proto = out.File + file_payment_initiation_proto_rawDesc = nil + file_payment_initiation_proto_goTypes = nil + file_payment_initiation_proto_depIdxs = nil +} diff --git a/internal/connectors/grpc/proto/payment_initiation.proto b/internal/connectors/grpc/proto/payment_initiation.proto new file mode 100644 index 00000000..3ce11ee3 --- /dev/null +++ b/internal/connectors/grpc/proto/payment_initiation.proto @@ -0,0 +1,30 @@ +syntax = "proto3"; +package formance.payments.connectors.grpc.proto; +option go_package = "github.com/formancehq/payments/internal/connectors/grpc/proto"; + +import "google/protobuf/timestamp.proto"; + +import "account.proto"; +import "monetary.proto"; + +message PaymentInitiation { + // Unique reference for the payment + string reference = 1; + + // Timestamp of the payment creation + google.protobuf.Timestamp created_at = 2; + + // Description of the payment + string description = 3; + + // PSP source account for the payment + formance.payments.connectors.grpc.proto.Account source_account = 4; + // PSP destination account for the payment + formance.payments.connectors.grpc.proto.Account destination_account = 5; + + // Amount and asset of the payment + formance.payments.connectors.grpc.proto.Monetary amount = 6; + + // Additional metadata + map metadata = 7; +} \ No newline at end of file diff --git a/internal/connectors/grpc/proto/services/plugin.pb.go b/internal/connectors/grpc/proto/services/plugin.pb.go index a0a3df7d..ed4b925f 100644 --- a/internal/connectors/grpc/proto/services/plugin.pb.go +++ b/internal/connectors/grpc/proto/services/plugin.pb.go @@ -1160,6 +1160,194 @@ func (x *CreateWebhooksResponse) GetOthers() []*proto.Other { return nil } +type CreateTransferRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PaymentInitiation *proto.PaymentInitiation `protobuf:"bytes,1,opt,name=payment_initiation,json=paymentInitiation,proto3" json:"payment_initiation,omitempty"` +} + +func (x *CreateTransferRequest) Reset() { + *x = CreateTransferRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[20] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateTransferRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTransferRequest) ProtoMessage() {} + +func (x *CreateTransferRequest) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[20] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateTransferRequest.ProtoReflect.Descriptor instead. +func (*CreateTransferRequest) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{20} +} + +func (x *CreateTransferRequest) GetPaymentInitiation() *proto.PaymentInitiation { + if x != nil { + return x.PaymentInitiation + } + return nil +} + +type CreateTransferResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Payment *proto.Payment `protobuf:"bytes,1,opt,name=payment,proto3" json:"payment,omitempty"` +} + +func (x *CreateTransferResponse) Reset() { + *x = CreateTransferResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[21] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreateTransferResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreateTransferResponse) ProtoMessage() {} + +func (x *CreateTransferResponse) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[21] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreateTransferResponse.ProtoReflect.Descriptor instead. +func (*CreateTransferResponse) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{21} +} + +func (x *CreateTransferResponse) GetPayment() *proto.Payment { + if x != nil { + return x.Payment + } + return nil +} + +type CreatePayoutRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + PaymentInitiation *proto.PaymentInitiation `protobuf:"bytes,1,opt,name=payment_initiation,json=paymentInitiation,proto3" json:"payment_initiation,omitempty"` +} + +func (x *CreatePayoutRequest) Reset() { + *x = CreatePayoutRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[22] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreatePayoutRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreatePayoutRequest) ProtoMessage() {} + +func (x *CreatePayoutRequest) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[22] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreatePayoutRequest.ProtoReflect.Descriptor instead. +func (*CreatePayoutRequest) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{22} +} + +func (x *CreatePayoutRequest) GetPaymentInitiation() *proto.PaymentInitiation { + if x != nil { + return x.PaymentInitiation + } + return nil +} + +type CreatePayoutResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Payment *proto.Payment `protobuf:"bytes,1,opt,name=payment,proto3" json:"payment,omitempty"` +} + +func (x *CreatePayoutResponse) Reset() { + *x = CreatePayoutResponse{} + if protoimpl.UnsafeEnabled { + mi := &file_services_plugin_proto_msgTypes[23] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CreatePayoutResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CreatePayoutResponse) ProtoMessage() {} + +func (x *CreatePayoutResponse) ProtoReflect() protoreflect.Message { + mi := &file_services_plugin_proto_msgTypes[23] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CreatePayoutResponse.ProtoReflect.Descriptor instead. +func (*CreatePayoutResponse) Descriptor() ([]byte, []int) { + return file_services_plugin_proto_rawDescGZIP(), []int{23} +} + +func (x *CreatePayoutResponse) GetPayment() *proto.Payment { + if x != nil { + return x.Payment + } + return nil +} + type TranslateWebhookResponse_Response struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1178,7 +1366,7 @@ type TranslateWebhookResponse_Response struct { func (x *TranslateWebhookResponse_Response) Reset() { *x = TranslateWebhookResponse_Response{} if protoimpl.UnsafeEnabled { - mi := &file_services_plugin_proto_msgTypes[20] + mi := &file_services_plugin_proto_msgTypes[24] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1191,7 +1379,7 @@ func (x *TranslateWebhookResponse_Response) String() string { func (*TranslateWebhookResponse_Response) ProtoMessage() {} func (x *TranslateWebhookResponse_Response) ProtoReflect() protoreflect.Message { - mi := &file_services_plugin_proto_msgTypes[20] + mi := &file_services_plugin_proto_msgTypes[24] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1290,78 +1478,98 @@ var file_services_plugin_proto_rawDesc = []byte{ 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x10, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0b, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x70, 0x61, 0x79, 0x6d, 0x65, - 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0d, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, - 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, - 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x28, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x74, 0x61, - 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, - 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, - 0x67, 0x22, 0x9c, 0x02, 0x0a, 0x0f, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x57, 0x0a, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, - 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x33, 0x2e, 0x66, 0x6f, - 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, - 0x52, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x4d, - 0x0a, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x31, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, - 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x66, - 0x6c, 0x6f, 0x77, 0x52, 0x08, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x12, 0x61, 0x0a, - 0x10, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, - 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x36, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, + 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x18, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x1a, 0x0d, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x1a, 0x0e, 0x77, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0x28, 0x0a, 0x0e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x16, 0x0a, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x06, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x9c, 0x02, 0x0a, 0x0f, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, + 0x57, 0x0a, 0x0c, 0x63, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x18, + 0x01, 0x20, 0x03, 0x28, 0x0e, 0x32, 0x33, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, + 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x43, 0x61, 0x70, 0x61, 0x62, 0x69, 0x6c, 0x69, 0x74, 0x79, 0x52, 0x0c, 0x63, 0x61, 0x70, 0x61, + 0x62, 0x69, 0x6c, 0x69, 0x74, 0x69, 0x65, 0x73, 0x12, 0x4d, 0x0a, 0x08, 0x77, 0x6f, 0x72, 0x6b, + 0x66, 0x6c, 0x6f, 0x77, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x52, 0x08, 0x77, + 0x6f, 0x72, 0x6b, 0x66, 0x6c, 0x6f, 0x77, 0x12, 0x61, 0x0a, 0x10, 0x77, 0x65, 0x62, 0x68, 0x6f, + 0x6f, 0x6b, 0x73, 0x5f, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x36, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, + 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x65, 0x62, 0x68, + 0x6f, 0x6f, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x0f, 0x77, 0x65, 0x62, 0x68, 0x6f, + 0x6f, 0x6b, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x22, 0x35, 0x0a, 0x10, 0x55, 0x6e, + 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, + 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, + 0x64, 0x22, 0x13, 0x0a, 0x11, 0x55, 0x6e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x82, 0x01, 0x0a, 0x16, 0x46, 0x65, 0x74, 0x63, 0x68, + 0x4e, 0x65, 0x78, 0x74, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, + 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, + 0x6d, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, + 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x99, 0x01, 0x0a, 0x17, + 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x06, 0x6f, 0x74, 0x68, 0x65, 0x72, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, - 0x0f, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, - 0x22, 0x35, 0x0a, 0x10, 0x55, 0x6e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x22, 0x13, 0x0a, 0x11, 0x55, 0x6e, 0x69, 0x6e, 0x73, - 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x82, 0x01, 0x0a, - 0x16, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x73, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x66, - 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x14, - 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, - 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, - 0x65, 0x22, 0x99, 0x01, 0x0a, 0x17, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x4f, - 0x74, 0x68, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, - 0x06, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, - 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, - 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x52, 0x06, 0x6f, - 0x74, 0x68, 0x65, 0x72, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, + 0x6f, 0x2e, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x52, 0x06, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x12, + 0x1b, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0c, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, + 0x68, 0x61, 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, + 0x68, 0x61, 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x22, 0x70, 0x0a, 0x18, 0x46, 0x65, 0x74, 0x63, 0x68, + 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6c, + 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x50, + 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, + 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0xa1, 0x01, 0x0a, 0x19, 0x46, 0x65, + 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x08, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x08, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x22, 0x70, 0x0a, - 0x18, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x18, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, - 0xa1, 0x01, 0x0a, 0x19, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x79, - 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, - 0x08, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0xa1, 0x01, 0x0a, 0x19, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x41, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, + 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, - 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x52, 0x08, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, + 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, + 0x74, 0x52, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, 0x73, 0x4d, - 0x6f, 0x72, 0x65, 0x22, 0x70, 0x0a, 0x18, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, - 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, - 0x21, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x50, 0x61, 0x79, 0x6c, 0x6f, - 0x61, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, - 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x67, - 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0xa1, 0x01, 0x0a, 0x19, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, - 0x65, 0x78, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6f, 0x72, 0x65, 0x22, 0x78, 0x0a, 0x20, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, + 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, + 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x66, + 0x72, 0x6f, 0x6d, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, + 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, + 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0xa9, 0x01, + 0x0a, 0x21, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x45, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x08, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, @@ -1370,205 +1578,232 @@ var file_services_plugin_proto_rawDesc = []byte{ 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x07, 0x68, 0x61, 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x22, 0x78, 0x0a, 0x20, 0x46, 0x65, 0x74, - 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, - 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, - 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, - 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, - 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, - 0x69, 0x7a, 0x65, 0x22, 0xa9, 0x01, 0x0a, 0x21, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, - 0x74, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x08, 0x61, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, + 0x52, 0x07, 0x68, 0x61, 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x22, 0x70, 0x0a, 0x18, 0x46, 0x65, 0x74, + 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, + 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, + 0x6d, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, + 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, + 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0xa1, 0x01, 0x0a, 0x19, + 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4c, 0x0a, 0x08, 0x62, 0x61, 0x6c, + 0x61, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x08, 0x61, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x08, 0x62, + 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x22, - 0x70, 0x0a, 0x18, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x42, 0x61, 0x6c, 0x61, - 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x66, - 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x14, - 0x0a, 0x05, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x73, - 0x74, 0x61, 0x74, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, - 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, - 0x65, 0x22, 0xa1, 0x01, 0x0a, 0x19, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x42, - 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, - 0x4c, 0x0a, 0x08, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, + 0x73, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x57, 0x0a, 0x0c, 0x62, + 0x61, 0x6e, 0x6b, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x34, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, - 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x42, 0x61, 0x6c, 0x61, - 0x6e, 0x63, 0x65, 0x52, 0x08, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x1b, 0x0a, - 0x09, 0x6e, 0x65, 0x77, 0x5f, 0x73, 0x74, 0x61, 0x74, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, - 0x52, 0x08, 0x6e, 0x65, 0x77, 0x53, 0x74, 0x61, 0x74, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, - 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, - 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x22, 0x73, 0x0a, 0x18, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, - 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, - 0x74, 0x12, 0x57, 0x0a, 0x0c, 0x62, 0x61, 0x6e, 0x6b, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, - 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x34, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, - 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x2e, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x0b, 0x62, - 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x76, 0x0a, 0x19, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x59, 0x0a, 0x0f, 0x72, 0x65, 0x6c, 0x61, 0x74, - 0x65, 0x64, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, - 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x52, 0x0e, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x22, 0x79, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x57, - 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, - 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, - 0x65, 0x12, 0x4a, 0x0a, 0x07, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, - 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x65, 0x62, - 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x07, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x22, 0x89, 0x04, - 0x0a, 0x18, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, - 0x6f, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x09, 0x72, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, - 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, + 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x42, 0x61, 0x6e, 0x6b, + 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x0b, 0x62, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x76, 0x0a, 0x19, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x61, + 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x12, 0x59, 0x0a, 0x0f, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, + 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x0e, 0x72, 0x65, + 0x6c, 0x61, 0x74, 0x65, 0x64, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x79, 0x0a, 0x17, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x1a, 0x8a, 0x03, 0x0a, - 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x64, 0x65, - 0x6d, 0x70, 0x6f, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0e, 0x69, 0x64, 0x65, 0x6d, 0x70, 0x6f, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x4b, - 0x65, 0x79, 0x12, 0x4c, 0x0a, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, - 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, - 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x63, - 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, - 0x12, 0x5d, 0x0a, 0x10, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x63, 0x63, - 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, + 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x4a, 0x0a, 0x07, 0x77, + 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x07, + 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x22, 0x89, 0x04, 0x0a, 0x18, 0x54, 0x72, 0x61, 0x6e, + 0x73, 0x6c, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x60, 0x0a, 0x09, 0x72, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x42, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, + 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c, + 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x2e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x09, 0x72, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x73, 0x1a, 0x8a, 0x03, 0x0a, 0x08, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x27, 0x0a, 0x0f, 0x69, 0x64, 0x65, 0x6d, 0x70, 0x6f, 0x74, 0x65, 0x6e, + 0x63, 0x79, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x69, 0x64, + 0x65, 0x6d, 0x70, 0x6f, 0x74, 0x65, 0x6e, 0x63, 0x79, 0x4b, 0x65, 0x79, 0x12, 0x4c, 0x0a, 0x07, + 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, + 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x48, + 0x00, 0x52, 0x07, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x5d, 0x0a, 0x10, 0x65, 0x78, + 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x0b, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, + 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, + 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0f, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x4c, 0x0a, 0x07, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, - 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x0f, - 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, - 0x4c, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, - 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, - 0x6e, 0x74, 0x48, 0x00, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x4c, 0x0a, - 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, + 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x00, 0x52, 0x07, + 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x4c, 0x0a, 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, + 0x63, 0x65, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, + 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, + 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x48, 0x00, 0x52, 0x07, 0x62, 0x61, + 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, + 0x74, 0x65, 0x64, 0x22, 0x87, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x57, 0x65, + 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, + 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, + 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, + 0x72, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x62, + 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, + 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x42, 0x61, 0x73, 0x65, 0x55, 0x72, 0x6c, 0x22, 0x60, 0x0a, + 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, 0x06, 0x6f, 0x74, 0x68, 0x65, 0x72, + 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, + 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x2e, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x52, 0x06, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x22, + 0x82, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, + 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x69, 0x0a, 0x12, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, + 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, + 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x11, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x64, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, + 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, + 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x22, 0x80, 0x01, 0x0a, 0x13, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x69, 0x0a, 0x12, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x69, 0x6e, + 0x69, 0x74, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x3a, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, - 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, - 0x48, 0x00, 0x52, 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x42, 0x0c, 0x0a, 0x0a, 0x74, - 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x64, 0x22, 0x87, 0x01, 0x0a, 0x15, 0x43, 0x72, - 0x65, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x0c, 0x66, 0x72, 0x6f, 0x6d, 0x5f, 0x70, 0x61, 0x79, 0x6c, - 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x66, 0x72, 0x6f, 0x6d, 0x50, - 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, - 0x74, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0b, 0x63, 0x6f, - 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x77, 0x65, 0x62, - 0x68, 0x6f, 0x6f, 0x6b, 0x5f, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x75, 0x72, 0x6c, 0x18, 0x03, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x0e, 0x77, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x42, 0x61, 0x73, 0x65, - 0x55, 0x72, 0x6c, 0x22, 0x60, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, - 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x46, 0x0a, - 0x06, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, - 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, - 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x52, 0x06, 0x6f, - 0x74, 0x68, 0x65, 0x72, 0x73, 0x32, 0xec, 0x0a, 0x0a, 0x06, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, - 0x12, 0x6e, 0x0a, 0x07, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x12, 0x2f, 0x2e, 0x66, 0x6f, - 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, - 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x49, 0x6e, - 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x66, + 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x11, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x49, 0x6e, 0x69, 0x74, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x22, 0x62, 0x0a, + 0x14, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x4a, 0x0a, 0x07, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, + 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x63, 0x6f, 0x6e, 0x6e, 0x65, + 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, + 0x2e, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x07, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x32, 0xf1, 0x0c, 0x0a, 0x06, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x12, 0x6e, 0x0a, 0x07, + 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x12, 0x2f, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, + 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, 0x6c, + 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, + 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x49, 0x6e, 0x73, 0x74, 0x61, + 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x74, 0x0a, 0x09, + 0x55, 0x6e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x12, 0x31, 0x2e, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, + 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x55, 0x6e, 0x69, 0x6e, + 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x32, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, - 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x49, - 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, - 0x12, 0x74, 0x0a, 0x09, 0x55, 0x6e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x12, 0x31, 0x2e, - 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, - 0x55, 0x6e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x32, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, - 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x2e, 0x55, 0x6e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, - 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x86, 0x01, 0x0a, 0x0f, 0x46, 0x65, 0x74, 0x63, 0x68, - 0x4e, 0x65, 0x78, 0x74, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x12, 0x37, 0x2e, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, - 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, - 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x38, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, - 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, - 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x4f, - 0x74, 0x68, 0x65, 0x72, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, - 0x8c, 0x01, 0x0a, 0x11, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x79, - 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x12, 0x39, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, - 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, - 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x3a, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, - 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x79, 0x6d, - 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x8c, - 0x01, 0x0a, 0x11, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x41, 0x63, 0x63, 0x6f, - 0x75, 0x6e, 0x74, 0x73, 0x12, 0x39, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, - 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, - 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x3a, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, + 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x55, + 0x6e, 0x69, 0x6e, 0x73, 0x74, 0x61, 0x6c, 0x6c, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x00, 0x12, 0x86, 0x01, 0x0a, 0x0f, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, + 0x4f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x12, 0x37, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, + 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, + 0x78, 0x74, 0x4f, 0x74, 0x68, 0x65, 0x72, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, + 0x38, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, - 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0xa4, 0x01, - 0x0a, 0x19, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x45, 0x78, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x41, 0x2e, 0x66, 0x6f, + 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x4f, 0x74, 0x68, 0x65, 0x72, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x8c, 0x01, 0x0a, 0x11, + 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x12, 0x39, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3a, 0x2e, 0x66, + 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, + 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, + 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x8c, 0x01, 0x0a, 0x11, 0x46, + 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, + 0x12, 0x39, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x41, 0x63, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3a, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, + 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0xa4, 0x01, 0x0a, 0x19, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, - 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x42, - 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, - 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, - 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, - 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x12, 0x8c, 0x01, 0x0a, 0x11, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, - 0x78, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x39, 0x2e, 0x66, 0x6f, 0x72, + 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x12, 0x41, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, + 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, + 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, + 0x65, 0x78, 0x74, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x63, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x42, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, - 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3a, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, + 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, + 0x12, 0x8c, 0x01, 0x0a, 0x11, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x42, 0x61, + 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x39, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, + 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, + 0x78, 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x1a, 0x3a, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, 0x74, 0x42, 0x61, 0x6c, + 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, + 0x8c, 0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, + 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x39, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x46, 0x65, 0x74, 0x63, 0x68, 0x4e, 0x65, 0x78, - 0x74, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x22, 0x00, 0x12, 0x8c, 0x01, 0x0a, 0x11, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x61, - 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x39, 0x2e, 0x66, 0x6f, 0x72, 0x6d, + 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x61, + 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x3a, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, + 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x83, + 0x01, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, + 0x72, 0x12, 0x36, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, + 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, + 0x65, 0x72, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, - 0x74, 0x65, 0x42, 0x61, 0x6e, 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, - 0x75, 0x65, 0x73, 0x74, 0x1a, 0x3a, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, + 0x74, 0x65, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0x00, 0x12, 0x7d, 0x0a, 0x0c, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, + 0x79, 0x6f, 0x75, 0x74, 0x12, 0x34, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, - 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x42, 0x61, 0x6e, - 0x6b, 0x41, 0x63, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0x00, 0x12, 0x83, 0x01, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, - 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x12, 0x36, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, - 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x57, 0x65, - 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, 0x2e, - 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, - 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x89, 0x01, 0x0a, 0x10, 0x54, 0x72, 0x61, - 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x38, 0x2e, - 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, - 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, - 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, - 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, - 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, - 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c, - 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x00, 0x42, 0x48, 0x5a, 0x46, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, - 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x68, 0x71, 0x2f, 0x70, 0x61, - 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, - 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, 0x06, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x50, 0x61, 0x79, + 0x6f, 0x75, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x66, 0x6f, 0x72, + 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, + 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x50, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x00, 0x12, 0x83, 0x01, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x57, 0x65, + 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x12, 0x36, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, + 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, + 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x57, + 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x37, + 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x73, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x12, 0x89, 0x01, 0x0a, 0x10, 0x54, 0x72, + 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x12, 0x38, + 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, + 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, + 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x6c, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, + 0x6b, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x39, 0x2e, 0x66, 0x6f, 0x72, 0x6d, 0x61, + 0x6e, 0x63, 0x65, 0x2e, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2e, 0x67, 0x72, 0x70, + 0x63, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, + 0x6c, 0x61, 0x74, 0x65, 0x57, 0x65, 0x62, 0x68, 0x6f, 0x6f, 0x6b, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x48, 0x5a, 0x46, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x68, 0x71, 0x2f, 0x70, + 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x63, 0x6f, 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x2f, 0x67, 0x72, 0x70, 0x63, + 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x62, + 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -1583,7 +1818,7 @@ func file_services_plugin_proto_rawDescGZIP() []byte { return file_services_plugin_proto_rawDescData } -var file_services_plugin_proto_msgTypes = make([]protoimpl.MessageInfo, 21) +var file_services_plugin_proto_msgTypes = make([]protoimpl.MessageInfo, 25) var file_services_plugin_proto_goTypes = []interface{}{ (*InstallRequest)(nil), // 0: formance.payments.grpc.services.InstallRequest (*InstallResponse)(nil), // 1: formance.payments.grpc.services.InstallResponse @@ -1605,60 +1840,73 @@ var file_services_plugin_proto_goTypes = []interface{}{ (*TranslateWebhookResponse)(nil), // 17: formance.payments.grpc.services.TranslateWebhookResponse (*CreateWebhooksRequest)(nil), // 18: formance.payments.grpc.services.CreateWebhooksRequest (*CreateWebhooksResponse)(nil), // 19: formance.payments.grpc.services.CreateWebhooksResponse - (*TranslateWebhookResponse_Response)(nil), // 20: formance.payments.grpc.services.TranslateWebhookResponse.Response - (proto.Capability)(0), // 21: formance.payments.connectors.grpc.proto.Capability - (*proto.Workflow)(nil), // 22: formance.payments.connectors.grpc.proto.Workflow - (*proto.WebhookConfig)(nil), // 23: formance.payments.connectors.grpc.proto.WebhookConfig - (*proto.Other)(nil), // 24: formance.payments.connectors.grpc.proto.Other - (*proto.Payment)(nil), // 25: formance.payments.connectors.grpc.proto.Payment - (*proto.Account)(nil), // 26: formance.payments.connectors.grpc.proto.Account - (*proto.Balance)(nil), // 27: formance.payments.connectors.grpc.proto.Balance - (*proto.BankAccount)(nil), // 28: formance.payments.connectors.grpc.proto.BankAccount - (*proto.Webhook)(nil), // 29: formance.payments.connectors.grpc.proto.Webhook + (*CreateTransferRequest)(nil), // 20: formance.payments.grpc.services.CreateTransferRequest + (*CreateTransferResponse)(nil), // 21: formance.payments.grpc.services.CreateTransferResponse + (*CreatePayoutRequest)(nil), // 22: formance.payments.grpc.services.CreatePayoutRequest + (*CreatePayoutResponse)(nil), // 23: formance.payments.grpc.services.CreatePayoutResponse + (*TranslateWebhookResponse_Response)(nil), // 24: formance.payments.grpc.services.TranslateWebhookResponse.Response + (proto.Capability)(0), // 25: formance.payments.connectors.grpc.proto.Capability + (*proto.Workflow)(nil), // 26: formance.payments.connectors.grpc.proto.Workflow + (*proto.WebhookConfig)(nil), // 27: formance.payments.connectors.grpc.proto.WebhookConfig + (*proto.Other)(nil), // 28: formance.payments.connectors.grpc.proto.Other + (*proto.Payment)(nil), // 29: formance.payments.connectors.grpc.proto.Payment + (*proto.Account)(nil), // 30: formance.payments.connectors.grpc.proto.Account + (*proto.Balance)(nil), // 31: formance.payments.connectors.grpc.proto.Balance + (*proto.BankAccount)(nil), // 32: formance.payments.connectors.grpc.proto.BankAccount + (*proto.Webhook)(nil), // 33: formance.payments.connectors.grpc.proto.Webhook + (*proto.PaymentInitiation)(nil), // 34: formance.payments.connectors.grpc.proto.PaymentInitiation } var file_services_plugin_proto_depIdxs = []int32{ - 21, // 0: formance.payments.grpc.services.InstallResponse.capabilities:type_name -> formance.payments.connectors.grpc.proto.Capability - 22, // 1: formance.payments.grpc.services.InstallResponse.workflow:type_name -> formance.payments.connectors.grpc.proto.Workflow - 23, // 2: formance.payments.grpc.services.InstallResponse.webhooks_configs:type_name -> formance.payments.connectors.grpc.proto.WebhookConfig - 24, // 3: formance.payments.grpc.services.FetchNextOthersResponse.others:type_name -> formance.payments.connectors.grpc.proto.Other - 25, // 4: formance.payments.grpc.services.FetchNextPaymentsResponse.payments:type_name -> formance.payments.connectors.grpc.proto.Payment - 26, // 5: formance.payments.grpc.services.FetchNextAccountsResponse.accounts:type_name -> formance.payments.connectors.grpc.proto.Account - 26, // 6: formance.payments.grpc.services.FetchNextExternalAccountsResponse.accounts:type_name -> formance.payments.connectors.grpc.proto.Account - 27, // 7: formance.payments.grpc.services.FetchNextBalancesResponse.balances:type_name -> formance.payments.connectors.grpc.proto.Balance - 28, // 8: formance.payments.grpc.services.CreateBankAccountRequest.bank_account:type_name -> formance.payments.connectors.grpc.proto.BankAccount - 26, // 9: formance.payments.grpc.services.CreateBankAccountResponse.related_account:type_name -> formance.payments.connectors.grpc.proto.Account - 29, // 10: formance.payments.grpc.services.TranslateWebhookRequest.webhook:type_name -> formance.payments.connectors.grpc.proto.Webhook - 20, // 11: formance.payments.grpc.services.TranslateWebhookResponse.responses:type_name -> formance.payments.grpc.services.TranslateWebhookResponse.Response - 24, // 12: formance.payments.grpc.services.CreateWebhooksResponse.others:type_name -> formance.payments.connectors.grpc.proto.Other - 26, // 13: formance.payments.grpc.services.TranslateWebhookResponse.Response.account:type_name -> formance.payments.connectors.grpc.proto.Account - 26, // 14: formance.payments.grpc.services.TranslateWebhookResponse.Response.external_account:type_name -> formance.payments.connectors.grpc.proto.Account - 25, // 15: formance.payments.grpc.services.TranslateWebhookResponse.Response.payment:type_name -> formance.payments.connectors.grpc.proto.Payment - 27, // 16: formance.payments.grpc.services.TranslateWebhookResponse.Response.balance:type_name -> formance.payments.connectors.grpc.proto.Balance - 0, // 17: formance.payments.grpc.services.Plugin.Install:input_type -> formance.payments.grpc.services.InstallRequest - 2, // 18: formance.payments.grpc.services.Plugin.Uninstall:input_type -> formance.payments.grpc.services.UninstallRequest - 4, // 19: formance.payments.grpc.services.Plugin.FetchNextOthers:input_type -> formance.payments.grpc.services.FetchNextOthersRequest - 6, // 20: formance.payments.grpc.services.Plugin.FetchNextPayments:input_type -> formance.payments.grpc.services.FetchNextPaymentsRequest - 8, // 21: formance.payments.grpc.services.Plugin.FetchNextAccounts:input_type -> formance.payments.grpc.services.FetchNextAccountsRequest - 10, // 22: formance.payments.grpc.services.Plugin.FetchNextExternalAccounts:input_type -> formance.payments.grpc.services.FetchNextExternalAccountsRequest - 12, // 23: formance.payments.grpc.services.Plugin.FetchNextBalances:input_type -> formance.payments.grpc.services.FetchNextBalancesRequest - 14, // 24: formance.payments.grpc.services.Plugin.CreateBankAccount:input_type -> formance.payments.grpc.services.CreateBankAccountRequest - 18, // 25: formance.payments.grpc.services.Plugin.CreateWebhooks:input_type -> formance.payments.grpc.services.CreateWebhooksRequest - 16, // 26: formance.payments.grpc.services.Plugin.TranslateWebhook:input_type -> formance.payments.grpc.services.TranslateWebhookRequest - 1, // 27: formance.payments.grpc.services.Plugin.Install:output_type -> formance.payments.grpc.services.InstallResponse - 3, // 28: formance.payments.grpc.services.Plugin.Uninstall:output_type -> formance.payments.grpc.services.UninstallResponse - 5, // 29: formance.payments.grpc.services.Plugin.FetchNextOthers:output_type -> formance.payments.grpc.services.FetchNextOthersResponse - 7, // 30: formance.payments.grpc.services.Plugin.FetchNextPayments:output_type -> formance.payments.grpc.services.FetchNextPaymentsResponse - 9, // 31: formance.payments.grpc.services.Plugin.FetchNextAccounts:output_type -> formance.payments.grpc.services.FetchNextAccountsResponse - 11, // 32: formance.payments.grpc.services.Plugin.FetchNextExternalAccounts:output_type -> formance.payments.grpc.services.FetchNextExternalAccountsResponse - 13, // 33: formance.payments.grpc.services.Plugin.FetchNextBalances:output_type -> formance.payments.grpc.services.FetchNextBalancesResponse - 15, // 34: formance.payments.grpc.services.Plugin.CreateBankAccount:output_type -> formance.payments.grpc.services.CreateBankAccountResponse - 19, // 35: formance.payments.grpc.services.Plugin.CreateWebhooks:output_type -> formance.payments.grpc.services.CreateWebhooksResponse - 17, // 36: formance.payments.grpc.services.Plugin.TranslateWebhook:output_type -> formance.payments.grpc.services.TranslateWebhookResponse - 27, // [27:37] is the sub-list for method output_type - 17, // [17:27] is the sub-list for method input_type - 17, // [17:17] is the sub-list for extension type_name - 17, // [17:17] is the sub-list for extension extendee - 0, // [0:17] is the sub-list for field type_name + 25, // 0: formance.payments.grpc.services.InstallResponse.capabilities:type_name -> formance.payments.connectors.grpc.proto.Capability + 26, // 1: formance.payments.grpc.services.InstallResponse.workflow:type_name -> formance.payments.connectors.grpc.proto.Workflow + 27, // 2: formance.payments.grpc.services.InstallResponse.webhooks_configs:type_name -> formance.payments.connectors.grpc.proto.WebhookConfig + 28, // 3: formance.payments.grpc.services.FetchNextOthersResponse.others:type_name -> formance.payments.connectors.grpc.proto.Other + 29, // 4: formance.payments.grpc.services.FetchNextPaymentsResponse.payments:type_name -> formance.payments.connectors.grpc.proto.Payment + 30, // 5: formance.payments.grpc.services.FetchNextAccountsResponse.accounts:type_name -> formance.payments.connectors.grpc.proto.Account + 30, // 6: formance.payments.grpc.services.FetchNextExternalAccountsResponse.accounts:type_name -> formance.payments.connectors.grpc.proto.Account + 31, // 7: formance.payments.grpc.services.FetchNextBalancesResponse.balances:type_name -> formance.payments.connectors.grpc.proto.Balance + 32, // 8: formance.payments.grpc.services.CreateBankAccountRequest.bank_account:type_name -> formance.payments.connectors.grpc.proto.BankAccount + 30, // 9: formance.payments.grpc.services.CreateBankAccountResponse.related_account:type_name -> formance.payments.connectors.grpc.proto.Account + 33, // 10: formance.payments.grpc.services.TranslateWebhookRequest.webhook:type_name -> formance.payments.connectors.grpc.proto.Webhook + 24, // 11: formance.payments.grpc.services.TranslateWebhookResponse.responses:type_name -> formance.payments.grpc.services.TranslateWebhookResponse.Response + 28, // 12: formance.payments.grpc.services.CreateWebhooksResponse.others:type_name -> formance.payments.connectors.grpc.proto.Other + 34, // 13: formance.payments.grpc.services.CreateTransferRequest.payment_initiation:type_name -> formance.payments.connectors.grpc.proto.PaymentInitiation + 29, // 14: formance.payments.grpc.services.CreateTransferResponse.payment:type_name -> formance.payments.connectors.grpc.proto.Payment + 34, // 15: formance.payments.grpc.services.CreatePayoutRequest.payment_initiation:type_name -> formance.payments.connectors.grpc.proto.PaymentInitiation + 29, // 16: formance.payments.grpc.services.CreatePayoutResponse.payment:type_name -> formance.payments.connectors.grpc.proto.Payment + 30, // 17: formance.payments.grpc.services.TranslateWebhookResponse.Response.account:type_name -> formance.payments.connectors.grpc.proto.Account + 30, // 18: formance.payments.grpc.services.TranslateWebhookResponse.Response.external_account:type_name -> formance.payments.connectors.grpc.proto.Account + 29, // 19: formance.payments.grpc.services.TranslateWebhookResponse.Response.payment:type_name -> formance.payments.connectors.grpc.proto.Payment + 31, // 20: formance.payments.grpc.services.TranslateWebhookResponse.Response.balance:type_name -> formance.payments.connectors.grpc.proto.Balance + 0, // 21: formance.payments.grpc.services.Plugin.Install:input_type -> formance.payments.grpc.services.InstallRequest + 2, // 22: formance.payments.grpc.services.Plugin.Uninstall:input_type -> formance.payments.grpc.services.UninstallRequest + 4, // 23: formance.payments.grpc.services.Plugin.FetchNextOthers:input_type -> formance.payments.grpc.services.FetchNextOthersRequest + 6, // 24: formance.payments.grpc.services.Plugin.FetchNextPayments:input_type -> formance.payments.grpc.services.FetchNextPaymentsRequest + 8, // 25: formance.payments.grpc.services.Plugin.FetchNextAccounts:input_type -> formance.payments.grpc.services.FetchNextAccountsRequest + 10, // 26: formance.payments.grpc.services.Plugin.FetchNextExternalAccounts:input_type -> formance.payments.grpc.services.FetchNextExternalAccountsRequest + 12, // 27: formance.payments.grpc.services.Plugin.FetchNextBalances:input_type -> formance.payments.grpc.services.FetchNextBalancesRequest + 14, // 28: formance.payments.grpc.services.Plugin.CreateBankAccount:input_type -> formance.payments.grpc.services.CreateBankAccountRequest + 20, // 29: formance.payments.grpc.services.Plugin.CreateTransfer:input_type -> formance.payments.grpc.services.CreateTransferRequest + 22, // 30: formance.payments.grpc.services.Plugin.CreatePayout:input_type -> formance.payments.grpc.services.CreatePayoutRequest + 18, // 31: formance.payments.grpc.services.Plugin.CreateWebhooks:input_type -> formance.payments.grpc.services.CreateWebhooksRequest + 16, // 32: formance.payments.grpc.services.Plugin.TranslateWebhook:input_type -> formance.payments.grpc.services.TranslateWebhookRequest + 1, // 33: formance.payments.grpc.services.Plugin.Install:output_type -> formance.payments.grpc.services.InstallResponse + 3, // 34: formance.payments.grpc.services.Plugin.Uninstall:output_type -> formance.payments.grpc.services.UninstallResponse + 5, // 35: formance.payments.grpc.services.Plugin.FetchNextOthers:output_type -> formance.payments.grpc.services.FetchNextOthersResponse + 7, // 36: formance.payments.grpc.services.Plugin.FetchNextPayments:output_type -> formance.payments.grpc.services.FetchNextPaymentsResponse + 9, // 37: formance.payments.grpc.services.Plugin.FetchNextAccounts:output_type -> formance.payments.grpc.services.FetchNextAccountsResponse + 11, // 38: formance.payments.grpc.services.Plugin.FetchNextExternalAccounts:output_type -> formance.payments.grpc.services.FetchNextExternalAccountsResponse + 13, // 39: formance.payments.grpc.services.Plugin.FetchNextBalances:output_type -> formance.payments.grpc.services.FetchNextBalancesResponse + 15, // 40: formance.payments.grpc.services.Plugin.CreateBankAccount:output_type -> formance.payments.grpc.services.CreateBankAccountResponse + 21, // 41: formance.payments.grpc.services.Plugin.CreateTransfer:output_type -> formance.payments.grpc.services.CreateTransferResponse + 23, // 42: formance.payments.grpc.services.Plugin.CreatePayout:output_type -> formance.payments.grpc.services.CreatePayoutResponse + 19, // 43: formance.payments.grpc.services.Plugin.CreateWebhooks:output_type -> formance.payments.grpc.services.CreateWebhooksResponse + 17, // 44: formance.payments.grpc.services.Plugin.TranslateWebhook:output_type -> formance.payments.grpc.services.TranslateWebhookResponse + 33, // [33:45] is the sub-list for method output_type + 21, // [21:33] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_services_plugin_proto_init() } @@ -1908,6 +2156,54 @@ func file_services_plugin_proto_init() { } } file_services_plugin_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateTransferRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreateTransferResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreatePayoutRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CreatePayoutResponse); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_services_plugin_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*TranslateWebhookResponse_Response); i { case 0: return &v.state @@ -1920,7 +2216,7 @@ func file_services_plugin_proto_init() { } } } - file_services_plugin_proto_msgTypes[20].OneofWrappers = []interface{}{ + file_services_plugin_proto_msgTypes[24].OneofWrappers = []interface{}{ (*TranslateWebhookResponse_Response_Account)(nil), (*TranslateWebhookResponse_Response_ExternalAccount)(nil), (*TranslateWebhookResponse_Response_Payment)(nil), @@ -1932,7 +2228,7 @@ func file_services_plugin_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_services_plugin_proto_rawDesc, NumEnums: 0, - NumMessages: 21, + NumMessages: 25, NumExtensions: 0, NumServices: 1, }, diff --git a/internal/connectors/grpc/proto/services/plugin.proto b/internal/connectors/grpc/proto/services/plugin.proto index 5648ef69..ca598d41 100644 --- a/internal/connectors/grpc/proto/services/plugin.proto +++ b/internal/connectors/grpc/proto/services/plugin.proto @@ -8,6 +8,7 @@ import "bank_account.proto"; import "capability.proto"; import "other.proto"; import "payment.proto"; +import "payment_initiation.proto"; import "webhook.proto"; import "workflow.proto"; @@ -123,6 +124,22 @@ message CreateWebhooksResponse { repeated formance.payments.connectors.grpc.proto.Other others = 1; } +message CreateTransferRequest { + formance.payments.connectors.grpc.proto.PaymentInitiation payment_initiation = 1; +} + +message CreateTransferResponse { + formance.payments.connectors.grpc.proto.Payment payment = 1; +} + +message CreatePayoutRequest { + formance.payments.connectors.grpc.proto.PaymentInitiation payment_initiation = 1; +} + +message CreatePayoutResponse { + formance.payments.connectors.grpc.proto.Payment payment = 1; +} + service Plugin { rpc Install(InstallRequest) returns (InstallResponse) {} rpc Uninstall(UninstallRequest) returns (UninstallResponse) {} @@ -133,6 +150,8 @@ service Plugin { rpc FetchNextExternalAccounts(FetchNextExternalAccountsRequest) returns (FetchNextExternalAccountsResponse) {} rpc FetchNextBalances(FetchNextBalancesRequest) returns (FetchNextBalancesResponse) {} rpc CreateBankAccount(CreateBankAccountRequest) returns (CreateBankAccountResponse) {} + rpc CreateTransfer(CreateTransferRequest) returns (CreateTransferResponse) {} + rpc CreatePayout(CreatePayoutRequest) returns (CreatePayoutResponse) {} rpc CreateWebhooks(CreateWebhooksRequest) returns (CreateWebhooksResponse) {} rpc TranslateWebhook(TranslateWebhookRequest) returns (TranslateWebhookResponse) {} } \ No newline at end of file diff --git a/internal/connectors/grpc/proto/services/plugin_grpc.pb.go b/internal/connectors/grpc/proto/services/plugin_grpc.pb.go index abe79e06..469a4f02 100644 --- a/internal/connectors/grpc/proto/services/plugin_grpc.pb.go +++ b/internal/connectors/grpc/proto/services/plugin_grpc.pb.go @@ -26,6 +26,8 @@ type PluginClient interface { FetchNextExternalAccounts(ctx context.Context, in *FetchNextExternalAccountsRequest, opts ...grpc.CallOption) (*FetchNextExternalAccountsResponse, error) FetchNextBalances(ctx context.Context, in *FetchNextBalancesRequest, opts ...grpc.CallOption) (*FetchNextBalancesResponse, error) CreateBankAccount(ctx context.Context, in *CreateBankAccountRequest, opts ...grpc.CallOption) (*CreateBankAccountResponse, error) + CreateTransfer(ctx context.Context, in *CreateTransferRequest, opts ...grpc.CallOption) (*CreateTransferResponse, error) + CreatePayout(ctx context.Context, in *CreatePayoutRequest, opts ...grpc.CallOption) (*CreatePayoutResponse, error) CreateWebhooks(ctx context.Context, in *CreateWebhooksRequest, opts ...grpc.CallOption) (*CreateWebhooksResponse, error) TranslateWebhook(ctx context.Context, in *TranslateWebhookRequest, opts ...grpc.CallOption) (*TranslateWebhookResponse, error) } @@ -110,6 +112,24 @@ func (c *pluginClient) CreateBankAccount(ctx context.Context, in *CreateBankAcco return out, nil } +func (c *pluginClient) CreateTransfer(ctx context.Context, in *CreateTransferRequest, opts ...grpc.CallOption) (*CreateTransferResponse, error) { + out := new(CreateTransferResponse) + err := c.cc.Invoke(ctx, "/formance.payments.grpc.services.Plugin/CreateTransfer", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *pluginClient) CreatePayout(ctx context.Context, in *CreatePayoutRequest, opts ...grpc.CallOption) (*CreatePayoutResponse, error) { + out := new(CreatePayoutResponse) + err := c.cc.Invoke(ctx, "/formance.payments.grpc.services.Plugin/CreatePayout", in, out, opts...) + if err != nil { + return nil, err + } + return out, nil +} + func (c *pluginClient) CreateWebhooks(ctx context.Context, in *CreateWebhooksRequest, opts ...grpc.CallOption) (*CreateWebhooksResponse, error) { out := new(CreateWebhooksResponse) err := c.cc.Invoke(ctx, "/formance.payments.grpc.services.Plugin/CreateWebhooks", in, out, opts...) @@ -140,6 +160,8 @@ type PluginServer interface { FetchNextExternalAccounts(context.Context, *FetchNextExternalAccountsRequest) (*FetchNextExternalAccountsResponse, error) FetchNextBalances(context.Context, *FetchNextBalancesRequest) (*FetchNextBalancesResponse, error) CreateBankAccount(context.Context, *CreateBankAccountRequest) (*CreateBankAccountResponse, error) + CreateTransfer(context.Context, *CreateTransferRequest) (*CreateTransferResponse, error) + CreatePayout(context.Context, *CreatePayoutRequest) (*CreatePayoutResponse, error) CreateWebhooks(context.Context, *CreateWebhooksRequest) (*CreateWebhooksResponse, error) TranslateWebhook(context.Context, *TranslateWebhookRequest) (*TranslateWebhookResponse, error) mustEmbedUnimplementedPluginServer() @@ -173,6 +195,12 @@ func (UnimplementedPluginServer) FetchNextBalances(context.Context, *FetchNextBa func (UnimplementedPluginServer) CreateBankAccount(context.Context, *CreateBankAccountRequest) (*CreateBankAccountResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateBankAccount not implemented") } +func (UnimplementedPluginServer) CreateTransfer(context.Context, *CreateTransferRequest) (*CreateTransferResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreateTransfer not implemented") +} +func (UnimplementedPluginServer) CreatePayout(context.Context, *CreatePayoutRequest) (*CreatePayoutResponse, error) { + return nil, status.Errorf(codes.Unimplemented, "method CreatePayout not implemented") +} func (UnimplementedPluginServer) CreateWebhooks(context.Context, *CreateWebhooksRequest) (*CreateWebhooksResponse, error) { return nil, status.Errorf(codes.Unimplemented, "method CreateWebhooks not implemented") } @@ -336,6 +364,42 @@ func _Plugin_CreateBankAccount_Handler(srv interface{}, ctx context.Context, dec return interceptor(ctx, in, info, handler) } +func _Plugin_CreateTransfer_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateTransferRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).CreateTransfer(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/formance.payments.grpc.services.Plugin/CreateTransfer", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).CreateTransfer(ctx, req.(*CreateTransferRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _Plugin_CreatePayout_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreatePayoutRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PluginServer).CreatePayout(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/formance.payments.grpc.services.Plugin/CreatePayout", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PluginServer).CreatePayout(ctx, req.(*CreatePayoutRequest)) + } + return interceptor(ctx, in, info, handler) +} + func _Plugin_CreateWebhooks_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(CreateWebhooksRequest) if err := dec(in); err != nil { @@ -411,6 +475,14 @@ var Plugin_ServiceDesc = grpc.ServiceDesc{ MethodName: "CreateBankAccount", Handler: _Plugin_CreateBankAccount_Handler, }, + { + MethodName: "CreateTransfer", + Handler: _Plugin_CreateTransfer_Handler, + }, + { + MethodName: "CreatePayout", + Handler: _Plugin_CreatePayout_Handler, + }, { MethodName: "CreateWebhooks", Handler: _Plugin_CreateWebhooks_Handler, diff --git a/internal/connectors/grpc/translate.go b/internal/connectors/grpc/translate.go index 91811a4f..6a2c49ac 100644 --- a/internal/connectors/grpc/translate.go +++ b/internal/connectors/grpc/translate.go @@ -219,6 +219,63 @@ func TranslateProtoPayment(payment *proto.Payment) (models.PSPPayment, error) { }, nil } +func TranslatePaymentInitiation(pi models.PSPPaymentInitiation) *proto.PaymentInitiation { + return &proto.PaymentInitiation{ + Reference: pi.Reference, + CreatedAt: timestamppb.New(pi.CreatedAt), + Description: pi.Description, + SourceAccount: func() *proto.Account { + if pi.SourceAccount == nil { + return nil + } + + return TranslateAccount(*pi.SourceAccount) + }(), + DestinationAccount: func() *proto.Account { + if pi.DestinationAccount == nil { + return nil + } + + return TranslateAccount(*pi.DestinationAccount) + }(), + Amount: &proto.Monetary{ + Asset: pi.Asset, + Amount: []byte(pi.Amount.Text(10)), + }, + Metadata: pi.Metadata, + } +} + +func TranslateProtoPaymentInitiation(pi *proto.PaymentInitiation) (models.PSPPaymentInitiation, error) { + amount, ok := big.NewInt(0).SetString(string(pi.Amount.Amount), 10) + if !ok { + return models.PSPPaymentInitiation{}, errors.New("failed to parse amount") + } + + return models.PSPPaymentInitiation{ + Reference: pi.Reference, + CreatedAt: pi.CreatedAt.AsTime(), + Description: pi.Description, + SourceAccount: func() *models.PSPAccount { + if pi.SourceAccount == nil { + return nil + } + + return pointer.For(TranslateProtoAccount(pi.SourceAccount)) + }(), + DestinationAccount: func() *models.PSPAccount { + if pi.DestinationAccount == nil { + return nil + } + + return pointer.For(TranslateProtoAccount(pi.DestinationAccount)) + }(), + Amount: amount, + Asset: pi.Amount.Asset, + Metadata: pi.Metadata, + }, nil +} + func TranslateTask(taskTree models.TaskTree) *proto.TaskTree { res := proto.TaskTree{ NextTasks: []*proto.TaskTree{}, diff --git a/internal/connectors/plugins/errors.go b/internal/connectors/plugins/errors.go index 127ea696..aa112649 100644 --- a/internal/connectors/plugins/errors.go +++ b/internal/connectors/plugins/errors.go @@ -66,7 +66,8 @@ func translateErrorToGRPC(err error) error { code = codes.Unimplemented reason = FailureReasonUnimplemented case errors.Is(err, models.ErrMissingFromPayloadInRequest), - errors.Is(err, models.ErrMissingAccountInMetadata): + errors.Is(err, models.ErrMissingAccountInMetadata), + errors.Is(err, models.ErrInvalidRequest): code = codes.FailedPrecondition reason = FailureReasonInvalidRequest case errors.Is(err, models.ErrInvalidConfig): diff --git a/internal/connectors/plugins/grpc.go b/internal/connectors/plugins/grpc.go index 4e1a6bb5..f286a9ff 100644 --- a/internal/connectors/plugins/grpc.go +++ b/internal/connectors/plugins/grpc.go @@ -235,6 +235,54 @@ func (i *impl) CreateBankAccount(ctx context.Context, req *services.CreateBankAc }, nil } +func (i *impl) CreateTransfer(ctx context.Context, req *services.CreateTransferRequest) (*services.CreateTransferResponse, error) { + hclog.Default().Info("creating transfer...") + + pi, err := grpc.TranslateProtoPaymentInitiation(req.PaymentInitiation) + if err != nil { + hclog.Default().Error("creating transfer failed: ", err) + return nil, translateErrorToGRPC(err) + } + + resp, err := i.plugin.CreateTransfer(ctx, models.CreateTransferRequest{ + PaymentInitiation: pi, + }) + if err != nil { + hclog.Default().Error("creating transfer failed: ", err) + return nil, translateErrorToGRPC(err) + } + + hclog.Default().Info("created transfer succeeded!") + + return &services.CreateTransferResponse{ + Payment: grpc.TranslatePayment(resp.Payment), + }, nil +} + +func (i *impl) CreatePayout(ctx context.Context, req *services.CreatePayoutRequest) (*services.CreatePayoutResponse, error) { + hclog.Default().Info("creating payout...") + + pi, err := grpc.TranslateProtoPaymentInitiation(req.PaymentInitiation) + if err != nil { + hclog.Default().Error("creating payout failed: ", err) + return nil, translateErrorToGRPC(err) + } + + resp, err := i.plugin.CreatePayout(ctx, models.CreatePayoutRequest{ + PaymentInitiation: pi, + }) + if err != nil { + hclog.Default().Error("creating payout failed: ", err) + return nil, translateErrorToGRPC(err) + } + + hclog.Default().Info("created payout succeeded!") + + return &services.CreatePayoutResponse{ + Payment: grpc.TranslatePayment(resp.Payment), + }, nil +} + func (i *impl) CreateWebhooks(ctx context.Context, req *services.CreateWebhooksRequest) (*services.CreateWebhooksResponse, error) { hclog.Default().Info("creating webhooks...") diff --git a/internal/connectors/plugins/public/adyen/plugin.go b/internal/connectors/plugins/public/adyen/plugin.go index 041df12c..a631dd7c 100644 --- a/internal/connectors/plugins/public/adyen/plugin.go +++ b/internal/connectors/plugins/public/adyen/plugin.go @@ -79,6 +79,14 @@ func (p *Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAcc return models.CreateBankAccountResponse{}, plugins.ErrNotImplemented } +func (p Plugin) CreateTransfer(ctx context.Context, req models.CreateTransferRequest) (models.CreateTransferResponse, error) { + return models.CreateTransferResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreatePayout(ctx context.Context, req models.CreatePayoutRequest) (models.CreatePayoutResponse, error) { + return models.CreatePayoutResponse{}, plugins.ErrNotImplemented +} + func (p *Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { if p.client == nil { return models.CreateWebhooksResponse{}, plugins.ErrNotYetInstalled diff --git a/internal/connectors/plugins/public/bankingcircle/plugin.go b/internal/connectors/plugins/public/bankingcircle/plugin.go index 665e2673..5c23b88b 100644 --- a/internal/connectors/plugins/public/bankingcircle/plugin.go +++ b/internal/connectors/plugins/public/bankingcircle/plugin.go @@ -71,6 +71,14 @@ func (p Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAcco return p.createBankAccount(ctx, req) } +func (p Plugin) CreateTransfer(ctx context.Context, req models.CreateTransferRequest) (models.CreateTransferResponse, error) { + return models.CreateTransferResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreatePayout(ctx context.Context, req models.CreatePayoutRequest) (models.CreatePayoutResponse, error) { + return models.CreatePayoutResponse{}, plugins.ErrNotImplemented +} + func (p Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { return models.CreateWebhooksResponse{}, plugins.ErrNotImplemented } diff --git a/internal/connectors/plugins/public/currencycloud/plugin.go b/internal/connectors/plugins/public/currencycloud/plugin.go index 5c993eb0..51fe9f80 100644 --- a/internal/connectors/plugins/public/currencycloud/plugin.go +++ b/internal/connectors/plugins/public/currencycloud/plugin.go @@ -71,6 +71,14 @@ func (p Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAcco return models.CreateBankAccountResponse{}, plugins.ErrNotImplemented } +func (p Plugin) CreateTransfer(ctx context.Context, req models.CreateTransferRequest) (models.CreateTransferResponse, error) { + return models.CreateTransferResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreatePayout(ctx context.Context, req models.CreatePayoutRequest) (models.CreatePayoutResponse, error) { + return models.CreatePayoutResponse{}, plugins.ErrNotImplemented +} + func (p Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { return models.CreateWebhooksResponse{}, plugins.ErrNotImplemented } diff --git a/internal/connectors/plugins/public/generic/plugin.go b/internal/connectors/plugins/public/generic/plugin.go index 1b6f78dc..33ffd6a5 100644 --- a/internal/connectors/plugins/public/generic/plugin.go +++ b/internal/connectors/plugins/public/generic/plugin.go @@ -67,6 +67,14 @@ func (p Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAcco return models.CreateBankAccountResponse{}, plugins.ErrNotImplemented } +func (p Plugin) CreateTransfer(ctx context.Context, req models.CreateTransferRequest) (models.CreateTransferResponse, error) { + return models.CreateTransferResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreatePayout(ctx context.Context, req models.CreatePayoutRequest) (models.CreatePayoutResponse, error) { + return models.CreatePayoutResponse{}, plugins.ErrNotImplemented +} + func (p Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { return models.CreateWebhooksResponse{}, plugins.ErrNotImplemented } diff --git a/internal/connectors/plugins/public/mangopay/accounts.go b/internal/connectors/plugins/public/mangopay/accounts.go index e2884534..1da38b25 100644 --- a/internal/connectors/plugins/public/mangopay/accounts.go +++ b/internal/connectors/plugins/public/mangopay/accounts.go @@ -75,7 +75,7 @@ func (p Plugin) fetchNextAccounts(ctx context.Context, req models.FetchNextAccou Name: &account.Description, DefaultAsset: pointer.For(currency.FormatAsset(supportedCurrenciesWithDecimal, account.Currency)), Metadata: map[string]string{ - "user_id": from.ID, + userIDMetadataKey: from.ID, }, Raw: raw, }) diff --git a/internal/connectors/plugins/public/mangopay/client/payout.go b/internal/connectors/plugins/public/mangopay/client/payout.go index ec390439..36df79be 100644 --- a/internal/connectors/plugins/public/mangopay/client/payout.go +++ b/internal/connectors/plugins/public/mangopay/client/payout.go @@ -11,6 +11,7 @@ import ( ) type PayoutRequest struct { + Reference string `json:"-"` // Needed for idempotency AuthorID string `json:"AuthorId"` DebitedFunds Funds `json:"DebitedFunds"` Fees Funds `json:"Fees"` @@ -60,6 +61,7 @@ func (c *Client) InitiatePayout(ctx context.Context, payoutRequest *PayoutReques return nil, fmt.Errorf("failed to create login request: %w", err) } req.Header.Set("Content-Type", "application/json") + req.Header.Set("Idempotency-Key", payoutRequest.Reference) var payoutResponse PayoutResponse statusCode, err := c.httpClient.Do(req, &payoutResponse, nil) diff --git a/internal/connectors/plugins/public/mangopay/client/transfer.go b/internal/connectors/plugins/public/mangopay/client/transfer.go index 5a5dc22a..5aac42f0 100644 --- a/internal/connectors/plugins/public/mangopay/client/transfer.go +++ b/internal/connectors/plugins/public/mangopay/client/transfer.go @@ -1,6 +1,7 @@ package client import ( + "bytes" "context" "encoding/json" "fmt" @@ -15,6 +16,7 @@ type Funds struct { } type TransferRequest struct { + Reference string `json:"-"` // Needed for idempotency AuthorID string `json:"AuthorId"` CreditedUserID string `json:"CreditedUserId,omitempty"` DebitedFunds Funds `json:"DebitedFunds"` @@ -41,6 +43,36 @@ type TransferResponse struct { CreditedWalletID string `json:"CreditedWalletId"` } +func (c *Client) InitiateWalletTransfer(ctx context.Context, transferRequest *TransferRequest) (*TransferResponse, error) { + // TODO(polo): metrics + // f := connectors.ClientMetrics(ctx, "mangopay", "initiate_transfer") + // now := time.Now() + // defer f(ctx, now) + + endpoint := fmt.Sprintf("%s/v2.01/%s/transfers", c.endpoint, c.clientID) + + body, err := json.Marshal(transferRequest) + if err != nil { + return nil, fmt.Errorf("failed to marshal transfer request: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewBuffer(body)) + if err != nil { + return nil, fmt.Errorf("failed to create transfer request: %w", err) + } + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Idempotency-Key", transferRequest.Reference) + + var transferResponse TransferResponse + var errRes mangopayError + statusCode, err := c.httpClient.Do(req, &transferResponse, &errRes) + if err != nil { + return nil, errorsutils.NewErrorWithExitCode(fmt.Errorf("failed to get wallets: %w %w", err, errRes.Error()), statusCode) + } + + return &transferResponse, nil +} + func (c *Client) GetWalletTransfer(ctx context.Context, transferID string) (TransferResponse, error) { // TODO(polo): metrics // f := connectors.ClientMetrics(ctx, "mangopay", "get_transfer") diff --git a/internal/connectors/plugins/public/mangopay/metadata.go b/internal/connectors/plugins/public/mangopay/metadata.go new file mode 100644 index 00000000..bd4a1355 --- /dev/null +++ b/internal/connectors/plugins/public/mangopay/metadata.go @@ -0,0 +1,5 @@ +package mangopay + +const ( + userIDMetadataKey = "userID" +) diff --git a/internal/connectors/plugins/public/mangopay/payouts.go b/internal/connectors/plugins/public/mangopay/payouts.go new file mode 100644 index 00000000..e0cb52ce --- /dev/null +++ b/internal/connectors/plugins/public/mangopay/payouts.go @@ -0,0 +1,109 @@ +package mangopay + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + "regexp" + "time" + + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/connectors/plugins/public/mangopay/client" + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" +) + +var ( + bankWireRefPatternRegexp = regexp.MustCompile("[a-zA-Z0-9 ]*") +) + +func (p *Plugin) validatePayoutRequest(pi models.PSPPaymentInitiation) error { + _, err := uuid.Parse(pi.Reference) + if err != nil { + return fmt.Errorf("reference is required as an uuid: %w", models.ErrInvalidRequest) + } + + if pi.SourceAccount == nil { + return models.ErrInvalidRequest + } + + if pi.DestinationAccount == nil { + return models.ErrInvalidRequest + } + + _, ok := pi.SourceAccount.Metadata[userIDMetadataKey] + if !ok { + return fmt.Errorf("source account metadata with user id is required: %w", models.ErrInvalidRequest) + } + + if len(pi.Description) > 12 || !bankWireRefPatternRegexp.MatchString(pi.Description) { + return fmt.Errorf("description must be alphanumeric and less than 12 characters: %w", models.ErrInvalidRequest) + } + + return nil +} + +func (p *Plugin) createPayout(ctx context.Context, pi models.PSPPaymentInitiation) (models.PSPPayment, error) { + if err := p.validatePayoutRequest(pi); err != nil { + return models.PSPPayment{}, err + } + + userID := pi.SourceAccount.Metadata[userIDMetadataKey] + + curr, _, err := currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, pi.Asset) + if err != nil { + return models.PSPPayment{}, fmt.Errorf("failed to get currency and precision from asset: %v: %w", err, models.ErrInvalidRequest) + } + + resp, err := p.client.InitiatePayout(ctx, &client.PayoutRequest{ + AuthorID: userID, + DebitedFunds: client.Funds{ + Currency: curr, + Amount: json.Number(pi.Amount.String()), + }, + Fees: client.Funds{ + Currency: curr, + Amount: json.Number("0"), + }, + DebitedWalletID: pi.SourceAccount.Reference, + BankAccountID: pi.DestinationAccount.Reference, + BankWireRef: pi.Description, + }) + if err != nil { + return models.PSPPayment{}, err + } + + payment, err := FromPayoutToPayment(resp, pi.DestinationAccount.Reference) + if err != nil { + return models.PSPPayment{}, err + } + + return payment, nil +} + +func FromPayoutToPayment(from *client.PayoutResponse, destinationAccountReference string) (models.PSPPayment, error) { + raw, err := json.Marshal(from) + if err != nil { + return models.PSPPayment{}, err + } + + var amount big.Int + _, ok := amount.SetString(from.DebitedFunds.Amount.String(), 10) + if !ok { + return models.PSPPayment{}, fmt.Errorf("failed to parse amount %s", from.DebitedFunds.Amount.String()) + } + + return models.PSPPayment{ + Reference: from.ID, + CreatedAt: time.Unix(from.CreationDate, 0), + Type: models.PAYMENT_TYPE_PAYOUT, + Amount: &amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, from.DebitedFunds.Currency), + Scheme: models.PAYMENT_SCHEME_OTHER, + Status: matchPaymentStatus(from.Status), + SourceAccountReference: &from.DebitedWalletID, + DestinationAccountReference: &destinationAccountReference, + Raw: raw, + }, nil +} diff --git a/internal/connectors/plugins/public/mangopay/plugin.go b/internal/connectors/plugins/public/mangopay/plugin.go index a923b841..fe5a0c53 100644 --- a/internal/connectors/plugins/public/mangopay/plugin.go +++ b/internal/connectors/plugins/public/mangopay/plugin.go @@ -94,6 +94,35 @@ func (p Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAcco return p.createBankAccount(ctx, req) } +func (p Plugin) CreateTransfer(ctx context.Context, req models.CreateTransferRequest) (models.CreateTransferResponse, error) { + if p.client == nil { + return models.CreateTransferResponse{}, plugins.ErrNotYetInstalled + } + payment, err := p.createTransfer(ctx, req.PaymentInitiation) + if err != nil { + return models.CreateTransferResponse{}, err + } + + return models.CreateTransferResponse{ + Payment: payment, + }, nil +} + +func (p Plugin) CreatePayout(ctx context.Context, req models.CreatePayoutRequest) (models.CreatePayoutResponse, error) { + if p.client == nil { + return models.CreatePayoutResponse{}, plugins.ErrNotYetInstalled + } + + payment, err := p.createPayout(ctx, req.PaymentInitiation) + if err != nil { + return models.CreatePayoutResponse{}, err + } + + return models.CreatePayoutResponse{ + Payment: payment, + }, nil +} + func (p Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { if p.client == nil { return models.CreateWebhooksResponse{}, plugins.ErrNotYetInstalled diff --git a/internal/connectors/plugins/public/mangopay/transfers.go b/internal/connectors/plugins/public/mangopay/transfers.go new file mode 100644 index 00000000..7586a44b --- /dev/null +++ b/internal/connectors/plugins/public/mangopay/transfers.go @@ -0,0 +1,103 @@ +package mangopay + +import ( + "context" + "encoding/json" + "fmt" + "math/big" + "time" + + "github.com/formancehq/payments/internal/connectors/plugins/currency" + "github.com/formancehq/payments/internal/connectors/plugins/public/mangopay/client" + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" +) + +func (p *Plugin) validateTransferRequest(pi models.PSPPaymentInitiation) error { + _, err := uuid.Parse(pi.Reference) + if err != nil { + return fmt.Errorf("reference is required as an uuid: %w", models.ErrInvalidRequest) + } + + if pi.SourceAccount == nil { + return fmt.Errorf("source account is required: %w", models.ErrInvalidRequest) + } + + if pi.SourceAccount == nil { + return fmt.Errorf("destination account is required: %w", models.ErrInvalidRequest) + } + + _, ok := pi.SourceAccount.Metadata[userIDMetadataKey] + if !ok { + return fmt.Errorf("source account metadata with user id is required: %w", models.ErrInvalidRequest) + } + + return nil +} + +func (p *Plugin) createTransfer(ctx context.Context, pi models.PSPPaymentInitiation) (models.PSPPayment, error) { + if err := p.validateTransferRequest(pi); err != nil { + return models.PSPPayment{}, err + } + + userID := pi.SourceAccount.Metadata[userIDMetadataKey] + + curr, _, err := currency.GetCurrencyAndPrecisionFromAsset(supportedCurrenciesWithDecimal, pi.Asset) + if err != nil { + return models.PSPPayment{}, fmt.Errorf("failed to get currency and precision from asset: %v: %w", err, models.ErrInvalidRequest) + } + + resp, err := p.client.InitiateWalletTransfer( + ctx, + &client.TransferRequest{ + Reference: pi.Reference, + AuthorID: userID, + DebitedFunds: client.Funds{ + Currency: curr, + Amount: json.Number(pi.Amount.String()), + }, + Fees: client.Funds{ + Currency: curr, + Amount: "0", + }, + DebitedWalletID: pi.SourceAccount.Reference, + CreditedWalletID: pi.DestinationAccount.Reference, + }, + ) + if err != nil { + return models.PSPPayment{}, err + } + + payment, err := FromTransferToPayment(resp) + if err != nil { + return models.PSPPayment{}, err + } + + return payment, nil +} + +func FromTransferToPayment(from *client.TransferResponse) (models.PSPPayment, error) { + raw, err := json.Marshal(from) + if err != nil { + return models.PSPPayment{}, err + } + + var amount big.Int + _, ok := amount.SetString(from.DebitedFunds.Amount.String(), 10) + if !ok { + return models.PSPPayment{}, fmt.Errorf("failed to parse amount %s", from.DebitedFunds.Amount.String()) + } + + return models.PSPPayment{ + Reference: from.ID, + CreatedAt: time.Unix(from.CreationDate, 0), + Type: models.PAYMENT_TYPE_TRANSFER, + Amount: &amount, + Asset: currency.FormatAsset(supportedCurrenciesWithDecimal, from.DebitedFunds.Currency), + Scheme: models.PAYMENT_SCHEME_OTHER, + Status: matchPaymentStatus(from.Status), + SourceAccountReference: &from.DebitedWalletID, + DestinationAccountReference: &from.CreditedWalletID, + Raw: raw, + }, nil +} diff --git a/internal/connectors/plugins/public/modulr/plugin.go b/internal/connectors/plugins/public/modulr/plugin.go index ee00c315..755d2c82 100644 --- a/internal/connectors/plugins/public/modulr/plugin.go +++ b/internal/connectors/plugins/public/modulr/plugin.go @@ -70,6 +70,14 @@ func (p Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAcco return models.CreateBankAccountResponse{}, plugins.ErrNotImplemented } +func (p Plugin) CreateTransfer(ctx context.Context, req models.CreateTransferRequest) (models.CreateTransferResponse, error) { + return models.CreateTransferResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreatePayout(ctx context.Context, req models.CreatePayoutRequest) (models.CreatePayoutResponse, error) { + return models.CreatePayoutResponse{}, plugins.ErrNotImplemented +} + func (p Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { return models.CreateWebhooksResponse{}, plugins.ErrNotImplemented } diff --git a/internal/connectors/plugins/public/moneycorp/plugin.go b/internal/connectors/plugins/public/moneycorp/plugin.go index a7e06793..03e483f6 100644 --- a/internal/connectors/plugins/public/moneycorp/plugin.go +++ b/internal/connectors/plugins/public/moneycorp/plugin.go @@ -70,6 +70,14 @@ func (p *Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAcc return models.CreateBankAccountResponse{}, plugins.ErrNotImplemented } +func (p *Plugin) CreateTransfer(ctx context.Context, req models.CreateTransferRequest) (models.CreateTransferResponse, error) { + return models.CreateTransferResponse{}, plugins.ErrNotImplemented +} + +func (p *Plugin) CreatePayout(ctx context.Context, req models.CreatePayoutRequest) (models.CreatePayoutResponse, error) { + return models.CreatePayoutResponse{}, plugins.ErrNotImplemented +} + func (p *Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { return models.CreateWebhooksResponse{}, plugins.ErrNotImplemented } diff --git a/internal/connectors/plugins/public/stripe/plugin.go b/internal/connectors/plugins/public/stripe/plugin.go index 0cd1208d..7e140a89 100644 --- a/internal/connectors/plugins/public/stripe/plugin.go +++ b/internal/connectors/plugins/public/stripe/plugin.go @@ -76,6 +76,14 @@ func (p *Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAcc return models.CreateBankAccountResponse{}, plugins.ErrNotImplemented } +func (p Plugin) CreateTransfer(ctx context.Context, req models.CreateTransferRequest) (models.CreateTransferResponse, error) { + return models.CreateTransferResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreatePayout(ctx context.Context, req models.CreatePayoutRequest) (models.CreatePayoutResponse, error) { + return models.CreatePayoutResponse{}, plugins.ErrNotImplemented +} + func (p *Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { return models.CreateWebhooksResponse{}, plugins.ErrNotImplemented } diff --git a/internal/connectors/plugins/public/wise/plugin.go b/internal/connectors/plugins/public/wise/plugin.go index 16b412f3..ddcd26c7 100644 --- a/internal/connectors/plugins/public/wise/plugin.go +++ b/internal/connectors/plugins/public/wise/plugin.go @@ -111,6 +111,14 @@ func (p Plugin) CreateBankAccount(ctx context.Context, req models.CreateBankAcco return models.CreateBankAccountResponse{}, plugins.ErrNotImplemented } +func (p Plugin) CreateTransfer(ctx context.Context, req models.CreateTransferRequest) (models.CreateTransferResponse, error) { + return models.CreateTransferResponse{}, plugins.ErrNotImplemented +} + +func (p Plugin) CreatePayout(ctx context.Context, req models.CreatePayoutRequest) (models.CreatePayoutResponse, error) { + return models.CreatePayoutResponse{}, plugins.ErrNotImplemented +} + func (p Plugin) CreateWebhooks(ctx context.Context, req models.CreateWebhooksRequest) (models.CreateWebhooksResponse, error) { if p.client == nil { return models.CreateWebhooksResponse{}, plugins.ErrNotYetInstalled diff --git a/internal/models/accounts.go b/internal/models/accounts.go index f7f8c625..a5e4bb9b 100644 --- a/internal/models/accounts.go +++ b/internal/models/accounts.go @@ -146,3 +146,14 @@ func FromPSPAccounts(from []PSPAccount, accountType AccountType, connectorID Con } return accounts } + +func ToPSPAccount(from *Account) *PSPAccount { + return &PSPAccount{ + Reference: from.Reference, + CreatedAt: from.CreatedAt, + Name: from.Name, + DefaultAsset: from.DefaultAsset, + Metadata: from.Metadata, + Raw: from.Raw, + } +} diff --git a/internal/models/errors.go b/internal/models/errors.go index 37e35f85..9ffcfdfa 100644 --- a/internal/models/errors.go +++ b/internal/models/errors.go @@ -7,4 +7,5 @@ var ( ErrFailedAccountCreation = errors.New("failed to create account") ErrMissingFromPayloadInRequest = errors.New("missing from payload in request") ErrMissingAccountInMetadata = errors.New("missing account number in metadata") + ErrInvalidRequest = errors.New("invalid request") ) diff --git a/internal/models/payment_initiation_adjustment_id.go b/internal/models/payment_initiation_adjustment_id.go new file mode 100644 index 00000000..4f699fd1 --- /dev/null +++ b/internal/models/payment_initiation_adjustment_id.go @@ -0,0 +1,80 @@ +package models + +import ( + "database/sql/driver" + "encoding/base64" + "errors" + "fmt" + "time" + + "github.com/gibson042/canonicaljson-go" +) + +type PaymentInitiationAdjustmentID struct { + PaymentInitiationID PaymentInitiationID + CreatedAt time.Time + Status PaymentInitiationAdjustmentStatus +} + +func (pid PaymentInitiationAdjustmentID) String() string { + data, err := canonicaljson.Marshal(pid) + if err != nil { + panic(err) + } + + return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(data) +} + +func PaymentInitiationAdjustmentIDFromString(value string) (PaymentInitiationAdjustmentID, error) { + data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) + if err != nil { + return PaymentInitiationAdjustmentID{}, err + } + ret := PaymentInitiationAdjustmentID{} + err = canonicaljson.Unmarshal(data, &ret) + if err != nil { + return PaymentInitiationAdjustmentID{}, err + } + + return ret, nil +} + +func MustPaymentInitiationAdjustmentIDFromString(value string) *PaymentInitiationAdjustmentID { + data, err := base64.URLEncoding.WithPadding(base64.NoPadding).DecodeString(value) + if err != nil { + panic(err) + } + ret := PaymentInitiationAdjustmentID{} + err = canonicaljson.Unmarshal(data, &ret) + if err != nil { + panic(err) + } + + return &ret +} + +func (pid PaymentInitiationAdjustmentID) Value() (driver.Value, error) { + return pid.String(), nil +} + +func (pid *PaymentInitiationAdjustmentID) Scan(value interface{}) error { + if value == nil { + return errors.New("payment adjustment id is nil") + } + + if s, err := driver.String.ConvertValue(value); err == nil { + + if v, ok := s.(string); ok { + + id, err := PaymentInitiationAdjustmentIDFromString(v) + if err != nil { + return fmt.Errorf("failed to parse payment adjustment id %s: %v", v, err) + } + + *pid = id + return nil + } + } + + return fmt.Errorf("failed to scan payment adjustement id: %v", value) +} diff --git a/internal/models/payment_initiation_type.go b/internal/models/payment_initiation_type.go new file mode 100644 index 00000000..e7d33086 --- /dev/null +++ b/internal/models/payment_initiation_type.go @@ -0,0 +1,99 @@ +package models + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "fmt" +) + +type PaymentInitiationType int + +const ( + PAYMENT_INITIATION_TYPE_UNKNOWN PaymentInitiationType = iota + PAYMENT_INITIATION_TYPE_TRANSFER + PAYMENT_INITIATION_TYPE_PAYOUT +) + +func (t PaymentInitiationType) String() string { + switch t { + case PAYMENT_INITIATION_TYPE_UNKNOWN: + return "UNKNOWN" + case PAYMENT_INITIATION_TYPE_TRANSFER: + return "TRANSFER" + case PAYMENT_INITIATION_TYPE_PAYOUT: + return "PAYOUT" + default: + return "UNKNOWN" + } +} + +func PaymentInitiationTypeFromString(value string) (PaymentInitiationType, error) { + switch value { + case "TRANSFER": + return PAYMENT_INITIATION_TYPE_TRANSFER, nil + case "PAYOUT": + return PAYMENT_INITIATION_TYPE_PAYOUT, nil + case "UNKNOWN": + return PAYMENT_INITIATION_TYPE_UNKNOWN, nil + default: + return PAYMENT_INITIATION_TYPE_UNKNOWN, errors.New("invalid payment initiation type value") + } +} + +func MustPaymentInitiationTypeFromString(value string) PaymentInitiationType { + ret, err := PaymentInitiationTypeFromString(value) + if err != nil { + panic(err) + } + return ret +} + +func (t PaymentInitiationType) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, t.String())), nil +} + +func (t *PaymentInitiationType) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + value, err := PaymentInitiationTypeFromString(v) + if err != nil { + return err + } + + *t = value + + return nil +} + +func (t PaymentInitiationType) Value() (driver.Value, error) { + return t.String(), nil +} + +func (t *PaymentInitiationType) Scan(value interface{}) error { + if value == nil { + return errors.New("payment status is nil") + } + + s, err := driver.String.ConvertValue(value) + if err != nil { + return fmt.Errorf("failed to convert payment status") + } + + v, ok := s.(string) + if !ok { + return fmt.Errorf("failed to cast payment status") + } + + res, err := PaymentInitiationTypeFromString(v) + if err != nil { + return err + } + + *t = res + + return nil +} diff --git a/internal/models/payments_initation_related_payments.go b/internal/models/payments_initation_related_payments.go new file mode 100644 index 00000000..a7e1cea1 --- /dev/null +++ b/internal/models/payments_initation_related_payments.go @@ -0,0 +1,9 @@ +package models + +type PaymentInitiationRelatedPayments struct { + // Payment Initiation ID + PaymentInitiationID PaymentInitiationID `json:"paymentInitiationID"` + + // Related Payment ID + PaymentID PaymentID `json:"paymentID"` +} diff --git a/internal/models/payments_initiation_adjusments_status.go b/internal/models/payments_initiation_adjusments_status.go new file mode 100644 index 00000000..26eb81a5 --- /dev/null +++ b/internal/models/payments_initiation_adjusments_status.go @@ -0,0 +1,135 @@ +package models + +import ( + "database/sql/driver" + "encoding/json" + "errors" + "fmt" +) + +type PaymentInitiationAdjustmentStatus int + +const ( + PAYMENT_INITIATION_ADJUSTMENT_STATUS_UNKNOWN = iota + PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION + PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING + PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSED + PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED + PAYMENT_INITIATION_ADJUSTMENT_STATUS_REJECTED + PAYMENT_INITIATION_ADJUSTMENT_STATUS_ASK_RETRIED + PAYMENT_INITIATION_ADJUSTMENT_STATUS_ASK_REVERSED + PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSE_PROCESSING + PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSE_FAILED + PAYMENT_INITIATION_ADJUSTMENT_STATUS_PARTIALLY_REVERSED + PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSED +) + +func (s PaymentInitiationAdjustmentStatus) String() string { + switch s { + case PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION: + return "WAITING_FOR_VALIDATION" + case PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING: + return "PROCESSING" + case PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSED: + return "PROCESSED" + case PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED: + return "FAILED" + case PAYMENT_INITIATION_ADJUSTMENT_STATUS_REJECTED: + return "REJECTED" + case PAYMENT_INITIATION_ADJUSTMENT_STATUS_ASK_RETRIED: + return "ASK_RETRIED" + case PAYMENT_INITIATION_ADJUSTMENT_STATUS_ASK_REVERSED: + return "ASK_REVERSED" + case PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSE_PROCESSING: + return "REVERSE_PROCESSING" + case PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSE_FAILED: + return "REVERSE_FAILED" + case PAYMENT_INITIATION_ADJUSTMENT_STATUS_PARTIALLY_REVERSED: + return "PARTIALLY_REVERSED" + case PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSED: + return "REVERSED" + case PAYMENT_INITIATION_ADJUSTMENT_STATUS_UNKNOWN: + return "UNKNOWN" + } + return "UNKNOWN" +} + +func PaymentInitiationAdjustmentStatusFromString(s string) (PaymentInitiationAdjustmentStatus, error) { + switch s { + case "WAITING_FOR_VALIDATION": + return PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION, nil + case "PROCESSING": + return PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING, nil + case "PROCESSED": + return PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSED, nil + case "FAILED": + return PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED, nil + case "REJECTED": + return PAYMENT_INITIATION_ADJUSTMENT_STATUS_REJECTED, nil + case "ASK_RETRIED": + return PAYMENT_INITIATION_ADJUSTMENT_STATUS_ASK_RETRIED, nil + case "ASK_REVERSED": + return PAYMENT_INITIATION_ADJUSTMENT_STATUS_ASK_REVERSED, nil + case "REVERSE_PROCESSING": + return PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSE_PROCESSING, nil + case "REVERSE_FAILED": + return PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSE_FAILED, nil + case "PARTIALLY_REVERSED": + return PAYMENT_INITIATION_ADJUSTMENT_STATUS_PARTIALLY_REVERSED, nil + case "REVERSED": + return PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSED, nil + case "UNKNOWN": + return PAYMENT_INITIATION_ADJUSTMENT_STATUS_UNKNOWN, nil + } + + return PAYMENT_INITIATION_ADJUSTMENT_STATUS_UNKNOWN, fmt.Errorf("unknown PaymentInitiationAdjustmentStatus: %s", s) +} + +func (t PaymentInitiationAdjustmentStatus) MarshalJSON() ([]byte, error) { + return []byte(fmt.Sprintf(`"%s"`, t.String())), nil +} + +func (t *PaymentInitiationAdjustmentStatus) UnmarshalJSON(data []byte) error { + var v string + if err := json.Unmarshal(data, &v); err != nil { + return err + } + + value, err := PaymentInitiationAdjustmentStatusFromString(v) + if err != nil { + return err + } + + *t = value + + return nil +} + +func (t PaymentInitiationAdjustmentStatus) Value() (driver.Value, error) { + return t.String(), nil +} + +func (t *PaymentInitiationAdjustmentStatus) Scan(value interface{}) error { + if value == nil { + return errors.New("payment initiation adjusmtent status status is nil") + } + + s, err := driver.String.ConvertValue(value) + if err != nil { + return fmt.Errorf("failed to convert payment initiation adjusmtent status status") + } + + v, ok := s.(string) + if !ok { + return fmt.Errorf("failed to cast payment initiation adjusmtent status status") + } + + res, err := PaymentInitiationAdjustmentStatusFromString(v) + if err != nil { + return err + } + + *t = res + + return nil +} diff --git a/internal/models/payments_initiation_adjustments.go b/internal/models/payments_initiation_adjustments.go new file mode 100644 index 00000000..224a5573 --- /dev/null +++ b/internal/models/payments_initiation_adjustments.go @@ -0,0 +1,74 @@ +package models + +import ( + "encoding/json" + "time" +) + +type PaymentInitiationAdjustment struct { + // Unique ID + ID PaymentInitiationAdjustmentID `json:"id"` + + // Related Payment Initiation ID + PaymentInitiationID PaymentInitiationID `json:"paymentInitiationID"` + // Creation date of the adjustment + CreatedAt time.Time `json:"createdAt"` + // Last status of the adjustment + Status PaymentInitiationAdjustmentStatus `json:"status"` + // Error description if we had one + Error *string `json:"error"` + // Additional metadata + Metadata map[string]string `json:"metadata"` +} + +func (pia PaymentInitiationAdjustment) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + ID string `json:"id"` + PaymentInitiationID string `json:"paymentInitiationID"` + CreatedAt time.Time `json:"createdAt"` + Status PaymentInitiationAdjustmentStatus `json:"status"` + Error *string `json:"error,omitempty"` + Metadata map[string]string `json:"metadata"` + }{ + ID: pia.ID.String(), + PaymentInitiationID: pia.PaymentInitiationID.String(), + CreatedAt: pia.CreatedAt, + Status: pia.Status, + Error: pia.Error, + Metadata: pia.Metadata, + }) +} + +func (pia *PaymentInitiationAdjustment) UnmarshalJSON(data []byte) error { + var aux struct { + ID string `json:"id"` + PaymentInitiationID string `json:"paymentInitiationID"` + CreatedAt time.Time `json:"createdAt"` + Status PaymentInitiationAdjustmentStatus `json:"status"` + Error *string `json:"error"` + Metadata map[string]string `json:"metadata"` + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + id, err := PaymentInitiationAdjustmentIDFromString(aux.ID) + if err != nil { + return err + } + + piID, err := PaymentInitiationIDFromString(aux.PaymentInitiationID) + if err != nil { + return err + } + + pia.ID = id + pia.PaymentInitiationID = piID + pia.CreatedAt = aux.CreatedAt + pia.Status = aux.Status + pia.Error = aux.Error + pia.Metadata = aux.Metadata + + return nil +} diff --git a/internal/models/payments_initiations.go b/internal/models/payments_initiations.go index ce7c55ce..b8beb652 100644 --- a/internal/models/payments_initiations.go +++ b/internal/models/payments_initiations.go @@ -1,15 +1,37 @@ package models import ( + "encoding/json" "math/big" "time" -) -const ( - PaymentInitiationTypeTransfer string = "TRANSFER" - PaymentInitiationTypePayout string = "PAYOUT" + "github.com/formancehq/go-libs/pointer" ) +type PSPPaymentInitiation struct { + // Reference of the unique payment initiation + Reference string + + // Payment Initiation creation date + CreatedAt time.Time + + // Description of the payment + Description string + + // PSP reference of the source account + SourceAccount *PSPAccount + // PSP reference of the destination account + DestinationAccount *PSPAccount + + // Amount of the payment + Amount *big.Int + // Asset of the payment + Asset string + + // Additional metadata + Metadata map[string]string +} + type PaymentInitiation struct { // Unique Payment initiation ID generated from payments information ID PaymentInitiationID `json:"id"` @@ -18,19 +40,22 @@ type PaymentInitiation struct { // Unique reference of the payment Reference string `json:"reference"` + // Payment Initiation creation date + CreatedAt time.Time `json:"createdAt"` + // Time to schedule the payment ScheduledAt time.Time `json:"scheduledAt"` // Description of the payment Description string `json:"description"` + Type PaymentInitiationType `json:"paymentInitiationType"` + // Source account of the payment SourceAccountID *AccountID `json:"sourceAccountID"` // Destination account of the payment - DestinationAccountID AccountID `json:"destinationAccountID"` + DestinationAccountID *AccountID `json:"destinationAccountID"` - // Payment initial amount - InitialAmount *big.Int `json:"initialAmount"` // Payment current amount (can be changed of reversed, refunded, etc...) Amount *big.Int `json:"amount"` // Asset of the payment @@ -39,3 +64,170 @@ type PaymentInitiation struct { // Additional metadata Metadata map[string]string `json:"metadata"` } + +func (pi PaymentInitiation) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + ID string `json:"id"` + ConnectorID string `json:"connectorID"` + Reference string `json:"reference"` + CreatedAt time.Time `json:"createdAt"` + ScheduledAt time.Time `json:"scheduledAt"` + Description string `json:"description"` + Type PaymentInitiationType `json:"paymentInitiationType"` + SourceAccountID *string `json:"sourceAccountID,omitempty"` + DestinationAccountID *string `json:"destinationAccountID,omitempty"` + Amount *big.Int `json:"amount"` + Asset string `json:"asset"` + Metadata map[string]string `json:"metadata"` + }{ + ID: pi.ID.String(), + ConnectorID: pi.ConnectorID.String(), + Reference: pi.Reference, + CreatedAt: pi.CreatedAt, + ScheduledAt: pi.ScheduledAt, + Description: pi.Description, + Type: pi.Type, + SourceAccountID: func() *string { + if pi.SourceAccountID == nil { + return nil + } + return pointer.For(pi.SourceAccountID.String()) + }(), + DestinationAccountID: func() *string { + if pi.DestinationAccountID == nil { + return nil + } + return pointer.For(pi.DestinationAccountID.String()) + }(), + Amount: pi.Amount, + Asset: pi.Asset, + Metadata: pi.Metadata, + }) +} + +func (pi *PaymentInitiation) UnmarshalJSON(data []byte) error { + var aux struct { + ID string `json:"id"` + ConnectorID string `json:"connectorID"` + Reference string `json:"reference"` + CreatedAt time.Time `json:"createdAt"` + ScheduledAt time.Time `json:"scheduledAt"` + Description string `json:"description"` + Type PaymentInitiationType `json:"paymentInitiationType"` + SourceAccountID *string `json:"sourceAccountID,omitempty"` + DestinationAccountID *string `json:"destinationAccountID,omitempty"` + Amount *big.Int `json:"amount"` + Asset string `json:"asset"` + Metadata map[string]string `json:"metadata"` + } + + if err := json.Unmarshal(data, &aux); err != nil { + return err + } + + id, err := PaymentInitiationIDFromString(aux.ID) + if err != nil { + return err + } + + connectorID, err := ConnectorIDFromString(aux.ConnectorID) + if err != nil { + return err + } + + var sourceAccountID *AccountID + if aux.SourceAccountID != nil { + id, err := AccountIDFromString(*aux.SourceAccountID) + if err != nil { + return err + } + sourceAccountID = &id + } + + var destinationAccountID *AccountID + if aux.DestinationAccountID != nil { + id, err := AccountIDFromString(*aux.DestinationAccountID) + if err != nil { + return err + } + destinationAccountID = &id + } + + pi.ID = id + pi.ConnectorID = connectorID + pi.Reference = aux.Reference + pi.CreatedAt = aux.CreatedAt + pi.ScheduledAt = aux.ScheduledAt + pi.Description = aux.Description + pi.Type = aux.Type + pi.SourceAccountID = sourceAccountID + pi.DestinationAccountID = destinationAccountID + pi.Amount = aux.Amount + pi.Asset = aux.Asset + pi.Metadata = aux.Metadata + + return nil +} + +func FromPaymentInitiationToPSPPaymentInitiation(from *PaymentInitiation, sourceAccount, destinationAccount *PSPAccount) *PSPPaymentInitiation { + return &PSPPaymentInitiation{ + Reference: from.Reference, + CreatedAt: from.CreatedAt, + Description: from.Description, + SourceAccount: sourceAccount, + DestinationAccount: destinationAccount, + Amount: from.Amount, + Asset: from.Asset, + Metadata: from.Metadata, + } +} + +type PaymentInitiationExpanded struct { + PaymentInitiation PaymentInitiation + Status string + Error *string +} + +func (pi PaymentInitiationExpanded) MarshalJSON() ([]byte, error) { + return json.Marshal(&struct { + ID string `json:"id"` + ConnectorID string `json:"connectorID"` + Reference string `json:"reference"` + CreatedAt time.Time `json:"createdAt"` + ScheduledAt time.Time `json:"scheduledAt"` + Description string `json:"description"` + Type PaymentInitiationType `json:"paymentInitiationType"` + SourceAccountID *string `json:"sourceAccountID,omitempty"` + DestinationAccountID *string `json:"destinationAccountID,omitempty"` + Amount *big.Int `json:"amount"` + Asset string `json:"asset"` + Metadata map[string]string `json:"metadata"` + Status string `json:"status"` + Error *string `json:"error,omitempty"` + }{ + ID: pi.PaymentInitiation.ID.String(), + ConnectorID: pi.PaymentInitiation.ConnectorID.String(), + Reference: pi.PaymentInitiation.Reference, + CreatedAt: pi.PaymentInitiation.CreatedAt, + ScheduledAt: pi.PaymentInitiation.ScheduledAt, + Description: pi.PaymentInitiation.Description, + Type: pi.PaymentInitiation.Type, + SourceAccountID: func() *string { + if pi.PaymentInitiation.SourceAccountID == nil { + return nil + } + return pointer.For(pi.PaymentInitiation.SourceAccountID.String()) + }(), + DestinationAccountID: func() *string { + if pi.PaymentInitiation.DestinationAccountID == nil { + return nil + } + return pointer.For(pi.PaymentInitiation.DestinationAccountID.String()) + }(), + Amount: pi.PaymentInitiation.Amount, + Asset: pi.PaymentInitiation.Asset, + Metadata: pi.PaymentInitiation.Metadata, + Status: pi.Status, + Error: pi.Error, + }) +} diff --git a/internal/models/plugin.go b/internal/models/plugin.go index fb243e6c..f09f61b1 100644 --- a/internal/models/plugin.go +++ b/internal/models/plugin.go @@ -17,6 +17,8 @@ type Plugin interface { FetchNextOthers(context.Context, FetchNextOthersRequest) (FetchNextOthersResponse, error) CreateBankAccount(context.Context, CreateBankAccountRequest) (CreateBankAccountResponse, error) + CreateTransfer(context.Context, CreateTransferRequest) (CreateTransferResponse, error) + CreatePayout(context.Context, CreatePayoutRequest) (CreatePayoutResponse, error) CreateWebhooks(context.Context, CreateWebhooksRequest) (CreateWebhooksResponse, error) TranslateWebhook(context.Context, TranslateWebhookRequest) (TranslateWebhookResponse, error) @@ -132,3 +134,19 @@ type WebhookResponse struct { type TranslateWebhookResponse struct { Responses []WebhookResponse } + +type CreateTransferRequest struct { + PaymentInitiation PSPPaymentInitiation +} + +type CreateTransferResponse struct { + Payment PSPPayment +} + +type CreatePayoutRequest struct { + PaymentInitiation PSPPaymentInitiation +} + +type CreatePayoutResponse struct { + Payment PSPPayment +} diff --git a/internal/models/plugin_generated.go b/internal/models/plugin_generated.go index dbd60d22..c5c42e28 100644 --- a/internal/models/plugin_generated.go +++ b/internal/models/plugin_generated.go @@ -54,6 +54,36 @@ func (mr *MockPluginMockRecorder) CreateBankAccount(arg0, arg1 any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateBankAccount", reflect.TypeOf((*MockPlugin)(nil).CreateBankAccount), arg0, arg1) } +// CreatePayout mocks base method. +func (m *MockPlugin) CreatePayout(arg0 context.Context, arg1 CreatePayoutRequest) (CreatePayoutResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreatePayout", arg0, arg1) + ret0, _ := ret[0].(CreatePayoutResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreatePayout indicates an expected call of CreatePayout. +func (mr *MockPluginMockRecorder) CreatePayout(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreatePayout", reflect.TypeOf((*MockPlugin)(nil).CreatePayout), arg0, arg1) +} + +// CreateTransfer mocks base method. +func (m *MockPlugin) CreateTransfer(arg0 context.Context, arg1 CreateTransferRequest) (CreateTransferResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateTransfer", arg0, arg1) + ret0, _ := ret[0].(CreateTransferResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateTransfer indicates an expected call of CreateTransfer. +func (mr *MockPluginMockRecorder) CreateTransfer(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTransfer", reflect.TypeOf((*MockPlugin)(nil).CreateTransfer), arg0, arg1) +} + // CreateWebhooks mocks base method. func (m *MockPlugin) CreateWebhooks(arg0 context.Context, arg1 CreateWebhooksRequest) (CreateWebhooksResponse, error) { m.ctrl.T.Helper() diff --git a/internal/storage/migrations/0-init-schema.sql b/internal/storage/migrations/0-init-schema.sql index f64316fc..1324bd50 100644 --- a/internal/storage/migrations/0-init-schema.sql +++ b/internal/storage/migrations/0-init-schema.sql @@ -276,13 +276,13 @@ alter table webhooks_configs -- Webhooks create table if not exists webhooks ( -- Mandatory fields - id text not null, + id text not null, connector_id varchar not null, -- Optional fields - headers json, + headers json, query_values json, - body bytea, + body bytea, -- Primary key primary key (id) @@ -290,4 +290,73 @@ create table if not exists webhooks ( alter table webhooks add constraint webhooks_connector_id_fk foreign key (connector_id) references connectors (id) + on delete cascade; + +-- Payment Initiations +create table if not exists payment_initiations ( + -- Mandatory fields + id text not null, + connector_id varchar not null, + reference text not null, + created_at timestamp without time zone not null, + scheduled_at timestamp without time zone not null, + description text not null, + type text not null, + amount numeric not null, + asset text not null, + + -- Optional fields + source_account_id varchar, + destination_account_id varchar, + + -- Optional fields with default + metadata jsonb not null default '{}'::jsonb, + + -- Primary key + primary key (id) +); +alter table payment_initiations + add constraint payment_initiations_connector_id_fk foreign key (connector_id) + references connectors (id) + on delete cascade; + +-- Payment Initiation Related Payments +create table if not exists payment_initiation_related_payments( + -- Mandatory fields + payment_initiation_id varchar not null, + payment_id varchar not null, + created_at timestamp without time zone not null, + + -- Primary key + primary key (payment_initiation_id, payment_id) +); +alter table payment_initiation_related_payments + add constraint payment_initiation_related_payments_payment_initiation_id_fk foreign key (payment_initiation_id) + references payment_initiations (id) + on delete cascade; +alter table payment_initiation_related_payments + add constraint payment_initiation_related_payments_payment_id_fk foreign key (payment_id) + references payments (id) + on delete cascade; + +-- Payment Initiation Adjustments +create table if not exists payment_initiation_adjustments( + -- Mandatory fields + id varchar not null, + payment_initiation_id varchar not null, + created_at timestamp without time zone not null, + status text not null, + + -- Optional fields + error text, + + -- Optional fields with default + metadata jsonb not null default '{}'::jsonb, + + -- Primary key + primary key (id) +); +alter table payment_initiation_adjustments + add constraint payment_initiation_adjustments_payment_initiation_id_fk foreign key (payment_initiation_id) + references payment_initiations (id) on delete cascade; \ No newline at end of file diff --git a/internal/storage/payment_initiations.go b/internal/storage/payment_initiations.go new file mode 100644 index 00000000..318ea7d5 --- /dev/null +++ b/internal/storage/payment_initiations.go @@ -0,0 +1,444 @@ +package storage + +import ( + "context" + "fmt" + "math/big" + stdtime "time" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/query" + "github.com/formancehq/go-libs/time" + "github.com/formancehq/payments/internal/models" + "github.com/pkg/errors" + "github.com/uptrace/bun" +) + +type paymentInitiation struct { + bun.BaseModel `bun:"payment_initiations"` + + // Mandatory fields + ID models.PaymentInitiationID `bun:"id,pk,type:character varying,notnull"` + ConnectorID models.ConnectorID `bun:"connector_id,type:character varying,notnull"` + Reference string `bun:"reference,type:text,notnull"` + CreatedAt time.Time `bun:"created_at,type:timestamp without time zone,notnull"` + ScheduledAt time.Time `bun:"scheduled_at,type:timestamp without time zone,notnull"` + Description string `bun:"description,type:text,notnull"` + Type models.PaymentInitiationType `bun:"type,type:text,notnull"` + Amount *big.Int `bun:"amount,type:numeric,notnull"` + Asset string `bun:"asset,type:text,notnull"` + + // Optional fields + SourceAccountID *models.AccountID `bun:"source_account_id,type:character varying"` + DestinationAccountID *models.AccountID `bun:"destination_account_id,type:character varying,notnull"` + + // Optional fields with default + // c.f. https://bun.uptrace.dev/guide/models.html#default + Metadata map[string]string `bun:"metadata,type:jsonb,nullzero,notnull,default:'{}'"` +} + +type paymentInitiationRelatedPayment struct { + bun.BaseModel `bun:"payment_initiation_related_payments"` + + // Mandatory fields + PaymentInitiationID models.PaymentInitiationID `bun:"payment_initiation_id,pk,type:character varying,notnull"` + PaymentID models.PaymentID `bun:"payment_id,pk,type:character varying,notnull"` + CreatedAt time.Time `bun:"created_at,type:timestamp without time zone,notnull"` +} + +type paymentInitiationAdjustment struct { + bun.BaseModel `bun:"payment_initiation_adjustments"` + + // Mandatory fields + ID models.PaymentInitiationAdjustmentID `bun:"id,pk,type:character varying,notnull"` + PaymentInitiationID models.PaymentInitiationID `bun:"payment_initiation_id,type:character varying,notnull"` + CreatedAt time.Time `bun:"created_at,type:timestamp without time zone,notnull"` + Status models.PaymentInitiationAdjustmentStatus `bun:"status,type:text,notnull"` + + // Optional fields + Error *string `bun:"error,type:text"` + + // Optional fields with default + // c.f. https://bun.uptrace.dev/guide/models.html#default + Metadata map[string]string `bun:"metadata,type:jsonb,nullzero,notnull,default:'{}'"` +} + +func (s *store) PaymentInitiationsUpsert(ctx context.Context, pi models.PaymentInitiation, adjustments ...models.PaymentInitiationAdjustment) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return e("update payment metadata", err) + } + defer tx.Rollback() + + toInsert := fromPaymentInitiationModels(pi) + adjustmentsToInsert := make([]paymentInitiationAdjustment, 0, len(adjustments)) + for _, adj := range adjustments { + adjustmentsToInsert = append(adjustmentsToInsert, fromPaymentInitiationAdjustmentModels(adj)) + } + + _, err = tx.NewInsert(). + Model(&toInsert). + On("CONFLICT (id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("failed to insert payment initiations", err) + } + + if len(adjustmentsToInsert) > 0 { + _, err = tx.NewInsert(). + Model(&adjustmentsToInsert). + On("CONFLICT (id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("failed to insert payment initiation adjustments", err) + } + } + + return e("failed to commit transaction", tx.Commit()) +} + +func (s *store) PaymentInitiationsUpdateMetadata(ctx context.Context, piID models.PaymentInitiationID, metadata map[string]string) error { + tx, err := s.db.BeginTx(ctx, nil) + if err != nil { + return e("update payment metadata", err) + } + defer tx.Rollback() + + var pi paymentInitiation + err = tx.NewSelect(). + Model(&pi). + Column("id", "metadata"). + Where("id = ?", piID). + Scan(ctx) + if err != nil { + return e("update payment initiation metadata", err) + } + + if pi.Metadata == nil { + pi.Metadata = make(map[string]string) + } + + for k, v := range metadata { + pi.Metadata[k] = v + } + + _, err = tx.NewUpdate(). + Model(&pi). + Column("metadata"). + Where("id = ?", piID). + Exec(ctx) + if err != nil { + return e("update payment initiation metadata", err) + } + + return e("failed to commit transaction", tx.Commit()) +} + +func (s *store) PaymentInitiationsGet(ctx context.Context, piID models.PaymentInitiationID) (*models.PaymentInitiation, error) { + var pi paymentInitiation + err := s.db.NewSelect(). + Model(&pi). + Where("id = ?", piID). + Scan(ctx) + if err != nil { + return nil, e("failed to get payment initiation", err) + } + + res := toPaymentInitiationModels(pi) + return &res, nil +} + +func (s *store) PaymentInitiationsDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error { + _, err := s.db.NewDelete(). + Model((*paymentInitiation)(nil)). + Where("connector_id = ?", connectorID). + Exec(ctx) + return e("failed to delete payment initiations", err) +} + +func (s *store) PaymentInitiationsDelete(ctx context.Context, piID models.PaymentInitiationID) error { + _, err := s.db.NewDelete(). + Model((*paymentInitiation)(nil)). + Where("id = ?", piID). + Exec(ctx) + return e("failed to delete payment initiation", err) +} + +type PaymentInitiationQuery struct{} + +type ListPaymentInitiationsQuery bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[PaymentInitiationQuery]] + +func NewListPaymentInitiationsQuery(opts bunpaginate.PaginatedQueryOptions[PaymentInitiationQuery]) ListPaymentInitiationsQuery { + return ListPaymentInitiationsQuery{ + Order: bunpaginate.OrderAsc, + PageSize: opts.PageSize, + Options: opts, + } +} + +func (s *store) paymentsInitiationQueryContext(qb query.Builder) (string, []any, error) { + where, args, err := qb.Build(query.ContextFn(func(key, operator string, value any) (string, []any, error) { + switch { + case key == "reference", + key == "connector_id", + key == "type", + key == "asset", + key == "source_account_id", + key == "destination_account_id": + if operator != "$match" { + return "", nil, errors.Wrap(ErrValidation, "'type' column can only be used with $match") + } + return fmt.Sprintf("%s = ?", key), []any{value}, nil + + case key == "amount": + return fmt.Sprintf("%s %s ?", key, query.DefaultComparisonOperatorsMapping[operator]), []any{value}, nil + case metadataRegex.Match([]byte(key)): + if operator != "$match" { + return "", nil, errors.Wrap(ErrValidation, "'metadata' column can only be used with $match") + } + match := metadataRegex.FindAllStringSubmatch(key, 3) + + key := "metadata" + return key + " @> ?", []any{map[string]any{ + match[0][1]: value, + }}, nil + default: + return "", nil, errors.Wrap(ErrValidation, fmt.Sprintf("unknown key '%s' when building query", key)) + } + })) + + return where, args, err +} + +func (s *store) PaymentInitiationsList(ctx context.Context, q ListPaymentInitiationsQuery) (*bunpaginate.Cursor[models.PaymentInitiation], error) { + var ( + where string + args []any + err error + ) + if q.Options.QueryBuilder != nil { + where, args, err = s.paymentsInitiationQueryContext(q.Options.QueryBuilder) + if err != nil { + return nil, err + } + } + + cursor, err := paginateWithOffset[bunpaginate.PaginatedQueryOptions[PaymentInitiationQuery], paymentInitiation](s, ctx, + (*bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[PaymentInitiationQuery]])(&q), + func(query *bun.SelectQuery) *bun.SelectQuery { + if where != "" { + query = query.Where(where, args...) + } + + // TODO(polo): sorter ? + query = query.Order("created_at DESC") + + return query + }, + ) + if err != nil { + return nil, e("failed to fetch accounts", err) + } + + pis := make([]models.PaymentInitiation, 0, len(cursor.Data)) + for _, pi := range cursor.Data { + pis = append(pis, toPaymentInitiationModels(pi)) + } + + return &bunpaginate.Cursor[models.PaymentInitiation]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: pis, + }, nil +} + +func (s *store) PaymentInitiationRelatedPaymentsUpsert(ctx context.Context, piID models.PaymentInitiationID, pID models.PaymentID, createdAt stdtime.Time) error { + toInsert := paymentInitiationRelatedPayment{ + PaymentInitiationID: piID, + PaymentID: pID, + CreatedAt: time.New(createdAt), + } + + _, err := s.db.NewInsert(). + Model(&toInsert). + On("CONFLICT (payment_initiation_id, payment_id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("failed to insert payment initiation related payments", err) + } + + return nil +} + +type PaymentInitiationRelatedPaymentsQuery struct{} + +type ListPaymentInitiationRelatedPaymentsQuery bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[PaymentInitiationRelatedPaymentsQuery]] + +func NewListPaymentInitiationRelatedPaymentsQuery(opts bunpaginate.PaginatedQueryOptions[PaymentInitiationRelatedPaymentsQuery]) ListPaymentInitiationRelatedPaymentsQuery { + return ListPaymentInitiationRelatedPaymentsQuery{ + Order: bunpaginate.OrderAsc, + PageSize: opts.PageSize, + Options: opts, + } +} + +func (s *store) PaymentInitiationRelatedPaymentsList(ctx context.Context, piID models.PaymentInitiationID, q ListPaymentInitiationRelatedPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) { + cursor, err := paginateWithOffset[bunpaginate.PaginatedQueryOptions[PaymentInitiationRelatedPaymentsQuery], paymentInitiationRelatedPayment](s, ctx, + (*bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[PaymentInitiationRelatedPaymentsQuery]])(&q), + func(query *bun.SelectQuery) *bun.SelectQuery { + // TODO(polo): sorter ? + query = query.Order("created_at DESC") + query.Where("payment_initiation_id = ?", piID) + + return query + }, + ) + if err != nil { + return nil, e("failed to fetch accounts", err) + } + + pis := make([]models.Payment, 0, len(cursor.Data)) + for _, pi := range cursor.Data { + p, err := s.PaymentsGet(ctx, pi.PaymentID) + if err != nil { + return nil, e("failed to get payment", err) + } + + pis = append(pis, *p) + } + + return &bunpaginate.Cursor[models.Payment]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: pis, + }, nil +} + +func (s *store) PaymentInitiationAdjustmentsUpsert(ctx context.Context, adj models.PaymentInitiationAdjustment) error { + toInsert := fromPaymentInitiationAdjustmentModels(adj) + + _, err := s.db.NewInsert(). + Model(&toInsert). + On("CONFLICT (id) DO NOTHING"). + Exec(ctx) + if err != nil { + return e("failed to insert payment initiation adjustments", err) + } + + return nil +} + +func (s *store) PaymentInitiationAdjustmentsGet(ctx context.Context, id models.PaymentInitiationAdjustmentID) (*models.PaymentInitiationAdjustment, error) { + var adj paymentInitiationAdjustment + err := s.db.NewSelect(). + Model(&adj). + Where("id = ?", id). + Scan(ctx) + if err != nil { + return nil, e("failed to get payment initiation adjustment", err) + } + + res := toPaymentInitiationAdjustmentModels(adj) + return &res, nil +} + +type PaymentInitiationAdjustmentsQuery struct{} + +type ListPaymentInitiationAdjustmentsQuery bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[PaymentInitiationAdjustmentsQuery]] + +func NewListPaymentInitiationAdjustmentsQuery(opts bunpaginate.PaginatedQueryOptions[PaymentInitiationAdjustmentsQuery]) ListPaymentInitiationAdjustmentsQuery { + return ListPaymentInitiationAdjustmentsQuery{ + Order: bunpaginate.OrderAsc, + PageSize: opts.PageSize, + Options: opts, + } +} + +func (s *store) PaymentInitiationAdjustmentsList(ctx context.Context, piID models.PaymentInitiationID, q ListPaymentInitiationAdjustmentsQuery) (*bunpaginate.Cursor[models.PaymentInitiationAdjustment], error) { + cursor, err := paginateWithOffset[bunpaginate.PaginatedQueryOptions[PaymentInitiationAdjustmentsQuery], paymentInitiationAdjustment](s, ctx, + (*bunpaginate.OffsetPaginatedQuery[bunpaginate.PaginatedQueryOptions[PaymentInitiationAdjustmentsQuery]])(&q), + func(query *bun.SelectQuery) *bun.SelectQuery { + // TODO(polo): sorter ? + query = query.Order("created_at DESC") + query.Where("payment_initiation_id = ?", piID) + + return query + }, + ) + if err != nil { + return nil, e("failed to fetch accounts", err) + } + + pis := make([]models.PaymentInitiationAdjustment, 0, len(cursor.Data)) + for _, pi := range cursor.Data { + pis = append(pis, toPaymentInitiationAdjustmentModels(pi)) + } + + return &bunpaginate.Cursor[models.PaymentInitiationAdjustment]{ + PageSize: cursor.PageSize, + HasMore: cursor.HasMore, + Previous: cursor.Previous, + Next: cursor.Next, + Data: pis, + }, nil +} + +func fromPaymentInitiationModels(from models.PaymentInitiation) paymentInitiation { + return paymentInitiation{ + ID: from.ID, + ConnectorID: from.ConnectorID, + Reference: from.Reference, + CreatedAt: time.New(from.CreatedAt), + ScheduledAt: time.New(from.ScheduledAt), + Description: from.Description, + Type: from.Type, + Amount: from.Amount, + Asset: from.Asset, + DestinationAccountID: from.DestinationAccountID, + SourceAccountID: from.SourceAccountID, + Metadata: from.Metadata, + } +} + +func toPaymentInitiationModels(from paymentInitiation) models.PaymentInitiation { + return models.PaymentInitiation{ + ID: from.ID, + ConnectorID: from.ConnectorID, + Reference: from.Reference, + CreatedAt: from.CreatedAt.Time, + ScheduledAt: from.ScheduledAt.Time, + Description: from.Description, + Type: from.Type, + SourceAccountID: from.SourceAccountID, + DestinationAccountID: from.DestinationAccountID, + Amount: from.Amount, + Asset: from.Asset, + Metadata: from.Metadata, + } +} + +func fromPaymentInitiationAdjustmentModels(from models.PaymentInitiationAdjustment) paymentInitiationAdjustment { + return paymentInitiationAdjustment{ + ID: from.ID, + PaymentInitiationID: from.PaymentInitiationID, + CreatedAt: time.New(from.CreatedAt), + Status: from.Status, + Error: from.Error, + Metadata: from.Metadata, + } +} + +func toPaymentInitiationAdjustmentModels(from paymentInitiationAdjustment) models.PaymentInitiationAdjustment { + return models.PaymentInitiationAdjustment{ + ID: from.ID, + PaymentInitiationID: from.PaymentInitiationID, + CreatedAt: from.CreatedAt.Time, + Status: from.Status, + Error: from.Error, + Metadata: from.Metadata, + } +} diff --git a/internal/storage/payment_initiations_test.go b/internal/storage/payment_initiations_test.go new file mode 100644 index 00000000..a2a36d4b --- /dev/null +++ b/internal/storage/payment_initiations_test.go @@ -0,0 +1,975 @@ +package storage + +import ( + "context" + "math/big" + "testing" + + "github.com/formancehq/go-libs/bun/bunpaginate" + "github.com/formancehq/go-libs/logging" + "github.com/formancehq/go-libs/pointer" + "github.com/formancehq/go-libs/query" + "github.com/formancehq/go-libs/time" + "github.com/formancehq/payments/internal/models" + "github.com/google/uuid" + "github.com/stretchr/testify/require" +) + +var ( + piID1 = models.PaymentInitiationID{ + Reference: "test1", + ConnectorID: defaultConnector.ID, + } + + piID2 = models.PaymentInitiationID{ + Reference: "test2", + ConnectorID: defaultConnector.ID, + } + + piID3 = models.PaymentInitiationID{ + Reference: "test3", + ConnectorID: defaultConnector.ID, + } + + defaultPaymentInitiations = []models.PaymentInitiation{ + { + ID: piID1, + ConnectorID: defaultConnector.ID, + Reference: "test1", + CreatedAt: now.Add(-60 * time.Minute).UTC().Time, + ScheduledAt: now.Add(-60 * time.Minute).UTC().Time, + Description: "test1", + Type: models.PAYMENT_INITIATION_TYPE_PAYOUT, + DestinationAccountID: &defaultAccounts[0].ID, + Amount: big.NewInt(100), + Asset: "EUR/2", + }, + { + ID: piID2, + ConnectorID: defaultConnector.ID, + Reference: "test2", + CreatedAt: now.Add(-30 * time.Minute).UTC().Time, + ScheduledAt: now.Add(-20 * time.Minute).UTC().Time, + Description: "test2", + Type: models.PAYMENT_INITIATION_TYPE_TRANSFER, + SourceAccountID: &defaultAccounts[0].ID, + DestinationAccountID: &defaultAccounts[1].ID, + Amount: big.NewInt(150), + Asset: "USD/2", + Metadata: map[string]string{ + "foo": "bar", + }, + }, + { + ID: piID3, + ConnectorID: defaultConnector.ID, + Reference: "test3", + CreatedAt: now.Add(-55 * time.Minute).UTC().Time, + ScheduledAt: now.Add(-40 * time.Minute).UTC().Time, + Description: "test3", + Type: models.PAYMENT_INITIATION_TYPE_PAYOUT, + DestinationAccountID: &defaultAccounts[1].ID, + Amount: big.NewInt(200), + Asset: "EUR/2", + Metadata: map[string]string{ + "foo2": "bar2", + }, + }, + } +) + +func upsertPaymentInitiations(t *testing.T, ctx context.Context, storage Storage, paymentInitiations []models.PaymentInitiation) { + for _, pi := range paymentInitiations { + err := storage.PaymentInitiationsUpsert(ctx, pi) + require.NoError(t, err) + } +} + +func TestPaymentInitiationsUpsert(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + upsertConnector(t, ctx, store, defaultConnector) + upsertAccounts(t, ctx, store, defaultAccounts) + upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations) + + t.Run("upsert with unknown connector", func(t *testing.T) { + connector := models.ConnectorID{ + Reference: uuid.New(), + Provider: "unknown", + } + p := defaultPaymentInitiations[0] + p.ID.ConnectorID = connector + p.ConnectorID = connector + + err := store.PaymentInitiationsUpsert(ctx, p) + require.Error(t, err) + }) + + t.Run("upsert with same id", func(t *testing.T) { + pi := models.PaymentInitiation{ + ID: piID1, + ConnectorID: defaultConnector.ID, + Reference: "test_changed", + CreatedAt: now.Add(-30 * time.Minute).UTC().Time, + ScheduledAt: now.Add(-20 * time.Minute).UTC().Time, + Description: "test_changed", + Type: models.PAYMENT_INITIATION_TYPE_PAYOUT, + DestinationAccountID: &defaultAccounts[0].ID, + Amount: big.NewInt(100), + Asset: "DKK/2", + } + + upsertPaymentInitiations(t, ctx, store, []models.PaymentInitiation{pi}) + + actual, err := store.PaymentInitiationsGet(ctx, piID1) + require.NoError(t, err) + comparePaymentInitiations(t, defaultPaymentInitiations[0], *actual) + }) +} + +func TestPaymentInitiationsUpdateMetadata(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + upsertConnector(t, ctx, store, defaultConnector) + upsertAccounts(t, ctx, store, defaultAccounts) + upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations) + + t.Run("update metadata of unknown payment initiation", func(t *testing.T) { + require.Error(t, store.PaymentInitiationsUpdateMetadata(ctx, models.PaymentInitiationID{ + Reference: "unknown", + ConnectorID: defaultConnector.ID, + }, map[string]string{})) + }) + + t.Run("update existing metadata", func(t *testing.T) { + metadata := map[string]string{ + "foo": "changed", + } + + require.NoError(t, store.PaymentInitiationsUpdateMetadata(ctx, piID2, metadata)) + + actual, err := store.PaymentInitiationsGet(ctx, piID2) + require.NoError(t, err) + require.Equal(t, len(metadata), len(actual.Metadata)) + for k, v := range metadata { + _, ok := actual.Metadata[k] + require.True(t, ok) + require.Equal(t, v, actual.Metadata[k]) + } + }) + + t.Run("add new metadata", func(t *testing.T) { + metadata := map[string]string{ + "key2": "value2", + "key3": "value3", + } + + require.NoError(t, store.PaymentInitiationsUpdateMetadata(ctx, piID1, metadata)) + + actual, err := store.PaymentInitiationsGet(ctx, piID1) + require.NoError(t, err) + require.Equal(t, len(metadata), len(actual.Metadata)) + for k, v := range metadata { + _, ok := actual.Metadata[k] + require.True(t, ok) + require.Equal(t, v, actual.Metadata[k]) + } + }) +} + +func TestPaymentInitiationsGet(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + upsertConnector(t, ctx, store, defaultConnector) + upsertAccounts(t, ctx, store, defaultAccounts) + upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations) + + t.Run("get unknown payment initiation", func(t *testing.T) { + _, err := store.PaymentInitiationsGet(ctx, models.PaymentInitiationID{ + Reference: "unknown", + ConnectorID: defaultConnector.ID, + }) + require.Error(t, err) + require.ErrorIs(t, err, ErrNotFound) + }) + + t.Run("get existing payment initiation", func(t *testing.T) { + for _, pi := range defaultPaymentInitiations { + actual, err := store.PaymentInitiationsGet(ctx, pi.ID) + require.NoError(t, err) + comparePaymentInitiations(t, pi, *actual) + } + }) +} + +func TestPaymentInitiationsDelete(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + upsertConnector(t, ctx, store, defaultConnector) + upsertAccounts(t, ctx, store, defaultAccounts) + upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations) + + t.Run("delete unknown payment initiation", func(t *testing.T) { + require.NoError(t, store.PaymentInitiationsDelete(ctx, models.PaymentInitiationID{ + Reference: "unknown", + ConnectorID: defaultConnector.ID, + })) + }) + + t.Run("delete existing payment initiation", func(t *testing.T) { + for _, pi := range defaultPaymentInitiations { + require.NoError(t, store.PaymentInitiationsDelete(ctx, pi.ID)) + + _, err := store.PaymentInitiationsGet(ctx, pi.ID) + require.Error(t, err) + require.ErrorIs(t, err, ErrNotFound) + } + }) +} + +func TestPaymentInitiationsDeleteFromConnectorID(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + upsertConnector(t, ctx, store, defaultConnector) + upsertAccounts(t, ctx, store, defaultAccounts) + upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations) + + t.Run("delete from unknown connector", func(t *testing.T) { + require.NoError(t, store.PaymentInitiationsDeleteFromConnectorID(ctx, models.ConnectorID{ + Reference: uuid.New(), + Provider: "unknown", + })) + + for _, pi := range defaultPaymentInitiations { + actual, err := store.PaymentInitiationsGet(ctx, pi.ID) + require.NoError(t, err) + comparePaymentInitiations(t, pi, *actual) + } + }) + + t.Run("delete from existing connector", func(t *testing.T) { + require.NoError(t, store.PaymentInitiationsDeleteFromConnectorID(ctx, defaultConnector.ID)) + + for _, pi := range defaultPaymentInitiations { + _, err := store.PaymentInitiationsGet(ctx, pi.ID) + require.Error(t, err) + require.ErrorIs(t, err, ErrNotFound) + } + }) +} + +func TestPaymentInitiationsList(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + upsertConnector(t, ctx, store, defaultConnector) + upsertAccounts(t, ctx, store, defaultAccounts) + upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations) + + t.Run("list payment intitiations by reference", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("reference", "test1")), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.False(t, cursor.HasMore) + comparePaymentInitiations(t, defaultPaymentInitiations[0], cursor.Data[0]) + }) + + t.Run("list payment initiations by unknown reference", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("reference", "unknown")), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 0) + require.False(t, cursor.HasMore) + }) + + t.Run("list payment initiations by connector_id", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("connector_id", defaultConnector.ID.String())), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 3) + require.False(t, cursor.HasMore) + comparePaymentInitiations(t, defaultPaymentInitiations[1], cursor.Data[0]) + comparePaymentInitiations(t, defaultPaymentInitiations[2], cursor.Data[1]) + comparePaymentInitiations(t, defaultPaymentInitiations[0], cursor.Data[2]) + }) + + t.Run("list payment initiations by unknown connector_id", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("connector_id", "unknown")), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 0) + require.False(t, cursor.HasMore) + }) + + t.Run("list payment initiations by type", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("type", "PAYOUT")), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 2) + require.False(t, cursor.HasMore) + comparePaymentInitiations(t, defaultPaymentInitiations[2], cursor.Data[0]) + comparePaymentInitiations(t, defaultPaymentInitiations[0], cursor.Data[1]) + }) + + t.Run("list payment initiations by type 2", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("type", "TRANSFER")), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.False(t, cursor.HasMore) + comparePaymentInitiations(t, defaultPaymentInitiations[1], cursor.Data[0]) + }) + + t.Run("list payment initiations by unknown type", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("type", "UNKNOWN")), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 0) + require.False(t, cursor.HasMore) + }) + + t.Run("list payment initiations by asset", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("asset", "EUR/2")), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 2) + require.False(t, cursor.HasMore) + comparePaymentInitiations(t, defaultPaymentInitiations[2], cursor.Data[0]) + comparePaymentInitiations(t, defaultPaymentInitiations[0], cursor.Data[1]) + }) + + t.Run("list payment initiations by asset 2", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("asset", "USD/2")), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.False(t, cursor.HasMore) + comparePaymentInitiations(t, defaultPaymentInitiations[1], cursor.Data[0]) + }) + + t.Run("list payment initiations by unknown asset", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("asset", "unknown")), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 0) + require.False(t, cursor.HasMore) + }) + + t.Run("list payment initiations by source account id", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("source_account_id", defaultAccounts[0].ID.String())), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.False(t, cursor.HasMore) + comparePaymentInitiations(t, defaultPaymentInitiations[1], cursor.Data[0]) + }) + + t.Run("list payment initiations by unknown source account id", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("source_account_id", "unknown")), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 0) + require.False(t, cursor.HasMore) + }) + + t.Run("list payment initiations by destination account id", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("destination_account_id", defaultAccounts[1].ID.String())), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 2) + require.False(t, cursor.HasMore) + comparePaymentInitiations(t, defaultPaymentInitiations[1], cursor.Data[0]) + comparePaymentInitiations(t, defaultPaymentInitiations[2], cursor.Data[1]) + }) + + t.Run("list payment initiations by unknown destination account id", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("destination_account_id", "unknown")), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 0) + require.False(t, cursor.HasMore) + }) + + t.Run("list payment initiations by amount", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("amount", 200)), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.False(t, cursor.HasMore) + comparePaymentInitiations(t, defaultPaymentInitiations[2], cursor.Data[0]) + }) + + t.Run("list payment initiations by unknown amount", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("amount", 0)), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 0) + require.False(t, cursor.HasMore) + }) + + t.Run("list payment initiations by metadata", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("metadata[foo]", "bar")), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.False(t, cursor.HasMore) + comparePaymentInitiations(t, defaultPaymentInitiations[1], cursor.Data[0]) + }) + + t.Run("list payment initiations by unknown metadata", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("metadata[foo]", "unknown")), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 0) + require.False(t, cursor.HasMore) + }) + + t.Run("list payment initiations by unknown metadata 2", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(15). + WithQueryBuilder(query.Match("metadata[unknown]", "bar")), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 0) + require.False(t, cursor.HasMore) + }) + + t.Run("list payment initiations test cursor", func(t *testing.T) { + q := NewListPaymentInitiationsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationQuery{}). + WithPageSize(1), + ) + + cursor, err := store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.True(t, cursor.HasMore) + require.Empty(t, cursor.Previous) + require.NotEmpty(t, cursor.Next) + comparePaymentInitiations(t, defaultPaymentInitiations[1], cursor.Data[0]) + + err = bunpaginate.UnmarshalCursor(cursor.Next, &q) + require.NoError(t, err) + cursor, err = store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.True(t, cursor.HasMore) + require.NotEmpty(t, cursor.Previous) + require.NotEmpty(t, cursor.Next) + comparePaymentInitiations(t, defaultPaymentInitiations[2], cursor.Data[0]) + + err = bunpaginate.UnmarshalCursor(cursor.Next, &q) + require.NoError(t, err) + cursor, err = store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.False(t, cursor.HasMore) + require.NotEmpty(t, cursor.Previous) + require.Empty(t, cursor.Next) + comparePaymentInitiations(t, defaultPaymentInitiations[0], cursor.Data[0]) + + err = bunpaginate.UnmarshalCursor(cursor.Previous, &q) + require.NoError(t, err) + cursor, err = store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.True(t, cursor.HasMore) + require.NotEmpty(t, cursor.Previous) + require.NotEmpty(t, cursor.Next) + comparePaymentInitiations(t, defaultPaymentInitiations[2], cursor.Data[0]) + + err = bunpaginate.UnmarshalCursor(cursor.Previous, &q) + require.NoError(t, err) + cursor, err = store.PaymentInitiationsList(ctx, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.True(t, cursor.HasMore) + require.Empty(t, cursor.Previous) + require.NotEmpty(t, cursor.Next) + comparePaymentInitiations(t, defaultPaymentInitiations[1], cursor.Data[0]) + }) +} + +func upsertPaymentInitiationRelatedPayments(t *testing.T, ctx context.Context, storage Storage) { + require.NoError(t, storage.PaymentInitiationRelatedPaymentsUpsert(ctx, piID1, defaultPayments[0].ID, now.Add(-10*time.Minute).UTC().Time)) + require.NoError(t, storage.PaymentInitiationRelatedPaymentsUpsert(ctx, piID1, defaultPayments[1].ID, now.Add(-5*time.Minute).UTC().Time)) + require.NoError(t, storage.PaymentInitiationRelatedPaymentsUpsert(ctx, piID1, defaultPayments[2].ID, now.Add(-7*time.Minute).UTC().Time)) + require.NoError(t, storage.PaymentInitiationRelatedPaymentsUpsert(ctx, piID2, defaultPayments[0].ID, now.Add(-7*time.Minute).UTC().Time)) +} + +func TestPaymentInitiationsRelatedPaymentUpsert(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + upsertConnector(t, ctx, store, defaultConnector) + upsertAccounts(t, ctx, store, defaultAccounts) + upsertPayments(t, ctx, store, defaultPayments) + upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations) + upsertPaymentInitiationRelatedPayments(t, ctx, store) + + t.Run("same id insert", func(t *testing.T) { + require.NoError(t, store.PaymentInitiationRelatedPaymentsUpsert(ctx, piID1, defaultPayments[0].ID, now.Add(-10*time.Minute).UTC().Time)) + + cursor, err := store.PaymentInitiationRelatedPaymentsList( + ctx, + piID1, + NewListPaymentInitiationRelatedPaymentsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationRelatedPaymentsQuery{}). + WithPageSize(15), + ), + ) + require.NoError(t, err) + require.Len(t, cursor.Data, 3) + require.False(t, cursor.HasMore) + comparePayments(t, defaultPayments[1], cursor.Data[0]) + comparePayments(t, defaultPayments[2], cursor.Data[1]) + comparePayments(t, defaultPayments[0], cursor.Data[2]) + }) + + t.Run("unknown payment initiation", func(t *testing.T) { + require.Error(t, store.PaymentInitiationRelatedPaymentsUpsert( + ctx, + models.PaymentInitiationID{}, + defaultPayments[0].ID, now.Add(-10*time.Minute).UTC().Time), + ) + }) + + t.Run("unknown payment id", func(t *testing.T) { + require.Error(t, store.PaymentInitiationRelatedPaymentsUpsert( + ctx, + piID1, + models.PaymentID{}, + now.Add(-10*time.Minute).UTC().Time), + ) + }) +} + +func TestPaymentInitiationRelatedPaymentsList(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + upsertConnector(t, ctx, store, defaultConnector) + upsertAccounts(t, ctx, store, defaultAccounts) + upsertPayments(t, ctx, store, defaultPayments) + upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations) + upsertPaymentInitiationRelatedPayments(t, ctx, store) + + t.Run("list related payments by unknown payment initiation", func(t *testing.T) { + cursor, err := store.PaymentInitiationRelatedPaymentsList( + ctx, + models.PaymentInitiationID{}, + NewListPaymentInitiationRelatedPaymentsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationRelatedPaymentsQuery{}). + WithPageSize(15), + ), + ) + require.NoError(t, err) + require.Len(t, cursor.Data, 0) + require.False(t, cursor.HasMore) + }) + + t.Run("list related payments by payment initiation", func(t *testing.T) { + q := NewListPaymentInitiationRelatedPaymentsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationRelatedPaymentsQuery{}). + WithPageSize(1), + ) + + cursor, err := store.PaymentInitiationRelatedPaymentsList(ctx, piID1, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.True(t, cursor.HasMore) + require.Empty(t, cursor.Previous) + require.NotEmpty(t, cursor.Next) + comparePayments(t, defaultPayments[1], cursor.Data[0]) + + err = bunpaginate.UnmarshalCursor(cursor.Next, &q) + require.NoError(t, err) + cursor, err = store.PaymentInitiationRelatedPaymentsList(ctx, piID1, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.True(t, cursor.HasMore) + require.NotEmpty(t, cursor.Previous) + require.NotEmpty(t, cursor.Next) + comparePayments(t, defaultPayments[2], cursor.Data[0]) + + err = bunpaginate.UnmarshalCursor(cursor.Next, &q) + require.NoError(t, err) + cursor, err = store.PaymentInitiationRelatedPaymentsList(ctx, piID1, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.False(t, cursor.HasMore) + require.NotEmpty(t, cursor.Previous) + require.Empty(t, cursor.Next) + comparePayments(t, defaultPayments[0], cursor.Data[0]) + + err = bunpaginate.UnmarshalCursor(cursor.Previous, &q) + require.NoError(t, err) + cursor, err = store.PaymentInitiationRelatedPaymentsList(ctx, piID1, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.True(t, cursor.HasMore) + require.NotEmpty(t, cursor.Previous) + require.NotEmpty(t, cursor.Next) + comparePayments(t, defaultPayments[2], cursor.Data[0]) + + err = bunpaginate.UnmarshalCursor(cursor.Previous, &q) + require.NoError(t, err) + cursor, err = store.PaymentInitiationRelatedPaymentsList(ctx, piID1, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.True(t, cursor.HasMore) + require.Empty(t, cursor.Previous) + require.NotEmpty(t, cursor.Next) + comparePayments(t, defaultPayments[1], cursor.Data[0]) + }) +} + +var ( + piAdjID1 = models.PaymentInitiationAdjustmentID{ + PaymentInitiationID: defaultPaymentInitiations[0].ID, + CreatedAt: now.Add(-10 * time.Minute).UTC().Time, + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION, + } + piAdjID2 = models.PaymentInitiationAdjustmentID{ + PaymentInitiationID: defaultPaymentInitiations[0].ID, + CreatedAt: now.Add(-5 * time.Minute).UTC().Time, + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED, + } + piAdjID3 = models.PaymentInitiationAdjustmentID{ + PaymentInitiationID: defaultPaymentInitiations[1].ID, + CreatedAt: now.Add(-7 * time.Minute).UTC().Time, + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING, + } + + defaultPaymentInitiationAdjustments = []models.PaymentInitiationAdjustment{ + { + ID: piAdjID1, + PaymentInitiationID: defaultPaymentInitiations[0].ID, + CreatedAt: now.Add(-10 * time.Minute).UTC().Time, + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION, + Metadata: map[string]string{ + "foo": "bar", + }, + }, + { + ID: piAdjID2, + PaymentInitiationID: defaultPaymentInitiations[0].ID, + CreatedAt: now.Add(-5 * time.Minute).UTC().Time, + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED, + Error: pointer.For("test"), + Metadata: map[string]string{ + "foo2": "bar2", + }, + }, + { + ID: piAdjID3, + PaymentInitiationID: defaultPaymentInitiations[1].ID, + CreatedAt: now.Add(-7 * time.Minute).UTC().Time, + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING, + Metadata: map[string]string{ + "foo3": "bar3", + }, + }, + } +) + +func upsertPaymentInitiationAdjustments(t *testing.T, ctx context.Context, storage Storage, adjustments []models.PaymentInitiationAdjustment) { + for _, adj := range adjustments { + require.NoError(t, storage.PaymentInitiationAdjustmentsUpsert(ctx, adj)) + } +} + +func TestPaymentInitiationAdjustmentsUpsert(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + upsertConnector(t, ctx, store, defaultConnector) + upsertAccounts(t, ctx, store, defaultAccounts) + upsertPayments(t, ctx, store, defaultPayments) + upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations) + upsertPaymentInitiationAdjustments(t, ctx, store, defaultPaymentInitiationAdjustments) + + t.Run("upsert with unknown payment initiation", func(t *testing.T) { + p := models.PaymentInitiationAdjustment{ + ID: models.PaymentInitiationAdjustmentID{}, + PaymentInitiationID: models.PaymentInitiationID{}, + CreatedAt: now.Add(-10 * time.Minute).UTC().Time, + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_WAITING_FOR_VALIDATION, + Metadata: map[string]string{ + "foo": "bar", + }, + } + + require.Error(t, store.PaymentInitiationAdjustmentsUpsert(ctx, p)) + }) + + t.Run("upsert with same id", func(t *testing.T) { + p := models.PaymentInitiationAdjustment{ + ID: piAdjID1, + PaymentInitiationID: defaultPaymentInitiationAdjustments[0].PaymentInitiationID, + CreatedAt: now.Add(-30 * time.Minute).UTC().Time, + Status: models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSED, + Metadata: map[string]string{ + "foo": "changed", + }, + } + + require.NoError(t, store.PaymentInitiationAdjustmentsUpsert(ctx, p)) + + for _, pa := range defaultPaymentInitiationAdjustments { + actual, err := store.PaymentInitiationAdjustmentsGet(ctx, pa.ID) + require.NoError(t, err) + comparePaymentInitiationAdjustments(t, pa, *actual) + } + }) +} + +func TestPaymentInitiationAdjustmentsGet(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + upsertConnector(t, ctx, store, defaultConnector) + upsertAccounts(t, ctx, store, defaultAccounts) + upsertPayments(t, ctx, store, defaultPayments) + upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations) + upsertPaymentInitiationAdjustments(t, ctx, store, defaultPaymentInitiationAdjustments) + + t.Run("get unknown payment initiation adjustment", func(t *testing.T) { + _, err := store.PaymentInitiationAdjustmentsGet(ctx, models.PaymentInitiationAdjustmentID{}) + require.Error(t, err) + require.ErrorIs(t, err, ErrNotFound) + }) + + t.Run("get existing payment initiation adjustment", func(t *testing.T) { + for _, pa := range defaultPaymentInitiationAdjustments { + actual, err := store.PaymentInitiationAdjustmentsGet(ctx, pa.ID) + require.NoError(t, err) + comparePaymentInitiationAdjustments(t, pa, *actual) + } + }) +} + +func TestPaymentInitiationAdjustmentsList(t *testing.T) { + t.Parallel() + + ctx := logging.TestingContext() + store := newStore(t) + + upsertConnector(t, ctx, store, defaultConnector) + upsertAccounts(t, ctx, store, defaultAccounts) + upsertPayments(t, ctx, store, defaultPayments) + upsertPaymentInitiations(t, ctx, store, defaultPaymentInitiations) + upsertPaymentInitiationAdjustments(t, ctx, store, defaultPaymentInitiationAdjustments) + + t.Run("list payment initiation adjustments by unknown payment initiation", func(t *testing.T) { + cursor, err := store.PaymentInitiationAdjustmentsList( + ctx, + models.PaymentInitiationID{}, + NewListPaymentInitiationAdjustmentsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationAdjustmentsQuery{}). + WithPageSize(15), + ), + ) + require.NoError(t, err) + require.Len(t, cursor.Data, 0) + require.False(t, cursor.HasMore) + }) + + t.Run("list payment initiation adjustments by payment initiation", func(t *testing.T) { + q := NewListPaymentInitiationAdjustmentsQuery( + bunpaginate.NewPaginatedQueryOptions(PaymentInitiationAdjustmentsQuery{}). + WithPageSize(1), + ) + + cursor, err := store.PaymentInitiationAdjustmentsList(ctx, defaultPaymentInitiationAdjustments[0].PaymentInitiationID, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.True(t, cursor.HasMore) + require.Empty(t, cursor.Previous) + require.NotEmpty(t, cursor.Next) + comparePaymentInitiationAdjustments(t, defaultPaymentInitiationAdjustments[1], cursor.Data[0]) + + err = bunpaginate.UnmarshalCursor(cursor.Next, &q) + require.NoError(t, err) + cursor, err = store.PaymentInitiationAdjustmentsList(ctx, defaultPaymentInitiationAdjustments[0].PaymentInitiationID, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.False(t, cursor.HasMore) + require.NotEmpty(t, cursor.Previous) + require.Empty(t, cursor.Next) + comparePaymentInitiationAdjustments(t, defaultPaymentInitiationAdjustments[0], cursor.Data[0]) + + err = bunpaginate.UnmarshalCursor(cursor.Previous, &q) + require.NoError(t, err) + cursor, err = store.PaymentInitiationAdjustmentsList(ctx, defaultPaymentInitiationAdjustments[0].PaymentInitiationID, q) + require.NoError(t, err) + require.Len(t, cursor.Data, 1) + require.True(t, cursor.HasMore) + require.Empty(t, cursor.Previous) + require.NotEmpty(t, cursor.Next) + comparePaymentInitiationAdjustments(t, defaultPaymentInitiationAdjustments[1], cursor.Data[0]) + }) +} + +func comparePaymentInitiations(t *testing.T, expected, actual models.PaymentInitiation) { + require.Equal(t, expected.ID, actual.ID) + require.Equal(t, expected.ConnectorID, actual.ConnectorID) + require.Equal(t, expected.Reference, actual.Reference) + require.Equal(t, expected.CreatedAt, actual.CreatedAt) + require.Equal(t, expected.ScheduledAt, actual.ScheduledAt) + require.Equal(t, expected.Description, actual.Description) + require.Equal(t, expected.Type, actual.Type) + + switch { + case expected.SourceAccountID != nil && actual.SourceAccountID != nil: + require.Equal(t, *expected.SourceAccountID, *actual.SourceAccountID) + case expected.SourceAccountID == nil && actual.SourceAccountID == nil: + default: + t.Fatalf("expected.SourceAccountID != actual.SourceAccountID") + } + + require.Equal(t, expected.DestinationAccountID, actual.DestinationAccountID) + require.Equal(t, expected.Amount, actual.Amount) + require.Equal(t, expected.Asset, actual.Asset) + + require.Equal(t, len(expected.Metadata), len(actual.Metadata)) + for k, v := range expected.Metadata { + _, ok := actual.Metadata[k] + require.True(t, ok) + require.Equal(t, v, actual.Metadata[k]) + } +} + +func comparePaymentInitiationAdjustments(t *testing.T, expected, actual models.PaymentInitiationAdjustment) { + require.Equal(t, expected.ID, actual.ID) + require.Equal(t, expected.PaymentInitiationID, actual.PaymentInitiationID) + require.Equal(t, expected.CreatedAt, actual.CreatedAt) + require.Equal(t, expected.Status, actual.Status) + require.Equal(t, expected.Error, actual.Error) + + require.Equal(t, len(expected.Metadata), len(actual.Metadata)) + for k, v := range expected.Metadata { + _, ok := actual.Metadata[k] + require.True(t, ok) + require.Equal(t, v, actual.Metadata[k]) + } +} diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 330c94bd..fa65d0a0 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -48,6 +48,23 @@ type Storage interface { PaymentsList(ctx context.Context, q ListPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) PaymentsDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error + // Payment Initiations + PaymentInitiationsUpsert(ctx context.Context, pi models.PaymentInitiation, adjustments ...models.PaymentInitiationAdjustment) error + PaymentInitiationsUpdateMetadata(ctx context.Context, piID models.PaymentInitiationID, metadata map[string]string) error + PaymentInitiationsGet(ctx context.Context, piID models.PaymentInitiationID) (*models.PaymentInitiation, error) + PaymentInitiationsDelete(ctx context.Context, piID models.PaymentInitiationID) error + PaymentInitiationsDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error + PaymentInitiationsList(ctx context.Context, q ListPaymentInitiationsQuery) (*bunpaginate.Cursor[models.PaymentInitiation], error) + + // Payment Initiation Adjustments + PaymentInitiationAdjustmentsUpsert(ctx context.Context, adj models.PaymentInitiationAdjustment) error + PaymentInitiationAdjustmentsGet(ctx context.Context, id models.PaymentInitiationAdjustmentID) (*models.PaymentInitiationAdjustment, error) + PaymentInitiationAdjustmentsList(ctx context.Context, piID models.PaymentInitiationID, q ListPaymentInitiationAdjustmentsQuery) (*bunpaginate.Cursor[models.PaymentInitiationAdjustment], error) + + // Payment Initiation Related Payments + PaymentInitiationRelatedPaymentsUpsert(ctx context.Context, piID models.PaymentInitiationID, pID models.PaymentID, createdAt time.Time) error + PaymentInitiationRelatedPaymentsList(ctx context.Context, piID models.PaymentInitiationID, q ListPaymentInitiationRelatedPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) + // Pools PoolsUpsert(ctx context.Context, pool models.Pool) error PoolsGet(ctx context.Context, id uuid.UUID) (*models.Pool, error) diff --git a/internal/storage/storage_generated.go b/internal/storage/storage_generated.go index afd32e76..030d4cb9 100644 --- a/internal/storage/storage_generated.go +++ b/internal/storage/storage_generated.go @@ -389,6 +389,170 @@ func (mr *MockStorageMockRecorder) InstancesUpsert(ctx, instance any) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "InstancesUpsert", reflect.TypeOf((*MockStorage)(nil).InstancesUpsert), ctx, instance) } +// PaymentInitiationAdjustmentsGet mocks base method. +func (m *MockStorage) PaymentInitiationAdjustmentsGet(ctx context.Context, id models.PaymentInitiationAdjustmentID) (*models.PaymentInitiationAdjustment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationAdjustmentsGet", ctx, id) + ret0, _ := ret[0].(*models.PaymentInitiationAdjustment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentInitiationAdjustmentsGet indicates an expected call of PaymentInitiationAdjustmentsGet. +func (mr *MockStorageMockRecorder) PaymentInitiationAdjustmentsGet(ctx, id any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationAdjustmentsGet", reflect.TypeOf((*MockStorage)(nil).PaymentInitiationAdjustmentsGet), ctx, id) +} + +// PaymentInitiationAdjustmentsList mocks base method. +func (m *MockStorage) PaymentInitiationAdjustmentsList(ctx context.Context, piID models.PaymentInitiationID, q ListPaymentInitiationAdjustmentsQuery) (*bunpaginate.Cursor[models.PaymentInitiationAdjustment], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationAdjustmentsList", ctx, piID, q) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.PaymentInitiationAdjustment]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentInitiationAdjustmentsList indicates an expected call of PaymentInitiationAdjustmentsList. +func (mr *MockStorageMockRecorder) PaymentInitiationAdjustmentsList(ctx, piID, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationAdjustmentsList", reflect.TypeOf((*MockStorage)(nil).PaymentInitiationAdjustmentsList), ctx, piID, q) +} + +// PaymentInitiationAdjustmentsUpsert mocks base method. +func (m *MockStorage) PaymentInitiationAdjustmentsUpsert(ctx context.Context, adj models.PaymentInitiationAdjustment) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationAdjustmentsUpsert", ctx, adj) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentInitiationAdjustmentsUpsert indicates an expected call of PaymentInitiationAdjustmentsUpsert. +func (mr *MockStorageMockRecorder) PaymentInitiationAdjustmentsUpsert(ctx, adj any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationAdjustmentsUpsert", reflect.TypeOf((*MockStorage)(nil).PaymentInitiationAdjustmentsUpsert), ctx, adj) +} + +// PaymentInitiationRelatedPaymentsList mocks base method. +func (m *MockStorage) PaymentInitiationRelatedPaymentsList(ctx context.Context, piID models.PaymentInitiationID, q ListPaymentInitiationRelatedPaymentsQuery) (*bunpaginate.Cursor[models.Payment], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationRelatedPaymentsList", ctx, piID, q) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.Payment]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentInitiationRelatedPaymentsList indicates an expected call of PaymentInitiationRelatedPaymentsList. +func (mr *MockStorageMockRecorder) PaymentInitiationRelatedPaymentsList(ctx, piID, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationRelatedPaymentsList", reflect.TypeOf((*MockStorage)(nil).PaymentInitiationRelatedPaymentsList), ctx, piID, q) +} + +// PaymentInitiationRelatedPaymentsUpsert mocks base method. +func (m *MockStorage) PaymentInitiationRelatedPaymentsUpsert(ctx context.Context, piID models.PaymentInitiationID, pID models.PaymentID, createdAt time.Time) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationRelatedPaymentsUpsert", ctx, piID, pID, createdAt) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentInitiationRelatedPaymentsUpsert indicates an expected call of PaymentInitiationRelatedPaymentsUpsert. +func (mr *MockStorageMockRecorder) PaymentInitiationRelatedPaymentsUpsert(ctx, piID, pID, createdAt any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationRelatedPaymentsUpsert", reflect.TypeOf((*MockStorage)(nil).PaymentInitiationRelatedPaymentsUpsert), ctx, piID, pID, createdAt) +} + +// PaymentInitiationsDelete mocks base method. +func (m *MockStorage) PaymentInitiationsDelete(ctx context.Context, piID models.PaymentInitiationID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationsDelete", ctx, piID) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentInitiationsDelete indicates an expected call of PaymentInitiationsDelete. +func (mr *MockStorageMockRecorder) PaymentInitiationsDelete(ctx, piID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsDelete", reflect.TypeOf((*MockStorage)(nil).PaymentInitiationsDelete), ctx, piID) +} + +// PaymentInitiationsDeleteFromConnectorID mocks base method. +func (m *MockStorage) PaymentInitiationsDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationsDeleteFromConnectorID", ctx, connectorID) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentInitiationsDeleteFromConnectorID indicates an expected call of PaymentInitiationsDeleteFromConnectorID. +func (mr *MockStorageMockRecorder) PaymentInitiationsDeleteFromConnectorID(ctx, connectorID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsDeleteFromConnectorID", reflect.TypeOf((*MockStorage)(nil).PaymentInitiationsDeleteFromConnectorID), ctx, connectorID) +} + +// PaymentInitiationsGet mocks base method. +func (m *MockStorage) PaymentInitiationsGet(ctx context.Context, piID models.PaymentInitiationID) (*models.PaymentInitiation, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationsGet", ctx, piID) + ret0, _ := ret[0].(*models.PaymentInitiation) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentInitiationsGet indicates an expected call of PaymentInitiationsGet. +func (mr *MockStorageMockRecorder) PaymentInitiationsGet(ctx, piID any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsGet", reflect.TypeOf((*MockStorage)(nil).PaymentInitiationsGet), ctx, piID) +} + +// PaymentInitiationsList mocks base method. +func (m *MockStorage) PaymentInitiationsList(ctx context.Context, q ListPaymentInitiationsQuery) (*bunpaginate.Cursor[models.PaymentInitiation], error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationsList", ctx, q) + ret0, _ := ret[0].(*bunpaginate.Cursor[models.PaymentInitiation]) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PaymentInitiationsList indicates an expected call of PaymentInitiationsList. +func (mr *MockStorageMockRecorder) PaymentInitiationsList(ctx, q any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsList", reflect.TypeOf((*MockStorage)(nil).PaymentInitiationsList), ctx, q) +} + +// PaymentInitiationsUpdateMetadata mocks base method. +func (m *MockStorage) PaymentInitiationsUpdateMetadata(ctx context.Context, piID models.PaymentInitiationID, metadata map[string]string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PaymentInitiationsUpdateMetadata", ctx, piID, metadata) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentInitiationsUpdateMetadata indicates an expected call of PaymentInitiationsUpdateMetadata. +func (mr *MockStorageMockRecorder) PaymentInitiationsUpdateMetadata(ctx, piID, metadata any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsUpdateMetadata", reflect.TypeOf((*MockStorage)(nil).PaymentInitiationsUpdateMetadata), ctx, piID, metadata) +} + +// PaymentInitiationsUpsert mocks base method. +func (m *MockStorage) PaymentInitiationsUpsert(ctx context.Context, pi models.PaymentInitiation, adjustments ...models.PaymentInitiationAdjustment) error { + m.ctrl.T.Helper() + varargs := []any{ctx, pi} + for _, a := range adjustments { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "PaymentInitiationsUpsert", varargs...) + ret0, _ := ret[0].(error) + return ret0 +} + +// PaymentInitiationsUpsert indicates an expected call of PaymentInitiationsUpsert. +func (mr *MockStorageMockRecorder) PaymentInitiationsUpsert(ctx, pi any, adjustments ...any) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]any{ctx, pi}, adjustments...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PaymentInitiationsUpsert", reflect.TypeOf((*MockStorage)(nil).PaymentInitiationsUpsert), varargs...) +} + // PaymentsDeleteFromConnectorID mocks base method. func (m *MockStorage) PaymentsDeleteFromConnectorID(ctx context.Context, connectorID models.ConnectorID) error { m.ctrl.T.Helper()