mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
* chore: rename RunNextTasks workflow * feat: added a trimmed down type for connector only containing the id * feat: make runNextTask use connectorIdOnly * feat: added isScheduledForDeletion to plugin interface * feat: add scheduledForDeletion at plugin level * feat: refactor manager.Load to take a connector instead of parts of it * feat: saves the connector's scheduledFOrDeletion into the plugin * feat: use plugin.IsScheduledForDeletion instead of loading connector * fix: make e2e tests less flaky * fix: remove config from most request types * feat: (WIP) remove config from runNextTask * fix: remove unneeded method definitions in each plugin * fix: simplify RunNextFlow as anyway we need to change the parameters * fix: add a workflow compatible with previous version for runNextTasks * fix(EN-577): added back RunSendEvents as a deprecated workflow * fix(EN-577): added back RunUpdatePaymentInitiationFromPayment as a deprecated workflow * fix: fix linting directives * fix: replace old usage with refactored one * fix: added back missing deprecated activities * fix: ensure fetch_next_balance works with v3.0 code * fix: simplify a bit the versionning etc * fix: quick refactor before applying solution elsewhere * fix: add back RunSendEvents and RunNextTask child workflows where necessary for backward compatibility * fix: add back missing activity, seems like it's mandatory? * fix: run payment initiation update as child workflow when necessary * fix: missing activy call in test, refine some todos * fix: lint * fix: ensure we always reload plugins into the manager -- so exposing isScheduledForDeletion * fix: ensure we stop storing clear config in temporal * fix: make store config support plain text config * fix: some comments * fix: change runSendEvent workflow to be match what we had in 3.0 * feat: enable worker versionning to avoid workflow from new version to get executed on old code * fix: lint * fix: revert worker versionning * fix: minor code review comments * fix: add context to Encrypt & Decrypt methods * fix: improve lint, add couple tests * fix: use atomic to handle isScheduledForDeletion * fix: make create_events work on migration * fix: avoid returning channel; just send what is needed in runSendEventAsChildWorkflow and runNextTaskAsChildWorkflow * fix: remove extra call to installWorkflow on start plugin * fix: typo in error log line * fix: simplify errChan initialization * fix: clarify feature flag names
764 lines
24 KiB
Go
764 lines
24 KiB
Go
package workflow
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"github.com/formancehq/payments/internal/connectors/engine/activities"
|
|
internalEvents "github.com/formancehq/payments/internal/events"
|
|
"github.com/formancehq/payments/internal/models"
|
|
"github.com/formancehq/payments/pkg/events"
|
|
"github.com/google/uuid"
|
|
"github.com/pkg/errors"
|
|
"go.temporal.io/api/enums/v1"
|
|
"go.temporal.io/sdk/temporal"
|
|
"go.temporal.io/sdk/workflow"
|
|
)
|
|
|
|
type HandleWebhooks struct {
|
|
ConnectorID models.ConnectorID
|
|
URL string
|
|
URLPath string
|
|
Webhook models.Webhook
|
|
Config *models.WebhookConfig
|
|
}
|
|
|
|
func (w Workflow) runHandleWebhooks(
|
|
ctx workflow.Context,
|
|
handleWebhooks HandleWebhooks,
|
|
) error {
|
|
err := activities.StorageWebhooksStore(infiniteRetryContext(ctx), handleWebhooks.Webhook)
|
|
if err != nil {
|
|
return fmt.Errorf("storing webhook: %w", err)
|
|
}
|
|
|
|
if handleWebhooks.Config == nil {
|
|
return fmt.Errorf("invalid config for webhook %q", handleWebhooks.Webhook.ID)
|
|
}
|
|
|
|
resp, err := activities.PluginTranslateWebhook(
|
|
infiniteRetryContext(ctx),
|
|
handleWebhooks.ConnectorID,
|
|
models.TranslateWebhookRequest{
|
|
Name: handleWebhooks.Config.Name,
|
|
Webhook: models.PSPWebhook{
|
|
BasicAuth: handleWebhooks.Webhook.BasicAuth,
|
|
QueryValues: handleWebhooks.Webhook.QueryValues,
|
|
Headers: handleWebhooks.Webhook.Headers,
|
|
Body: handleWebhooks.Webhook.Body,
|
|
},
|
|
Config: handleWebhooks.Config,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("translating webhook: %w", err)
|
|
}
|
|
|
|
for i, response := range resp.Responses {
|
|
switch {
|
|
case response.DataReadyToFetch != nil:
|
|
// A webhook has been received from the connector indicating that
|
|
// there is new data to fetch from the connector.
|
|
// Let's launch the related workflow to fetch the data.
|
|
if err := w.handleOpenBankingDataReadyToFetchWebhook(ctx, handleWebhooks, response); err != nil {
|
|
return fmt.Errorf("handling open banking webhook: %w", err)
|
|
}
|
|
case response.UserLinkSessionFinished != nil:
|
|
// OpenBanking specific webhook. A user has finished the link flow
|
|
// and has a valid connection to his bank. We need to update the
|
|
// open banking status to active and send an event to the user.
|
|
if err := w.handleUserLinkSessionFinishedWebhook(ctx, response); err != nil {
|
|
return fmt.Errorf("handling user link session finished webhook: %w", err)
|
|
}
|
|
|
|
case response.UserDisconnected != nil:
|
|
// OpenBanking specific webhook. A user has disconnected was totally
|
|
// disconnected from the open banking connector. We need to update
|
|
// the open banking status to disconnected and send an event to the
|
|
// user.
|
|
if err := w.handleUserDisconnectedWebhook(ctx, handleWebhooks, response); err != nil {
|
|
return fmt.Errorf("handling user disconnected webhook: %w", err)
|
|
}
|
|
|
|
case response.UserConnectionDisconnected != nil:
|
|
// OpenBanking specific webhook. A user has disconnected from his
|
|
// bank. We need to update the open banking status to disconnected
|
|
// and send an event to the user.
|
|
if err := w.handleUserConnectionDisconnectedWebhook(ctx, handleWebhooks, response); err != nil {
|
|
return fmt.Errorf("handling user disconnected webhook: %w", err)
|
|
}
|
|
|
|
case response.UserConnectionReconnected != nil:
|
|
// OpenBanking specific webhook. A user has reconnected to his bank.
|
|
// We need to update the open banking status to active and send an
|
|
// event to the user.
|
|
if err := w.handleUserConnectionReconnectedWebhook(ctx, handleWebhooks, response); err != nil {
|
|
return fmt.Errorf("handling user reconnected webhook: %w", err)
|
|
}
|
|
|
|
case response.UserConnectionPendingDisconnect != nil:
|
|
// OpenBanking specific webhook. A user is nearly disconnected from
|
|
// his bank. We need to send an event to the user to warn him.
|
|
if err := w.handleUserPendingDisconnectWebhook(ctx, handleWebhooks, response); err != nil {
|
|
return fmt.Errorf("handling user pending disconnect webhook: %w", err)
|
|
}
|
|
|
|
case response.OpenBankingAccount != nil:
|
|
// OpenBanking specific webhook. A new account has been found in the
|
|
// bank. We need to store the account in the database.
|
|
if err := w.handleOpenBankingAccountWebhook(ctx, i, handleWebhooks, response); err != nil {
|
|
return fmt.Errorf("handling open banking account webhook: %w", err)
|
|
}
|
|
|
|
case response.OpenBankingPayment != nil:
|
|
// OpenBanking specific webhook. A new payment has been found in the
|
|
// bank. We need to store the payment in the database.
|
|
if err := w.handleOpenBankingPaymentWebhook(ctx, i, handleWebhooks, response); err != nil {
|
|
return fmt.Errorf("handling open banking payment webhook: %w", err)
|
|
}
|
|
|
|
default:
|
|
// Default case, all the other webhooks are to store data
|
|
if err := w.handleDataToStoreWebhook(ctx, i, handleWebhooks, response); err != nil {
|
|
return fmt.Errorf("handling data to store webhook: %w", err)
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w Workflow) handleDataToStoreWebhook(
|
|
ctx workflow.Context,
|
|
index int,
|
|
handleWebhooks HandleWebhooks,
|
|
response models.WebhookResponse,
|
|
) error {
|
|
// TODO do we need a child workflow, or an activity could do?
|
|
if err := workflow.ExecuteChildWorkflow(
|
|
workflow.WithChildOptions(
|
|
ctx,
|
|
workflow.ChildWorkflowOptions{
|
|
WorkflowID: fmt.Sprintf("store-webhook-%s-%s-%s-%d", w.stack, handleWebhooks.ConnectorID.String(), handleWebhooks.Webhook.ID, index),
|
|
TaskQueue: w.getDefaultTaskQueue(),
|
|
ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON,
|
|
WorkflowIDReusePolicy: enums.WORKFLOW_ID_REUSE_POLICY_ALLOW_DUPLICATE_FAILED_ONLY,
|
|
SearchAttributes: map[string]interface{}{
|
|
SearchAttributeStack: w.stack,
|
|
},
|
|
},
|
|
),
|
|
RunStoreWebhookTranslation,
|
|
StoreWebhookTranslation{
|
|
ConnectorID: handleWebhooks.ConnectorID,
|
|
Account: response.Account,
|
|
ExternalAccount: response.ExternalAccount,
|
|
Payment: response.Payment,
|
|
PaymentToDelete: response.PaymentToDelete,
|
|
PaymentToCancel: response.PaymentToCancel,
|
|
Balance: response.Balance,
|
|
},
|
|
).Get(ctx, nil); err != nil {
|
|
applicationError := &temporal.ApplicationError{}
|
|
if errors.As(err, &applicationError) {
|
|
if applicationError.Type() != "ChildWorkflowExecutionAlreadyStartedError" {
|
|
return err
|
|
}
|
|
} else {
|
|
return fmt.Errorf("storing webhook translation: %w", err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w Workflow) handleOpenBankingAccountWebhook(
|
|
ctx workflow.Context,
|
|
index int,
|
|
handleWebhooks HandleWebhooks,
|
|
response models.WebhookResponse,
|
|
) error {
|
|
account := models.PSPAccount{
|
|
Reference: response.OpenBankingAccount.Reference,
|
|
CreatedAt: response.OpenBankingAccount.CreatedAt,
|
|
Name: response.OpenBankingAccount.Name,
|
|
DefaultAsset: response.OpenBankingAccount.DefaultAsset,
|
|
Metadata: response.OpenBankingAccount.Metadata,
|
|
Raw: response.OpenBankingAccount.Raw,
|
|
}
|
|
|
|
if account.Metadata == nil {
|
|
account.Metadata = make(map[string]string)
|
|
}
|
|
|
|
if response.OpenBankingAccount.OpenBankingUserID != nil {
|
|
forwardedUser, err := activities.StorageOpenBankingForwardedUsersGetByPSPUserID(
|
|
infiniteRetryContext(ctx),
|
|
*response.OpenBankingAccount.OpenBankingUserID,
|
|
handleWebhooks.ConnectorID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("getting open banking forwarded user: %w", err)
|
|
}
|
|
|
|
account.PsuID = &forwardedUser.PsuID
|
|
}
|
|
|
|
if response.OpenBankingAccount.OpenBankingConnectionID != nil {
|
|
account.OpenBankingConnectionID = response.OpenBankingAccount.OpenBankingConnectionID
|
|
}
|
|
|
|
return w.handleDataToStoreWebhook(ctx, index, handleWebhooks, models.WebhookResponse{
|
|
Account: &account,
|
|
})
|
|
}
|
|
|
|
func (w Workflow) handleOpenBankingPaymentWebhook(
|
|
ctx workflow.Context,
|
|
index int,
|
|
handleWebhooks HandleWebhooks,
|
|
response models.WebhookResponse,
|
|
) error {
|
|
payment := models.PSPPayment{
|
|
ParentReference: response.OpenBankingPayment.ParentReference,
|
|
Reference: response.OpenBankingPayment.Reference,
|
|
CreatedAt: response.OpenBankingPayment.CreatedAt,
|
|
Type: response.OpenBankingPayment.Type,
|
|
Amount: response.OpenBankingPayment.Amount,
|
|
Asset: response.OpenBankingPayment.Asset,
|
|
Scheme: response.OpenBankingPayment.Scheme,
|
|
Status: response.OpenBankingPayment.Status,
|
|
SourceAccountReference: response.OpenBankingPayment.SourceAccountReference,
|
|
DestinationAccountReference: response.OpenBankingPayment.DestinationAccountReference,
|
|
Metadata: response.OpenBankingPayment.Metadata,
|
|
Raw: response.OpenBankingPayment.Raw,
|
|
}
|
|
|
|
if payment.Metadata == nil {
|
|
payment.Metadata = make(map[string]string)
|
|
}
|
|
|
|
if response.OpenBankingPayment.OpenBankingUserID != nil {
|
|
forwardedUser, err := activities.StorageOpenBankingForwardedUsersGetByPSPUserID(
|
|
infiniteRetryContext(ctx),
|
|
*response.OpenBankingPayment.OpenBankingUserID,
|
|
handleWebhooks.ConnectorID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("getting open banking forwardedUser: %w", err)
|
|
}
|
|
|
|
payment.PsuID = &forwardedUser.PsuID
|
|
}
|
|
|
|
if response.OpenBankingPayment.OpenBankingConnectionID != nil {
|
|
payment.OpenBankingConnectionID = response.OpenBankingPayment.OpenBankingConnectionID
|
|
}
|
|
|
|
return w.handleDataToStoreWebhook(ctx, index, handleWebhooks, models.WebhookResponse{
|
|
Payment: &payment,
|
|
})
|
|
}
|
|
|
|
// TODO do we need a child workflow, or an activity could do?
|
|
func (w Workflow) handleOpenBankingDataReadyToFetchWebhook(
|
|
ctx workflow.Context,
|
|
handleWebhooks HandleWebhooks,
|
|
response models.WebhookResponse,
|
|
) error {
|
|
var conn *models.OpenBankingConnection
|
|
var obForwardedUser *models.OpenBankingForwardedUser
|
|
var psuID uuid.UUID
|
|
var connectionID string
|
|
if response.DataReadyToFetch.PSUID != nil {
|
|
openBankingForwardedUser, err := activities.StorageOpenBankingForwardedUsersGet(
|
|
infiniteRetryContext(ctx),
|
|
*response.DataReadyToFetch.PSUID,
|
|
handleWebhooks.ConnectorID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("getting open banking: %w", err)
|
|
}
|
|
|
|
obForwardedUser = openBankingForwardedUser
|
|
psuID = obForwardedUser.PsuID
|
|
}
|
|
|
|
if response.DataReadyToFetch.ConnectionID != nil {
|
|
connection, psu, err := activities.StorageOpenBankingConnectionsGetFromConnectionID(
|
|
infiniteRetryContext(ctx),
|
|
handleWebhooks.ConnectorID,
|
|
*response.DataReadyToFetch.ConnectionID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("getting open banking connection: %w", err)
|
|
}
|
|
|
|
conn = connection
|
|
connectionID = connection.ConnectionID
|
|
psuID = psu
|
|
}
|
|
|
|
payload, err := json.Marshal(&models.OpenBankingForwardedUserFromPayload{
|
|
PSUID: psuID,
|
|
OpenBankingForwardedUser: obForwardedUser,
|
|
OpenBankingConnection: conn,
|
|
FromPayload: response.DataReadyToFetch.FromPayload,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("marshalling open banking from payload: %w", err)
|
|
}
|
|
|
|
fromPayload := &FromPayload{
|
|
ID: func() string {
|
|
if response.DataReadyToFetch.ConnectionID != nil {
|
|
return *response.DataReadyToFetch.ConnectionID
|
|
}
|
|
|
|
return ""
|
|
}(),
|
|
Payload: payload,
|
|
}
|
|
|
|
if err := workflow.ExecuteChildWorkflow(
|
|
workflow.WithChildOptions(
|
|
ctx,
|
|
workflow.ChildWorkflowOptions{
|
|
TaskQueue: w.getDefaultTaskQueue(),
|
|
ParentClosePolicy: enums.PARENT_CLOSE_POLICY_ABANDON,
|
|
SearchAttributes: map[string]interface{}{
|
|
SearchAttributeStack: w.stack,
|
|
},
|
|
},
|
|
),
|
|
RunFetchOpenBankingData,
|
|
FetchOpenBankingData{
|
|
PsuID: psuID,
|
|
ConnectionID: connectionID,
|
|
ConnectorID: handleWebhooks.ConnectorID,
|
|
DataToFetch: response.DataReadyToFetch.DataToFetch,
|
|
FromPayload: fromPayload,
|
|
},
|
|
[]models.ConnectorTaskTree{},
|
|
).GetChildWorkflowExecution().Get(ctx, nil); err != nil {
|
|
return fmt.Errorf("running %s: %w", RunFetchOpenBankingData, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w Workflow) handleUserLinkSessionFinishedWebhook(
|
|
ctx workflow.Context,
|
|
response models.WebhookResponse,
|
|
) error {
|
|
|
|
err := activities.StorageOpenBankingConnectionAttemptsUpdateStatus(
|
|
infiniteRetryContext(ctx),
|
|
response.UserLinkSessionFinished.AttemptID,
|
|
response.UserLinkSessionFinished.Status,
|
|
response.UserLinkSessionFinished.Error,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("updating open banking connection attempt status: %w", err)
|
|
}
|
|
|
|
// Outbox event is now created automatically in OpenBankingConnectionAttemptsUpdateStatus
|
|
return nil
|
|
}
|
|
|
|
func (w Workflow) handleUserPendingDisconnectWebhook(
|
|
ctx workflow.Context,
|
|
handleWebhooks HandleWebhooks,
|
|
response models.WebhookResponse,
|
|
) error {
|
|
_, psuID, err := activities.StorageOpenBankingConnectionsGetFromConnectionID(
|
|
infiniteRetryContext(ctx),
|
|
handleWebhooks.ConnectorID,
|
|
response.UserConnectionPendingDisconnect.ConnectionID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("getting open banking connection: %w", err)
|
|
}
|
|
|
|
userPendingDisconnect := models.UserConnectionPendingDisconnect{
|
|
PsuID: psuID,
|
|
ConnectorID: handleWebhooks.ConnectorID,
|
|
ConnectionID: response.UserConnectionPendingDisconnect.ConnectionID,
|
|
At: response.UserConnectionPendingDisconnect.At,
|
|
Reason: response.UserConnectionPendingDisconnect.Reason,
|
|
}
|
|
|
|
// Create outbox event for user pending disconnect
|
|
// Note -- as we are storing the event as an independant entity, we are relying on an
|
|
// independent activity to emit the event (without transaction)
|
|
payload := internalEvents.OpenBankingUserConnectionPendingDisconnect{
|
|
PsuID: userPendingDisconnect.PsuID,
|
|
ConnectorID: userPendingDisconnect.ConnectorID.String(),
|
|
ConnectionID: userPendingDisconnect.ConnectionID,
|
|
At: userPendingDisconnect.At,
|
|
Reason: userPendingDisconnect.Reason,
|
|
}
|
|
|
|
payloadBytes, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal user pending disconnect event payload: %w", err)
|
|
}
|
|
|
|
idempotencyKey := models.IdempotencyKey(payload)
|
|
outboxEvent := models.OutboxEvent{
|
|
ID: models.EventID{
|
|
EventIdempotencyKey: idempotencyKey,
|
|
ConnectorID: &userPendingDisconnect.ConnectorID,
|
|
},
|
|
EventType: events.EventTypeOpenBankingUserConnectionPendingDisconnect,
|
|
EntityID: userPendingDisconnect.ConnectionID,
|
|
Payload: payloadBytes,
|
|
CreatedAt: workflow.Now(ctx).UTC(),
|
|
Status: models.OUTBOX_STATUS_PENDING,
|
|
ConnectorID: &userPendingDisconnect.ConnectorID,
|
|
}
|
|
|
|
if err := activities.StorageOutboxEventsInsert(
|
|
infiniteRetryContext(ctx),
|
|
[]models.OutboxEvent{outboxEvent},
|
|
); err != nil {
|
|
return fmt.Errorf("failed to insert user pending disconnect outbox event: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w Workflow) handleUserDisconnectedWebhook(
|
|
ctx workflow.Context,
|
|
handleWebhooks HandleWebhooks,
|
|
response models.WebhookResponse,
|
|
) error {
|
|
openBanking, err := activities.StorageOpenBankingForwardedUsersGetByPSPUserID(
|
|
infiniteRetryContext(ctx),
|
|
response.UserDisconnected.PSPUserID,
|
|
handleWebhooks.ConnectorID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("getting open banking: %w", err)
|
|
}
|
|
|
|
userDisconnected := models.UserDisconnected{
|
|
PsuID: openBanking.PsuID,
|
|
ConnectorID: handleWebhooks.ConnectorID,
|
|
At: workflow.Now(ctx),
|
|
}
|
|
|
|
// Create outbox event for user disconnected
|
|
// Note -- as we are storing the event as an independant entity, we are relying on an
|
|
// independent activity to emit the event (without transaction)
|
|
payload := internalEvents.OpenBankingUserDisconnected{
|
|
PsuID: userDisconnected.PsuID,
|
|
ConnectorID: userDisconnected.ConnectorID.String(),
|
|
At: userDisconnected.At,
|
|
Reason: nil, // UserDisconnected doesn't have a reason field
|
|
}
|
|
|
|
payloadBytes, err := json.Marshal(payload)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to marshal user disconnected event payload: %w", err)
|
|
}
|
|
|
|
idempotencyKey := models.IdempotencyKey(payload)
|
|
outboxEvent := models.OutboxEvent{
|
|
ID: models.EventID{
|
|
EventIdempotencyKey: idempotencyKey,
|
|
ConnectorID: &userDisconnected.ConnectorID,
|
|
},
|
|
EventType: events.EventTypeOpenBankingUserDisconnected,
|
|
EntityID: userDisconnected.PsuID.String(),
|
|
Payload: payloadBytes,
|
|
CreatedAt: workflow.Now(ctx).UTC(),
|
|
Status: models.OUTBOX_STATUS_PENDING,
|
|
ConnectorID: &userDisconnected.ConnectorID,
|
|
}
|
|
|
|
if err := activities.StorageOutboxEventsInsert(
|
|
infiniteRetryContext(ctx),
|
|
[]models.OutboxEvent{outboxEvent},
|
|
); err != nil {
|
|
return fmt.Errorf("failed to insert user disconnected outbox event: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (w Workflow) handleUserConnectionDisconnectedWebhook(
|
|
ctx workflow.Context,
|
|
handleWebhooks HandleWebhooks,
|
|
response models.WebhookResponse,
|
|
) error {
|
|
connection, psuID, err := activities.StorageOpenBankingConnectionsGetFromConnectionID(
|
|
infiniteRetryContext(ctx),
|
|
handleWebhooks.ConnectorID,
|
|
response.UserConnectionDisconnected.ConnectionID,
|
|
)
|
|
if err != nil {
|
|
if response.UserConnectionDisconnected.PSPUserID == "" {
|
|
// Nothing more to do, we're missing crucial information in order to continue
|
|
return fmt.Errorf("getting open banking connection: %w", err)
|
|
}
|
|
|
|
// Let's try to fetch the psu via the forwarded user
|
|
user, errGetUser := activities.StorageOpenBankingForwardedUsersGetByPSPUserID(
|
|
infiniteRetryContext(ctx),
|
|
response.UserConnectionDisconnected.PSPUserID,
|
|
handleWebhooks.ConnectorID,
|
|
)
|
|
if errGetUser != nil {
|
|
return fmt.Errorf("error getting connection: %w and getting forwarded user by pspuserID: %w", err, errGetUser)
|
|
}
|
|
|
|
psuID = user.PsuID
|
|
}
|
|
|
|
updatedConnection := craftUpdatedConnection(
|
|
ctx,
|
|
response.UserConnectionDisconnected.ConnectionID,
|
|
handleWebhooks.ConnectorID,
|
|
connection,
|
|
models.ConnectionStatusError,
|
|
response.UserConnectionDisconnected.Reason,
|
|
)
|
|
|
|
err = activities.StorageOpenBankingConnectionsStore(
|
|
infiniteRetryContext(ctx),
|
|
psuID,
|
|
updatedConnection,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("storing open banking connection: %w", err)
|
|
}
|
|
|
|
// Outbox event is now created automatically in OpenBankingConnectionsUpsert based on connection status
|
|
return nil
|
|
}
|
|
|
|
func (w Workflow) handleUserConnectionReconnectedWebhook(
|
|
ctx workflow.Context,
|
|
handleWebhooks HandleWebhooks,
|
|
response models.WebhookResponse,
|
|
) error {
|
|
connection, psuID, err := activities.StorageOpenBankingConnectionsGetFromConnectionID(
|
|
infiniteRetryContext(ctx),
|
|
handleWebhooks.ConnectorID,
|
|
response.UserConnectionReconnected.ConnectionID,
|
|
)
|
|
if err != nil {
|
|
if response.UserConnectionReconnected.PSPUserID == "" {
|
|
// Nothing more to do, we're missing crucial information in order to continue
|
|
return fmt.Errorf("getting open banking connection: %w", err)
|
|
}
|
|
|
|
// Let's try to fetch the ob forwarded user
|
|
user, errGetUser := activities.StorageOpenBankingForwardedUsersGetByPSPUserID(
|
|
infiniteRetryContext(ctx),
|
|
response.UserConnectionReconnected.PSPUserID,
|
|
handleWebhooks.ConnectorID,
|
|
)
|
|
if errGetUser != nil {
|
|
return fmt.Errorf("error getting connection: %w and getting forwarded user by psuId: %w", err, errGetUser)
|
|
}
|
|
|
|
psuID = user.PsuID
|
|
}
|
|
|
|
updatedConnection := craftUpdatedConnection(
|
|
ctx,
|
|
response.UserConnectionReconnected.ConnectionID,
|
|
handleWebhooks.ConnectorID,
|
|
connection,
|
|
models.ConnectionStatusActive,
|
|
nil,
|
|
)
|
|
|
|
err = activities.StorageOpenBankingConnectionsStore(
|
|
infiniteRetryContext(ctx),
|
|
psuID,
|
|
updatedConnection,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("storing open banking connection: %w", err)
|
|
}
|
|
|
|
// Outbox event is now created automatically in OpenBankingConnectionsUpsert based on connection status
|
|
return nil
|
|
}
|
|
|
|
const RunHandleWebhooks = "RunHandleWebhooks"
|
|
|
|
type StoreWebhookTranslation struct {
|
|
ConnectorID models.ConnectorID
|
|
Account *models.PSPAccount
|
|
ExternalAccount *models.PSPAccount
|
|
Payment *models.PSPPayment
|
|
PaymentToDelete *models.PSPPaymentsToDelete
|
|
PaymentToCancel *models.PSPPaymentsToCancel
|
|
Balance *models.PSPBalance
|
|
}
|
|
|
|
func (w Workflow) runStoreWebhookTranslation(
|
|
ctx workflow.Context,
|
|
storeWebhookTranslation StoreWebhookTranslation,
|
|
) error {
|
|
if storeWebhookTranslation.Account != nil {
|
|
accounts, err := models.FromPSPAccounts(
|
|
[]models.PSPAccount{*storeWebhookTranslation.Account},
|
|
models.ACCOUNT_TYPE_INTERNAL,
|
|
storeWebhookTranslation.ConnectorID,
|
|
)
|
|
if err != nil {
|
|
return temporal.NewNonRetryableApplicationError(
|
|
"failed to translate accounts",
|
|
ErrValidation,
|
|
err,
|
|
)
|
|
}
|
|
|
|
err = activities.StorageAccountsStore(
|
|
infiniteRetryContext(ctx),
|
|
accounts,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("storing next accounts: %w", err)
|
|
}
|
|
}
|
|
|
|
if storeWebhookTranslation.Balance != nil {
|
|
acc, err := activities.StorageAccountsGet(
|
|
infiniteRetryContext(ctx),
|
|
models.AccountID{
|
|
Reference: storeWebhookTranslation.Balance.AccountReference,
|
|
ConnectorID: storeWebhookTranslation.ConnectorID,
|
|
},
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to get account: %w", err)
|
|
}
|
|
|
|
balance, err := models.FromPSPBalance(
|
|
*storeWebhookTranslation.Balance,
|
|
storeWebhookTranslation.ConnectorID,
|
|
acc.PsuID,
|
|
acc.OpenBankingConnectionID,
|
|
)
|
|
if err != nil {
|
|
return temporal.NewNonRetryableApplicationError(
|
|
"failed to translate balances",
|
|
ErrValidation,
|
|
err,
|
|
)
|
|
}
|
|
|
|
err = activities.StorageBalancesStore(
|
|
infiniteRetryContext(ctx),
|
|
[]models.Balance{balance},
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("storing next balances: %w", err)
|
|
}
|
|
|
|
}
|
|
|
|
if storeWebhookTranslation.ExternalAccount != nil {
|
|
accounts, err := models.FromPSPAccounts(
|
|
[]models.PSPAccount{*storeWebhookTranslation.ExternalAccount},
|
|
models.ACCOUNT_TYPE_EXTERNAL,
|
|
storeWebhookTranslation.ConnectorID,
|
|
)
|
|
if err != nil {
|
|
return temporal.NewNonRetryableApplicationError(
|
|
"failed to translate accounts",
|
|
ErrValidation,
|
|
err,
|
|
)
|
|
}
|
|
|
|
err = activities.StorageAccountsStore(
|
|
infiniteRetryContext(ctx),
|
|
accounts,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("storing next accounts: %w", err)
|
|
}
|
|
}
|
|
|
|
if storeWebhookTranslation.Payment != nil {
|
|
payments, err := models.FromPSPPayments(
|
|
[]models.PSPPayment{*storeWebhookTranslation.Payment},
|
|
storeWebhookTranslation.ConnectorID,
|
|
)
|
|
if err != nil {
|
|
return temporal.NewNonRetryableApplicationError(
|
|
"failed to translate psp payments",
|
|
ErrValidation,
|
|
err,
|
|
)
|
|
}
|
|
|
|
err = activities.StoragePaymentsStore(
|
|
infiniteRetryContext(ctx),
|
|
payments,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("storing next payments: %w", err)
|
|
}
|
|
}
|
|
|
|
if storeWebhookTranslation.PaymentToDelete != nil {
|
|
err := activities.StoragePaymentsDeleteFromReference(
|
|
infiniteRetryContext(ctx),
|
|
storeWebhookTranslation.PaymentToDelete.Reference,
|
|
storeWebhookTranslation.ConnectorID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("deleting payment: %w", err)
|
|
}
|
|
}
|
|
|
|
if storeWebhookTranslation.PaymentToCancel != nil {
|
|
payment, err := activities.StoragePaymentsGetByReference(
|
|
infiniteRetryContext(ctx),
|
|
storeWebhookTranslation.PaymentToCancel.Reference,
|
|
storeWebhookTranslation.ConnectorID,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("getting payment: %w", err)
|
|
}
|
|
|
|
now := workflow.Now(ctx)
|
|
payment.Adjustments = []models.PaymentAdjustment{
|
|
{
|
|
ID: models.PaymentAdjustmentID{
|
|
PaymentID: payment.ID,
|
|
Reference: storeWebhookTranslation.PaymentToCancel.Reference,
|
|
CreatedAt: now,
|
|
Status: models.PAYMENT_STATUS_CANCELLED,
|
|
},
|
|
Reference: storeWebhookTranslation.PaymentToCancel.Reference,
|
|
CreatedAt: now,
|
|
Status: models.PAYMENT_STATUS_CANCELLED,
|
|
},
|
|
}
|
|
|
|
err = activities.StoragePaymentsStore(
|
|
infiniteRetryContext(ctx),
|
|
[]models.Payment{*payment},
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("storing payment: %w", err)
|
|
}
|
|
}
|
|
|
|
// All events now use outbox pattern - Account, Balance, Payment, and BankAccount events
|
|
// are created in storage methods, and other events are handled via outbox in workflows
|
|
return nil
|
|
}
|
|
|
|
const RunStoreWebhookTranslation = "RunStoreWebhookTranslation"
|