mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
* feat(EN-214): add outbox table migration * feat(EN-214): add outbox models and storage layer * feat(EN-214): implement outbox publisher activity * feat(EN-214): implement outbox schedule activity * feat(EN-214): add outbox publisher workflow * feat(EN-214): migrate accounts to outbox pattern * feat(EN-214): migrate balances to outbox pattern * feat(EN-214): migrate payments to outbox pattern * feat(EN-214): migrate bank accounts to outbox pattern * feat(EN-214): migrate pools to outbox pattern * feat(EN-214): migrate payment initiations to outbox pattern * feat(EN-214): migrate PSU and connections to outbox pattern * feat(EN-214): migrate connectors to outbox pattern * feat(EN-214): remove legacy send_events workflow * feat(EN-214): update workflows to use outbox pattern * feat(EN-214): improve outbox idempotency and error handling * feat(EN-214): update storage tasks and tests * test(EN-214): update e2e tests for outbox pattern * test(EN-214): clean up workflow tests * chore(EN-214): update docker compose configuration * feat(EN-214): add e2e run config in ide * fix: add back docker-compose-dev run config * chore: improve connector uninstall comments & remove connector id being passed * fix: ensure events are not pushed if balances are not created * fix: ensure err is not overwritten when using rollback * fix: ensure we use v3 provider format * chore: apply the nitpick comments from coderabbit * chore: consistentely use e() error handling * feat: optionally disable publisher schedule to make e2e test more consistent * fix: improve error handling on uninstall connector * avoid using custom payload and rely on existing interfaces; also avoid using different types for events * fix: remove unecessary unmarshalling * fix: ensure that rollbackOnTxError picks up errors on tx.Commit() * fix: add extra validation on iban & account number in the test * test: cover marshall/unmarshalling of event payload * fix: increase test timeout * try to make test more resiliant * fix: wrong pointer use in loop * fix: error formatting issue * fix: move oubtox schedule creation out of an activity and do it in the worker itself * feat: OutboxEventDeleteAndSend as batch * change outbox_event to have id only instead of idempotency key * make e2e test less brittle, save eventID as idempotency key in publication * feat: make outbox_event keep an history * feat(EN-214): added a schedule to cleanup outbox events * test: make cleanup actually clean * ignore linting rules for now * feat: do not add psu metadata to event anymore as it's irrelevant * fix: create local variable instead of relying on pointers, even if not 100% necessary * fix: remove temporal schedule describe calls
207 lines
5.1 KiB
Go
207 lines
5.1 KiB
Go
package workflow
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"math/big"
|
|
|
|
"github.com/formancehq/payments/internal/connectors/engine/activities"
|
|
"github.com/formancehq/payments/internal/models"
|
|
"github.com/formancehq/payments/internal/storage"
|
|
"github.com/google/uuid"
|
|
"go.temporal.io/sdk/workflow"
|
|
)
|
|
|
|
func (w Workflow) storePIPaymentWithStatus(
|
|
ctx workflow.Context,
|
|
payment models.Payment,
|
|
paymentInitiationID models.PaymentInitiationID,
|
|
status models.PaymentInitiationAdjustmentStatus,
|
|
) error {
|
|
// payment is available, storing it
|
|
err := activities.StoragePaymentsStore(
|
|
infiniteRetryContext(ctx),
|
|
[]models.Payment{payment},
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = activities.StoragePaymentInitiationsRelatedPaymentsStore(
|
|
infiniteRetryContext(ctx),
|
|
paymentInitiationID,
|
|
payment.ID,
|
|
payment.CreatedAt,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Payment events are now sent via outbox pattern in PaymentsUpsert
|
|
|
|
err = w.addPIAdjustment(
|
|
ctx,
|
|
models.PaymentInitiationAdjustmentID{
|
|
PaymentInitiationID: paymentInitiationID,
|
|
CreatedAt: workflow.Now(ctx),
|
|
Status: status,
|
|
},
|
|
payment.Amount,
|
|
&payment.Asset,
|
|
nil,
|
|
nil,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (w Workflow) addPIAdjustment(
|
|
ctx workflow.Context,
|
|
adjustmentID models.PaymentInitiationAdjustmentID,
|
|
amount *big.Int,
|
|
asset *string,
|
|
err error,
|
|
metadata map[string]string,
|
|
) error {
|
|
adj := models.PaymentInitiationAdjustment{
|
|
ID: adjustmentID,
|
|
CreatedAt: workflow.Now(ctx),
|
|
Status: adjustmentID.Status,
|
|
Amount: amount,
|
|
Asset: asset,
|
|
Error: err,
|
|
Metadata: metadata,
|
|
}
|
|
|
|
if err := activities.StoragePaymentInitiationsAdjustmentsStore(
|
|
infiniteRetryContext(ctx),
|
|
adj,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Payment initiation adjustment events are now sent via outbox pattern in PaymentInitiationAdjustmentsUpsert
|
|
return nil
|
|
}
|
|
|
|
func (w Workflow) getPSPPI(
|
|
ctx workflow.Context,
|
|
pi *models.PaymentInitiation,
|
|
) (models.PSPPaymentInitiation, error) {
|
|
var sourceAccount *models.Account
|
|
if pi.SourceAccountID != nil {
|
|
var err error
|
|
sourceAccount, err = activities.StorageAccountsGet(
|
|
infiniteRetryContext(ctx),
|
|
*pi.SourceAccountID,
|
|
)
|
|
if err != nil {
|
|
return models.PSPPaymentInitiation{}, err
|
|
}
|
|
}
|
|
|
|
var destinationAccount *models.Account
|
|
if pi.DestinationAccountID != nil {
|
|
var err error
|
|
destinationAccount, err = activities.StorageAccountsGet(
|
|
infiniteRetryContext(ctx),
|
|
*pi.DestinationAccountID,
|
|
)
|
|
if err != nil {
|
|
return models.PSPPaymentInitiation{}, err
|
|
}
|
|
|
|
if destinationAccount.Type == models.ACCOUNT_TYPE_EXTERNAL {
|
|
if err := fillFormanceBankAccount(ctx, destinationAccount); err != nil {
|
|
return models.PSPPaymentInitiation{}, err
|
|
}
|
|
}
|
|
}
|
|
pspPI := models.FromPaymentInitiationToPSPPaymentInitiation(pi, models.ToPSPAccount(sourceAccount), models.ToPSPAccount(destinationAccount))
|
|
return pspPI, nil
|
|
}
|
|
|
|
func fillFormanceBankAccount(
|
|
ctx workflow.Context,
|
|
account *models.Account,
|
|
) error {
|
|
bankAccountUUID, err := uuid.Parse(account.ID.Reference)
|
|
if err != nil {
|
|
// Not an uuid, so cannot be a formance bank account
|
|
return nil
|
|
}
|
|
|
|
bankAccount, err := activities.StorageBankAccountsGet(
|
|
infiniteRetryContext(ctx),
|
|
bankAccountUUID,
|
|
true,
|
|
)
|
|
if err != nil {
|
|
if errors.Is(err, storage.ErrNotFound) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
models.FillBankAccountDetailsToAccountMetadata(account, bankAccount)
|
|
|
|
return nil
|
|
}
|
|
|
|
func getPIStatusFromPayment(status models.PaymentStatus) models.PaymentInitiationAdjustmentStatus {
|
|
switch status {
|
|
case models.PAYMENT_STATUS_SUCCEEDED,
|
|
models.PAYMENT_STATUS_CAPTURE,
|
|
models.PAYMENT_STATUS_REFUND_REVERSED:
|
|
return models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSED
|
|
case models.PAYMENT_STATUS_CANCELLED,
|
|
models.PAYMENT_STATUS_CAPTURE_FAILED,
|
|
models.PAYMENT_STATUS_FAILED,
|
|
models.PAYMENT_STATUS_EXPIRED:
|
|
return models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_FAILED
|
|
case models.PAYMENT_STATUS_PENDING,
|
|
models.PAYMENT_STATUS_AUTHORISATION:
|
|
return models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_PROCESSING
|
|
case models.PAYMENT_STATUS_REFUNDED:
|
|
return models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSED
|
|
case models.PAYMENT_STATUS_REFUNDED_FAILURE:
|
|
return models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_REVERSE_FAILED
|
|
default:
|
|
return models.PAYMENT_INITIATION_ADJUSTMENT_STATUS_UNKNOWN
|
|
}
|
|
}
|
|
|
|
func craftUpdatedConnection(
|
|
ctx workflow.Context,
|
|
connectionID string,
|
|
connectorID models.ConnectorID,
|
|
connection *models.OpenBankingConnection,
|
|
updatedStatus models.ConnectionStatus,
|
|
updatedError *string,
|
|
) models.OpenBankingConnection {
|
|
now := workflow.Now(ctx)
|
|
updatedConnection := models.OpenBankingConnection{
|
|
ConnectionID: connectionID,
|
|
ConnectorID: connectorID,
|
|
CreatedAt: now,
|
|
UpdatedAt: now,
|
|
Status: updatedStatus,
|
|
Error: updatedError,
|
|
}
|
|
|
|
if connection != nil {
|
|
updatedConnection.CreatedAt = connection.CreatedAt
|
|
updatedConnection.AccessToken = connection.AccessToken
|
|
updatedConnection.Metadata = connection.Metadata
|
|
updatedConnection.DataUpdatedAt = connection.DataUpdatedAt
|
|
}
|
|
|
|
return updatedConnection
|
|
}
|
|
|
|
func (w Workflow) getDefaultTaskQueue() string {
|
|
return fmt.Sprintf("%s-default", w.stack)
|
|
}
|