package store import ( "context" "errors" "fmt" "time" "go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/mongo" "bountyboard/internal/domain" "bountyboard/internal/ulid" ) func (s *Store) customers() *mongo.Collection { return s.DB.Collection("customers") } func (s *Store) CreateCustomer(ctx context.Context, c *domain.Customer) error { now := time.Now().UTC() if c.ID == "" { c.ID = ulid.New() } c.CreatedAt, c.UpdatedAt, c.Version = now, now, 1 if c.ConsultantIDs == nil { c.ConsultantIDs = []string{} } if _, err := s.customers().InsertOne(ctx, c); err != nil { if mongo.IsDuplicateKeyError(err) { return fmt.Errorf("customer %q: %w", c.Name, ErrDuplicate) } return fmt.Errorf("insert customer: %w", err) } return nil } func (s *Store) CustomerByID(ctx context.Context, id string) (*domain.Customer, error) { var c domain.Customer err := s.customers().FindOne(ctx, bson.M{"_id": id}).Decode(&c) if errors.Is(err, mongo.ErrNoDocuments) { return nil, ErrNotFound } if err != nil { return nil, fmt.Errorf("find customer: %w", err) } return &c, nil } // ListCustomers returns customers, optionally restricted to a consultant and // optionally including archived ones, sorted by name. func (s *Store) ListCustomers(ctx context.Context, consultantID string, includeArchived bool) ([]domain.Customer, error) { filter := bson.M{} if !includeArchived { filter["archived"] = false } if consultantID != "" { filter["consultantIds"] = consultantID } cur, err := s.customers().Find(ctx, filter) if err != nil { return nil, fmt.Errorf("list customers: %w", err) } out := []domain.Customer{} if err := cur.All(ctx, &out); err != nil { return nil, fmt.Errorf("decode customers: %w", err) } return out, nil } // UpdateCustomer applies a version-checked partial update. func (s *Store) UpdateCustomer(ctx context.Context, id string, version int64, set bson.M) error { return UpdateVersioned(ctx, s.customers(), id, version, bson.M{"$set": set}) } func (s *Store) DeleteCustomer(ctx context.Context, id string) error { res, err := s.customers().DeleteOne(ctx, bson.M{"_id": id}) if err != nil { return fmt.Errorf("delete customer: %w", err) } if res.DeletedCount == 0 { return ErrNotFound } return nil } // SetSyncStatus records the outcome of a sync run (not version-checked: // background bookkeeping must not race with admin edits). func (s *Store) SetSyncStatus(ctx context.Context, id string, at time.Time, status, errMsg string) error { _, err := s.customers().UpdateOne(ctx, bson.M{"_id": id}, bson.M{"$set": bson.M{ "ticketing.lastSyncAt": at, "ticketing.lastSyncStatus": status, "ticketing.lastSyncError": errMsg, }}) if err != nil { return fmt.Errorf("set sync status: %w", err) } return nil } // CountTasksForCustomer guards customer deletion. func (s *Store) CountTasksForCustomer(ctx context.Context, customerID string) (int64, error) { n, err := s.DB.Collection("tasks").CountDocuments(ctx, bson.M{"customerId": customerID}) if err != nil { return 0, fmt.Errorf("count tasks: %w", err) } return n, nil }