package store import ( "context" "errors" "strings" "time" "github.com/tech/sendico/gateway/chain/storage" "github.com/tech/sendico/gateway/chain/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" "github.com/tech/sendico/pkg/mservice" "go.mongodb.org/mongo-driver/mongo" "go.uber.org/zap" ) const ( defaultDepositPageSize int64 = 100 maxDepositPageSize int64 = 500 ) type Deposits struct { logger mlogger.Logger repo repository.Repository } // NewDeposits constructs a Mongo-backed deposits store. func NewDeposits(logger mlogger.Logger, db *mongo.Database) (*Deposits, error) { if db == nil { return nil, merrors.InvalidArgument("mongo database is nil") } repo := repository.CreateMongoRepository(db, mservice.ChainDeposits) indexes := []*ri.Definition{ { Keys: []ri.Key{{Field: "depositRef", Sort: ri.Asc}}, Unique: true, }, { Keys: []ri.Key{{Field: "walletRef", Sort: ri.Asc}, {Field: "status", Sort: ri.Asc}}, }, { Keys: []ri.Key{{Field: "txHash", Sort: ri.Asc}}, Unique: true, }, } for _, def := range indexes { if err := repo.CreateIndex(def); err != nil { logger.Error("failed to ensure deposit index", zap.Error(err), zap.String("collection", repo.Collection())) return nil, err } } childLogger := logger.Named("deposits") childLogger.Debug("deposits store initialised") return &Deposits{logger: childLogger, repo: repo}, nil } func (d *Deposits) Record(ctx context.Context, deposit *model.Deposit) error { if deposit == nil { return merrors.InvalidArgument("depositsStore: nil deposit") } deposit.Normalize() if strings.TrimSpace(deposit.DepositRef) == "" { return merrors.InvalidArgument("depositsStore: empty depositRef") } if deposit.Status == "" { deposit.Status = model.DepositStatusPending } if deposit.ObservedAt.IsZero() { deposit.ObservedAt = time.Now().UTC() } if deposit.RecordedAt.IsZero() { deposit.RecordedAt = time.Now().UTC() } if deposit.LastStatusAt.IsZero() { deposit.LastStatusAt = time.Now().UTC() } existing := &model.Deposit{} err := d.repo.FindOneByFilter(ctx, repository.Filter("depositRef", deposit.DepositRef), existing) switch { case err == nil: existing.Status = deposit.Status existing.ObservedAt = deposit.ObservedAt existing.RecordedAt = deposit.RecordedAt existing.LastStatusAt = time.Now().UTC() if deposit.Amount != nil { existing.Amount = deposit.Amount } if deposit.BlockID != "" { existing.BlockID = deposit.BlockID } if deposit.TxHash != "" { existing.TxHash = deposit.TxHash } if deposit.Network != "" { existing.Network = deposit.Network } if deposit.TokenSymbol != "" { existing.TokenSymbol = deposit.TokenSymbol } if deposit.ContractAddress != "" { existing.ContractAddress = deposit.ContractAddress } if deposit.SourceAddress != "" { existing.SourceAddress = deposit.SourceAddress } if err := d.repo.Update(ctx, existing); err != nil { return err } return nil case errors.Is(err, merrors.ErrNoData): if err := d.repo.Insert(ctx, deposit, repository.Filter("depositRef", deposit.DepositRef)); err != nil { return err } return nil default: return err } } func (d *Deposits) ListPending(ctx context.Context, network string, limit int32) ([]*model.Deposit, error) { query := repository.Query().Filter(repository.Field("status"), model.DepositStatusPending) if net := strings.TrimSpace(network); net != "" { query = query.Filter(repository.Field("network"), strings.ToLower(net)) } pageSize := sanitizeDepositLimit(limit) query = query.Sort(repository.Field("observedAt"), true).Limit(&pageSize) deposits := make([]*model.Deposit, 0, pageSize) decoder := func(cur *mongo.Cursor) error { item := &model.Deposit{} if err := cur.Decode(item); err != nil { return err } deposits = append(deposits, item) return nil } if err := d.repo.FindManyByFilter(ctx, query, decoder); err != nil && !errors.Is(err, merrors.ErrNoData) { return nil, err } return deposits, nil } func sanitizeDepositLimit(requested int32) int64 { if requested <= 0 { return defaultDepositPageSize } if requested > int32(maxDepositPageSize) { return maxDepositPageSize } return int64(requested) } var _ storage.DepositsStore = (*Deposits)(nil)