package store import ( "context" "errors" "strings" "time" storage "github.com/tech/sendico/gateway/mntx/storage" "github.com/tech/sendico/gateway/mntx/storage/model" "github.com/tech/sendico/pkg/db/repository" ri "github.com/tech/sendico/pkg/db/repository/index" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" "go.uber.org/zap" ) const ( payoutsCollection = "card_payouts" payoutIdemField = "idempotencyKey" payoutIdField = "payoutId" ) type Payouts struct { logger mlogger.Logger coll *mongo.Collection } func NewPayouts(logger mlogger.Logger, db *mongo.Database) (*Payouts, error) { if db == nil { return nil, merrors.InvalidArgument("mongo database is nil") } if logger == nil { logger = zap.NewNop() } logger = logger.Named("payouts").With(zap.String("collection", payoutsCollection)) repo := repository.CreateMongoRepository(db, payoutsCollection) if err := repo.CreateIndex(&ri.Definition{ Keys: []ri.Key{{Field: payoutIdemField, Sort: ri.Asc}}, Unique: true, }); err != nil { logger.Error("Failed to create payouts idempotency index", zap.Error(err), zap.String("index_field", payoutIdemField)) return nil, err } p := &Payouts{ logger: logger, coll: db.Collection(payoutsCollection), } p.logger.Debug("Payouts store initialised") return p, nil } func (p *Payouts) findOneByField(ctx context.Context, field, value string) (*model.CardPayout, error) { value = strings.TrimSpace(value) if value == "" { return nil, merrors.InvalidArgument("lookup key is required", field) } var result model.CardPayout err := p.coll.FindOne(ctx, bson.M{field: value}).Decode(&result) if err == mongo.ErrNoDocuments { return nil, nil } if err != nil { if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { p.logger.Warn("Payout record lookup failed", zap.String("field", field), zap.String("value", value), zap.Error(err)) } return nil, err } return &result, nil } func (p *Payouts) FindByIdempotencyKey(ctx context.Context, key string) (*model.CardPayout, error) { return p.findOneByField(ctx, payoutIdemField, key) // operationRef } func (p *Payouts) FindByPaymentID(ctx context.Context, paymentID string) (*model.CardPayout, error) { return p.findOneByField(ctx, payoutIdField, paymentID) } func (p *Payouts) Upsert(ctx context.Context, record *model.CardPayout) error { if record == nil { return merrors.InvalidArgument("payout record is nil", "record") } record.OperationRef = strings.TrimSpace(record.OperationRef) record.PayoutID = strings.TrimSpace(record.PayoutID) record.CustomerID = strings.TrimSpace(record.CustomerID) record.ProviderCode = strings.TrimSpace(record.ProviderCode) record.ProviderPaymentID = strings.TrimSpace(record.ProviderPaymentID) if record.OperationRef == "" { return merrors.InvalidArgument("operation ref is required", "operation_ref") } now := time.Now() if record.CreatedAt.IsZero() { record.CreatedAt = now } record.UpdatedAt = now // critical: never let caller override _id record.ID = bson.NilObjectID update := bson.M{ "$set": record, } _, err := p.coll.UpdateOne( ctx, bson.M{payoutIdemField: record.OperationRef}, update, options.UpdateOne().SetUpsert(true), ) if err != nil { if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) { p.logger.Warn("Failed to upsert payout record", zap.String("operation_ref", record.OperationRef), zap.String("payout_id", record.PayoutID), zap.Error(err)) } return err } return nil } var _ storage.PayoutsStore = (*Payouts)(nil)