Merge pull request 'improved storing' (#456) from mntx-452 into main
All checks were successful
ci/woodpecker/push/gateway_mntx Pipeline was successful

Reviewed-on: #456
This commit was merged in pull request #456.
This commit is contained in:
2026-02-10 12:37:09 +00:00
7 changed files with 38 additions and 31 deletions

View File

@@ -51,13 +51,13 @@ func (s *cardPayoutStore) FindByPaymentID(_ context.Context, id string) (*model.
} }
func (s *cardPayoutStore) Upsert(_ context.Context, record *model.CardPayout) error { func (s *cardPayoutStore) Upsert(_ context.Context, record *model.CardPayout) error {
s.data[record.PayoutID] = record s.data[record.PaymentRef] = record
return nil return nil
} }
// Save is a helper for tests to pre-populate data. // Save is a helper for tests to pre-populate data.
func (s *cardPayoutStore) Save(state *model.CardPayout) { func (s *cardPayoutStore) Save(state *model.CardPayout) {
s.data[state.PayoutID] = state s.data[state.PaymentRef] = state
} }
// Get is a helper for tests to retrieve data. // Get is a helper for tests to retrieve data.

View File

@@ -92,7 +92,7 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
Base: storable.Base{ Base: storable.Base{
ID: bson.NilObjectID, ID: bson.NilObjectID,
}, },
PayoutID: strings.TrimSpace(req.GetPayoutId()), PaymentRef: strings.TrimSpace(req.GetPayoutId()),
OperationRef: strings.TrimSpace(req.GetOperationRef()), OperationRef: strings.TrimSpace(req.GetOperationRef()),
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()), IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
IntentRef: strings.TrimSpace(req.GetIntentRef()), IntentRef: strings.TrimSpace(req.GetIntentRef()),
@@ -106,7 +106,7 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
} }
// Keep CreatedAt/refs if record already exists. // Keep CreatedAt/refs if record already exists.
if existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PayoutID); err == nil && existing != nil { if existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PaymentRef); err == nil && existing != nil {
if !existing.CreatedAt.IsZero() { if !existing.CreatedAt.IsZero() {
state.CreatedAt = existing.CreatedAt state.CreatedAt = existing.CreatedAt
} }
@@ -130,7 +130,7 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
if e := p.updatePayoutStatus(ctx, state); e != nil { if e := p.updatePayoutStatus(ctx, state); e != nil {
p.logger.Warn("Failed to update payout status", p.logger.Warn("Failed to update payout status",
zap.Error(e), zap.Error(e),
zap.String("payout_id", state.PayoutID), zap.String("payment_ref", state.PaymentRef),
zap.String("customer_id", state.CustomerID), zap.String("customer_id", state.CustomerID),
zap.String("operation_ref", state.OperationRef), zap.String("operation_ref", state.OperationRef),
zap.String("idempotency_key", state.IdempotencyKey), zap.String("idempotency_key", state.IdempotencyKey),
@@ -141,7 +141,7 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
p.logger.Warn("Monetix payout submission failed", p.logger.Warn("Monetix payout submission failed",
zap.Error(err), zap.Error(err),
zap.String("payout_id", state.PayoutID), zap.String("payment_ref", state.PaymentRef),
zap.String("customer_id", state.CustomerID), zap.String("customer_id", state.CustomerID),
zap.String("operation_ref", state.OperationRef), zap.String("operation_ref", state.OperationRef),
zap.String("idempotency_key", state.IdempotencyKey), zap.String("idempotency_key", state.IdempotencyKey),
@@ -166,7 +166,7 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
if err := p.updatePayoutStatus(ctx, state); err != nil { if err := p.updatePayoutStatus(ctx, state); err != nil {
p.logger.Warn("Failed to store payout", p.logger.Warn("Failed to store payout",
zap.Error(err), zap.Error(err),
zap.String("payout_id", state.PayoutID), zap.String("payment_ref", state.PaymentRef),
zap.String("customer_id", state.CustomerID), zap.String("customer_id", state.CustomerID),
zap.String("operation_ref", state.OperationRef), zap.String("operation_ref", state.OperationRef),
zap.String("idempotency_key", state.IdempotencyKey), zap.String("idempotency_key", state.IdempotencyKey),
@@ -183,7 +183,7 @@ func (p *cardPayoutProcessor) Submit(ctx context.Context, req *mntxv1.CardPayout
} }
p.logger.Info("Card payout submission stored", p.logger.Info("Card payout submission stored",
zap.String("payout_id", state.PayoutID), zap.String("payment_ref", state.PaymentRef),
zap.String("status", string(state.Status)), zap.String("status", string(state.Status)),
zap.Bool("accepted", result.Accepted), zap.Bool("accepted", result.Accepted),
zap.String("provider_request_id", result.ProviderRequestID), zap.String("provider_request_id", result.ProviderRequestID),
@@ -233,7 +233,7 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
now := p.clock.Now() now := p.clock.Now()
state := &model.CardPayout{ state := &model.CardPayout{
PayoutID: strings.TrimSpace(req.GetPayoutId()), PaymentRef: strings.TrimSpace(req.GetPayoutId()),
OperationRef: strings.TrimSpace(req.GetOperationRef()), OperationRef: strings.TrimSpace(req.GetOperationRef()),
IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()), IdempotencyKey: strings.TrimSpace(req.GetIdempotencyKey()),
ProjectID: projectID, ProjectID: projectID,
@@ -245,7 +245,7 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
UpdatedAt: now, UpdatedAt: now,
} }
if existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PayoutID); err == nil && existing != nil { if existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PaymentRef); err == nil && existing != nil {
if !existing.CreatedAt.IsZero() { if !existing.CreatedAt.IsZero() {
state.CreatedAt = existing.CreatedAt state.CreatedAt = existing.CreatedAt
} }
@@ -269,7 +269,7 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
_ = p.updatePayoutStatus(ctx, state) _ = p.updatePayoutStatus(ctx, state)
p.logger.Warn("Monetix token payout submission failed", p.logger.Warn("Monetix token payout submission failed",
zap.String("payout_id", state.PayoutID), zap.String("payment_ref", state.PaymentRef),
zap.String("customer_id", state.CustomerID), zap.String("customer_id", state.CustomerID),
zap.Error(err), zap.Error(err),
) )
@@ -301,7 +301,7 @@ func (p *cardPayoutProcessor) SubmitToken(ctx context.Context, req *mntxv1.CardT
} }
p.logger.Info("Card token payout submission stored", p.logger.Info("Card token payout submission stored",
zap.String("payout_id", state.PayoutID), zap.String("payment_ref", state.PaymentRef),
zap.String("status", string(state.Status)), zap.String("status", string(state.Status)),
zap.Bool("accepted", result.Accepted), zap.Bool("accepted", result.Accepted),
zap.String("provider_request_id", result.ProviderRequestID), zap.String("provider_request_id", result.ProviderRequestID),
@@ -396,7 +396,7 @@ func (p *cardPayoutProcessor) Status(ctx context.Context, payoutID string) (*mnt
} }
p.logger.Info("Card payout status resolved", p.logger.Info("Card payout status resolved",
zap.String("payout_id", state.PayoutID), zap.String("payment_ref", state.PaymentRef),
zap.String("status", string(state.Status)), zap.String("status", string(state.Status)),
) )
@@ -449,10 +449,10 @@ func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byt
state := CardPayoutStateFromProto(p.clock, pbState) state := CardPayoutStateFromProto(p.clock, pbState)
// Preserve CreatedAt + internal keys from existing record if present. // Preserve CreatedAt + internal keys from existing record if present.
if existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PayoutID); err != nil { if existing, err := p.store.Payouts().FindByPaymentID(ctx, state.PaymentRef); err != nil {
p.logger.Warn("Failed to fetch payout state while processing callback", p.logger.Warn("Failed to fetch payout state while processing callback",
zap.Error(err), zap.Error(err),
zap.String("payout_id", state.PayoutID), zap.String("payment_ref", state.PaymentRef),
) )
return http.StatusInternalServerError, err return http.StatusInternalServerError, err
} else if existing != nil { } else if existing != nil {
@@ -477,7 +477,7 @@ func (p *cardPayoutProcessor) ProcessCallback(ctx context.Context, payload []byt
monetix.ObserveCallback(statusLabel) monetix.ObserveCallback(statusLabel)
p.logger.Info("Monetix payout callback processed", p.logger.Info("Monetix payout callback processed",
zap.String("payout_id", state.PayoutID), zap.String("payment_ref", state.PaymentRef),
zap.String("status", statusLabel), zap.String("status", statusLabel),
zap.String("provider_code", state.ProviderCode), zap.String("provider_code", state.ProviderCode),
zap.String("provider_message", state.ProviderMessage), zap.String("provider_message", state.ProviderMessage),

View File

@@ -44,7 +44,7 @@ func TestCardPayoutProcessor_Submit_Success(t *testing.T) {
repo := newMockRepository() repo := newMockRepository()
repo.payouts.Save(&model.CardPayout{ repo.payouts.Save(&model.CardPayout{
PayoutID: "payout-1", PaymentRef: "payout-1",
CreatedAt: existingCreated, CreatedAt: existingCreated,
}) })

View File

@@ -22,7 +22,7 @@ func CardPayoutStateFromProto(clock clockpkg.Clock, p *mntxv1.CardPayoutState) *
} }
return &model.CardPayout{ return &model.CardPayout{
PayoutID: p.PayoutId, PaymentRef: p.PayoutId,
OperationRef: p.GetOperationRef(), OperationRef: p.GetOperationRef(),
IntentRef: p.GetIntentRef(), IntentRef: p.GetIntentRef(),
IdempotencyKey: p.GetIdempotencyKey(), IdempotencyKey: p.GetIdempotencyKey(),
@@ -41,7 +41,7 @@ func CardPayoutStateFromProto(clock clockpkg.Clock, p *mntxv1.CardPayoutState) *
func StateToProto(m *model.CardPayout) *mntxv1.CardPayoutState { func StateToProto(m *model.CardPayout) *mntxv1.CardPayoutState {
return &mntxv1.CardPayoutState{ return &mntxv1.CardPayoutState{
PayoutId: m.PayoutID, PayoutId: m.PaymentRef,
ProjectId: m.ProjectID, ProjectId: m.ProjectID,
CustomerId: m.CustomerID, CustomerId: m.CustomerID,
AmountMinor: m.AmountMinor, AmountMinor: m.AmountMinor,

View File

@@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"github.com/tech/sendico/gateway/mntx/storage/model" "github.com/tech/sendico/gateway/mntx/storage/model"
"github.com/tech/sendico/pkg/merrors"
paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway" paymentgateway "github.com/tech/sendico/pkg/messaging/notifications/paymentgateway"
pmodel "github.com/tech/sendico/pkg/model" pmodel "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
@@ -23,23 +24,23 @@ func isFinalStatus(t *model.CardPayout) bool {
} }
} }
func toOpStatus(t *model.CardPayout) rail.OperationResult { func toOpStatus(t *model.CardPayout) (rail.OperationResult, error) {
switch t.Status { switch t.Status {
case model.PayoutStatusFailed: case model.PayoutStatusFailed:
return rail.OperationResultFailed return rail.OperationResultFailed, nil
case model.PayoutStatusSuccess: case model.PayoutStatusSuccess:
return rail.OperationResultSuccess return rail.OperationResultSuccess, nil
case model.PayoutStatusCancelled: case model.PayoutStatusCancelled:
return rail.OperationResultCancelled return rail.OperationResultCancelled, nil
default: default:
panic(fmt.Sprintf("unexpected transfer status, %s", t.Status)) return rail.OperationResultFailed, merrors.InvalidArgument(fmt.Sprintf("unexpected transfer status, %s", t.Status), "t.Status")
} }
} }
func (p *cardPayoutProcessor) updatePayoutStatus(ctx context.Context, state *model.CardPayout) error { func (p *cardPayoutProcessor) updatePayoutStatus(ctx context.Context, state *model.CardPayout) error {
if err := p.store.Payouts().Upsert(ctx, state); err != nil { if err := p.store.Payouts().Upsert(ctx, state); err != nil {
p.logger.Warn("Failed to update transfer status", zap.Error(err), mzap.ObjRef("payout_ref", state.ID), p.logger.Warn("Failed to update transfer status", zap.Error(err), mzap.ObjRef("payout_ref", state.ID),
zap.String("transfer_ref", state.PayoutID), zap.String("status", string(state.Status)), zap.String("payment_ref", state.PaymentRef), zap.String("status", string(state.Status)),
) )
} }
if isFinalStatus(state) { if isFinalStatus(state) {
@@ -53,6 +54,13 @@ func (p *cardPayoutProcessor) emitTransferStatusEvent(payout *model.CardPayout)
return return
} }
status, err := toOpStatus(payout)
if err != nil {
p.logger.Warn("Failed to convert payout status to operation status for transfer status event", zap.Error(err),
mzap.ObjRef("payout_ref", payout.ID), zap.String("payment_ref", payout.PaymentRef), zap.String("status", string(payout.Status)))
return
}
exec := pmodel.PaymentGatewayExecution{ exec := pmodel.PaymentGatewayExecution{
PaymentIntentID: payout.IntentRef, PaymentIntentID: payout.IntentRef,
IdempotencyKey: payout.IdempotencyKey, IdempotencyKey: payout.IdempotencyKey,
@@ -61,7 +69,7 @@ func (p *cardPayoutProcessor) emitTransferStatusEvent(payout *model.CardPayout)
Currency: payout.Currency, Currency: payout.Currency,
}, },
PaymentRef: payout.PaymentRef, PaymentRef: payout.PaymentRef,
Status: toOpStatus(payout), Status: status,
OperationRef: payout.OperationRef, OperationRef: payout.OperationRef,
Error: payout.FailureReason, Error: payout.FailureReason,
TransferRef: payout.GetID().Hex(), TransferRef: payout.GetID().Hex(),

View File

@@ -9,7 +9,6 @@ import (
// CardPayout is a Mongo/JSON representation of proto CardPayout // CardPayout is a Mongo/JSON representation of proto CardPayout
type CardPayout struct { type CardPayout struct {
storable.Base `bson:",inline" json:",inline"` storable.Base `bson:",inline" json:",inline"`
PayoutID string `bson:"payoutId" json:"payout_id"`
PaymentRef string `bson:"paymentRef" json:"payment_ref"` PaymentRef string `bson:"paymentRef" json:"payment_ref"`
OperationRef string `bson:"operationRef" json:"operation_ref"` OperationRef string `bson:"operationRef" json:"operation_ref"`
IdempotencyKey string `bson:"idempotencyKey" json:"idempotency_key"` IdempotencyKey string `bson:"idempotencyKey" json:"idempotency_key"`

View File

@@ -18,7 +18,7 @@ import (
const ( const (
payoutsCollection = "card_payouts" payoutsCollection = "card_payouts"
payoutIdemField = "idempotencyKey" payoutIdemField = "idempotencyKey"
payoutIdField = "payoutId" payoutIdField = "paymentRef"
) )
type Payouts struct { type Payouts struct {
@@ -74,7 +74,7 @@ func (p *Payouts) Upsert(ctx context.Context, record *model.CardPayout) error {
} }
record.OperationRef = strings.TrimSpace(record.OperationRef) record.OperationRef = strings.TrimSpace(record.OperationRef)
record.PayoutID = strings.TrimSpace(record.PayoutID) record.PaymentRef = strings.TrimSpace(record.PaymentRef)
record.CustomerID = strings.TrimSpace(record.CustomerID) record.CustomerID = strings.TrimSpace(record.CustomerID)
record.ProviderCode = strings.TrimSpace(record.ProviderCode) record.ProviderCode = strings.TrimSpace(record.ProviderCode)
record.ProviderPaymentID = strings.TrimSpace(record.ProviderPaymentID) record.ProviderPaymentID = strings.TrimSpace(record.ProviderPaymentID)
@@ -86,7 +86,7 @@ func (p *Payouts) Upsert(ctx context.Context, record *model.CardPayout) error {
if err := p.repository.Upsert(ctx, record); err != nil { if err := p.repository.Upsert(ctx, record); err != nil {
p.logger.Warn("Failed to upsert payout record", zap.Error(err), mzap.ObjRef("payout_ref", record.ID), p.logger.Warn("Failed to upsert payout record", zap.Error(err), mzap.ObjRef("payout_ref", record.ID),
zap.String("operation_ref", record.OperationRef), zap.String("payment_ref", record.PayoutID)) zap.String("operation_ref", record.OperationRef), zap.String("payment_ref", record.PaymentRef))
return err return err
} }
return nil return nil