package store import ( "context" "errors" "strings" "github.com/tech/sendico/billing/documents/storage" "github.com/tech/sendico/billing/documents/storage/model" "github.com/tech/sendico/pkg/db/repository" "github.com/tech/sendico/pkg/db/repository/builder" 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/mongo" "go.uber.org/zap" ) type Documents struct { logger mlogger.Logger repo repository.Repository } // NewDocuments constructs a Mongo-backed documents store. func NewDocuments(logger mlogger.Logger, db *mongo.Database) (*Documents, error) { if db == nil { return nil, merrors.InvalidArgument("documentsStore: database is nil") } repo := repository.CreateMongoRepository(db, model.DocumentRecordsCollection) indexes := []*ri.Definition{ { Keys: []ri.Key{{Field: "paymentRef", Sort: ri.Asc}}, Unique: true, }, } for _, def := range indexes { if err := repo.CreateIndex(def); err != nil { logger.Error("Failed to ensure documents index", zap.Error(err), zap.String("collection", repo.Collection())) return nil, err } } childLogger := logger.Named("documents") childLogger.Debug("Documents store initialised") return &Documents{ logger: childLogger, repo: repo, }, nil } func (d *Documents) Create(ctx context.Context, record *model.DocumentRecord) error { if record == nil { return merrors.InvalidArgument("documentsStore: nil record") } record.Normalize() if record.PaymentRef == "" { return merrors.InvalidArgument("documentsStore: empty paymentRef") } record.Update() if err := d.repo.Insert(ctx, record, repository.Filter("paymentRef", record.PaymentRef)); err != nil { if errors.Is(err, merrors.ErrDataConflict) { return storage.ErrDuplicateDocument } return err } d.logger.Debug("Document record created", zap.String("payment_ref", record.PaymentRef)) return nil } func (d *Documents) Update(ctx context.Context, record *model.DocumentRecord) error { if record == nil { return merrors.InvalidArgument("documentsStore: nil record") } if record.ID.IsZero() { return merrors.InvalidArgument("documentsStore: missing record id") } record.Normalize() record.Update() if err := d.repo.Update(ctx, record); err != nil { if errors.Is(err, merrors.ErrNoData) { return storage.ErrDocumentNotFound } return err } return nil } func (d *Documents) GetByPaymentRef(ctx context.Context, paymentRef string) (*model.DocumentRecord, error) { paymentRef = strings.TrimSpace(paymentRef) if paymentRef == "" { return nil, merrors.InvalidArgument("documentsStore: empty paymentRef") } entity := &model.DocumentRecord{} if err := d.repo.FindOneByFilter(ctx, repository.Filter("paymentRef", paymentRef), entity); err != nil { if errors.Is(err, merrors.ErrNoData) { return nil, storage.ErrDocumentNotFound } return nil, err } return entity, nil } func (d *Documents) ListByPaymentRefs(ctx context.Context, paymentRefs []string) ([]*model.DocumentRecord, error) { refs := make([]string, 0, len(paymentRefs)) for _, ref := range paymentRefs { clean := strings.TrimSpace(ref) if clean == "" { continue } refs = append(refs, clean) } if len(refs) == 0 { return []*model.DocumentRecord{}, nil } query := repository.Query().Comparison(repository.Field("paymentRef"), builder.In, refs) records := make([]*model.DocumentRecord, 0) decoder := func(cur *mongo.Cursor) error { var rec model.DocumentRecord if err := cur.Decode(&rec); err != nil { d.logger.Warn("Failed to decode document record", zap.Error(err)) return err } records = append(records, &rec) return nil } if err := d.repo.FindManyByFilter(ctx, query, decoder); err != nil { return nil, err } return records, nil } var _ storage.DocumentsStore = (*Documents)(nil)