package repository import ( "context" "strings" "haixun-backend/internal/library/clock" app "haixun-backend/internal/library/errors" "haixun-backend/internal/library/errors/code" "haixun-backend/internal/model/brand/domain/entity" domrepo "haixun-backend/internal/model/brand/domain/repository" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" ) type mongoRepository struct { collection *mongo.Collection } func NewMongoRepository(db *mongo.Database) domrepo.Repository { if db == nil { return &mongoRepository{} } return &mongoRepository{collection: db.Collection(entity.CollectionName)} } func (r *mongoRepository) EnsureIndexes(ctx context.Context) error { if r.collection == nil { return nil } _, err := r.collection.Indexes().CreateMany(ctx, []mongo.IndexModel{ {Keys: bson.D{{Key: "tenant_id", Value: 1}, {Key: "owner_uid", Value: 1}, {Key: "update_at", Value: -1}}}, {Keys: bson.D{{Key: "tenant_id", Value: 1}, {Key: "owner_uid", Value: 1}, {Key: "_id", Value: 1}}, Options: options.Index().SetUnique(true)}, }) return err } func (r *mongoRepository) Create(ctx context.Context, brand *entity.Brand) (*entity.Brand, error) { if r.collection == nil { return nil, app.For(code.Brand).DBUnavailable("Mongo is not configured") } now := clock.NowUnixNano() brand.CreateAt = now brand.UpdateAt = now if brand.Status == "" { brand.Status = entity.StatusOpen } _, err := r.collection.InsertOne(ctx, brand) if err != nil { return nil, err } return brand, nil } func (r *mongoRepository) FindByID(ctx context.Context, tenantID, ownerUID, brandID string) (*entity.Brand, error) { return r.findOne(ctx, bson.M{ "_id": strings.TrimSpace(brandID), "tenant_id": tenantID, "owner_uid": ownerUID, "status": entity.StatusOpen, }) } func (r *mongoRepository) ListByOwner(ctx context.Context, tenantID, ownerUID string) ([]*entity.Brand, error) { if r.collection == nil { return nil, app.For(code.Brand).DBUnavailable("Mongo is not configured") } cursor, err := r.collection.Find( ctx, bson.M{"tenant_id": tenantID, "owner_uid": ownerUID, "status": entity.StatusOpen}, options.Find().SetSort(bson.D{{Key: "update_at", Value: -1}}), ) if err != nil { return nil, err } defer cursor.Close(ctx) var items []*entity.Brand if err := cursor.All(ctx, &items); err != nil { return nil, err } return items, nil } func (r *mongoRepository) Update(ctx context.Context, tenantID, ownerUID, brandID string, patch map[string]interface{}) (*entity.Brand, error) { if r.collection == nil { return nil, app.For(code.Brand).DBUnavailable("Mongo is not configured") } if len(patch) == 0 { return r.FindByID(ctx, tenantID, ownerUID, brandID) } patch["update_at"] = clock.NowUnixNano() var out entity.Brand err := r.collection.FindOneAndUpdate( ctx, bson.M{"_id": brandID, "tenant_id": tenantID, "owner_uid": ownerUID, "status": entity.StatusOpen}, bson.M{"$set": patch}, options.FindOneAndUpdate().SetReturnDocument(options.After), ).Decode(&out) if err == mongo.ErrNoDocuments { return nil, app.For(code.Brand).ResNotFound("brand not found") } return &out, err } func (r *mongoRepository) SoftDelete(ctx context.Context, tenantID, ownerUID, brandID string) error { if r.collection == nil { return app.For(code.Brand).DBUnavailable("Mongo is not configured") } res, err := r.collection.UpdateOne( ctx, bson.M{"_id": brandID, "tenant_id": tenantID, "owner_uid": ownerUID, "status": entity.StatusOpen}, bson.M{"$set": bson.M{"status": entity.StatusDeleted, "update_at": clock.NowUnixNano()}}, ) if err != nil { return err } if res.MatchedCount == 0 { return app.For(code.Brand).ResNotFound("brand not found") } return nil } func (r *mongoRepository) PushProduct(ctx context.Context, tenantID, ownerUID, brandID string, product entity.Product) (*entity.Product, error) { if r.collection == nil { return nil, app.For(code.Brand).DBUnavailable("Mongo is not configured") } now := clock.NowUnixNano() product.CreateAt = now product.UpdateAt = now err := r.collection.FindOneAndUpdate( ctx, bson.M{"_id": brandID, "tenant_id": tenantID, "owner_uid": ownerUID, "status": entity.StatusOpen}, bson.M{ "$push": bson.M{"products": product}, "$set": bson.M{"update_at": now}, }, options.FindOneAndUpdate().SetReturnDocument(options.After), ).Err() if err == mongo.ErrNoDocuments { return nil, app.For(code.Brand).ResNotFound("brand not found") } if err != nil { return nil, err } return &product, nil } func (r *mongoRepository) UpdateProduct(ctx context.Context, tenantID, ownerUID, brandID, productID string, patch map[string]interface{}) (*entity.Product, error) { if r.collection == nil { return nil, app.For(code.Brand).DBUnavailable("Mongo is not configured") } if len(patch) == 0 { return nil, app.For(code.Brand).InputMissingRequired("product patch is empty") } now := clock.NowUnixNano() patch["products.$.update_at"] = now set := bson.M{} for key, value := range patch { set[key] = value } set["update_at"] = now var out entity.Brand err := r.collection.FindOneAndUpdate( ctx, bson.M{ "_id": brandID, "tenant_id": tenantID, "owner_uid": ownerUID, "status": entity.StatusOpen, "products.id": productID, }, bson.M{"$set": set}, options.FindOneAndUpdate().SetReturnDocument(options.After), ).Decode(&out) if err == mongo.ErrNoDocuments { return nil, app.For(code.Brand).ResNotFound("brand or product not found") } if err != nil { return nil, err } for i := range out.Products { if out.Products[i].ID == productID { item := out.Products[i] return &item, nil } } return nil, app.For(code.Brand).ResNotFound("product not found") } func (r *mongoRepository) PullProduct(ctx context.Context, tenantID, ownerUID, brandID, productID string) error { if r.collection == nil { return app.For(code.Brand).DBUnavailable("Mongo is not configured") } now := clock.NowUnixNano() res, err := r.collection.UpdateOne( ctx, bson.M{"_id": brandID, "tenant_id": tenantID, "owner_uid": ownerUID, "status": entity.StatusOpen}, bson.M{ "$pull": bson.M{"products": bson.M{"id": productID}}, "$set": bson.M{"update_at": now}, }, ) if err != nil { return err } if res.MatchedCount == 0 { return app.For(code.Brand).ResNotFound("brand not found") } if res.ModifiedCount == 0 { return app.For(code.Brand).ResNotFound("product not found") } return nil } func (r *mongoRepository) findOne(ctx context.Context, filter bson.M) (*entity.Brand, error) { if r.collection == nil { return nil, app.For(code.Brand).DBUnavailable("Mongo is not configured") } var out entity.Brand err := r.collection.FindOne(ctx, filter).Decode(&out) if err == mongo.ErrNoDocuments { return nil, app.For(code.Brand).ResNotFound("brand not found") } if err != nil { return nil, err } return &out, nil }