package repository import ( "context" "errors" "strings" "time" libmongo "gateway/internal/library/mongo" member "gateway/internal/model/member/domain" "gateway/internal/model/member/domain/entity" "gateway/internal/model/member/domain/enum" domrepo "gateway/internal/model/member/domain/repository" "go.mongodb.org/mongo-driver/v2/bson" mongodriver "go.mongodb.org/mongo-driver/v2/mongo" "go.mongodb.org/mongo-driver/v2/mongo/options" ) // MemberRepositoryParam configures the Mongo member repository. type MemberRepositoryParam struct { Conf *libmongo.Conf } type memberRepository struct { db libmongo.DocumentDBUseCase } // NewMemberRepository creates a Mongo-backed MemberRepository. func NewMemberRepository(param MemberRepositoryParam) domrepo.MemberRepository { documentDB, err := libmongo.NewDocumentDB(param.Conf, entity.Member{}.CollectionName()) if err != nil { panic(err) } return &memberRepository{db: documentDB} } func (r *memberRepository) Insert(ctx context.Context, rec *entity.Member) error { now := time.Now().UTC().UnixMilli() if rec.ID.IsZero() { rec.ID = bson.NewObjectID() } if rec.CreateAt == 0 { rec.CreateAt = now } if rec.UpdateAt == 0 { rec.UpdateAt = now } _, err := r.db.GetClient().InsertOne(ctx, rec) if err != nil { if mongodriver.IsDuplicateKeyError(err) { return member.ErrDuplicateMember } return err } return nil } func (r *memberRepository) GetByUID(ctx context.Context, tenantID, uid string) (*entity.Member, error) { var doc entity.Member filter := bson.M{ member.BSONFieldTenantID: tenantID, member.BSONFieldUID: uid, } if err := r.db.GetClient().FindOne(ctx, &doc, filter); err != nil { if errors.Is(err, mongodriver.ErrNoDocuments) { return nil, member.ErrNotFound } return nil, err } return &doc, nil } func (r *memberRepository) GetByZitadelUserID(ctx context.Context, tenantID, zitadelUserID string) (*entity.Member, error) { var doc entity.Member filter := bson.M{ member.BSONFieldTenantID: tenantID, member.BSONFieldZitadelUserID: zitadelUserID, } if err := r.db.GetClient().FindOne(ctx, &doc, filter); err != nil { if errors.Is(err, mongodriver.ErrNoDocuments) { return nil, member.ErrNotFound } return nil, err } return &doc, nil } func (r *memberRepository) GetByZitadelEmail(ctx context.Context, tenantID, email string) (*entity.Member, error) { var doc entity.Member filter := bson.M{ member.BSONFieldTenantID: tenantID, member.BSONFieldZitadelEmail: strings.ToLower(strings.TrimSpace(email)), } if err := r.db.GetClient().FindOne(ctx, &doc, filter); err != nil { if errors.Is(err, mongodriver.ErrNoDocuments) { return nil, member.ErrNotFound } return nil, err } return &doc, nil } func (r *memberRepository) UpdateProfile(ctx context.Context, tenantID, uid string, update *domrepo.MemberUpdate) (*entity.Member, error) { set := bson.M{member.BSONFieldUpdateAt: time.Now().UTC().UnixMilli()} if update.DisplayName != nil { set[member.BSONFieldDisplayName] = *update.DisplayName } if update.Avatar != nil { set[member.BSONFieldAvatar] = *update.Avatar } if update.Language != nil { set[member.BSONFieldLanguage] = *update.Language } if update.Currency != nil { set[member.BSONFieldCurrency] = *update.Currency } if update.Phone != nil { set[member.BSONFieldPhone] = *update.Phone } filter := bson.M{member.BSONFieldTenantID: tenantID, member.BSONFieldUID: uid} opts := options.FindOneAndUpdate().SetReturnDocument(options.After) var doc entity.Member if err := r.db.GetClient().FindOneAndUpdate(ctx, &doc, filter, bson.M{bsonOpSet: set}, opts); err != nil { if errors.Is(err, mongodriver.ErrNoDocuments) { return nil, member.ErrNotFound } return nil, err } return &doc, nil } func (r *memberRepository) UpdateStatus(ctx context.Context, tenantID, uid string, status enum.MemberStatus, suspendReason string) error { set := bson.M{ member.BSONFieldMemberStatus: status, member.BSONFieldUpdateAt: time.Now().UTC().UnixMilli(), } if status == enum.MemberStatusSuspended { set[member.BSONFieldSuspendReason] = suspendReason } if status == enum.MemberStatusDeleted { set[member.BSONFieldDeletedAt] = time.Now().UTC().UnixMilli() } filter := bson.M{member.BSONFieldTenantID: tenantID, member.BSONFieldUID: uid} res, err := r.db.GetClient().UpdateOne(ctx, filter, bson.M{bsonOpSet: set}) if err != nil { return err } if res.MatchedCount == 0 { return member.ErrNotFound } return nil } func (r *memberRepository) List(ctx context.Context, filter domrepo.ListMembersFilter) ([]*entity.Member, int64, error) { q := bson.M{member.BSONFieldTenantID: filter.TenantID} if filter.Status != "" { q[member.BSONFieldMemberStatus] = filter.Status } total, err := r.db.GetClient().CountDocuments(ctx, q) if err != nil { return nil, 0, err } limit := filter.Limit if limit <= 0 { limit = 20 } if limit > 100 { limit = 100 } opts := options.Find(). SetSkip(filter.Offset). SetLimit(limit). SetSort(bson.D{{Key: member.BSONFieldCreateAt, Value: -1}}) var docs []*entity.Member if err := r.db.GetClient().Find(ctx, &docs, q, opts); err != nil { return nil, 0, err } return docs, total, nil } func (r *memberRepository) SetBusinessEmailVerified(ctx context.Context, tenantID, uid, email string) error { now := time.Now().UTC().UnixMilli() set := bson.M{ member.BSONFieldBusinessEmail: email, member.BSONFieldBusinessEmailVerified: true, member.BSONFieldBusinessEmailVerifiedAt: now, member.BSONFieldUpdateAt: now, } filter := bson.M{member.BSONFieldTenantID: tenantID, member.BSONFieldUID: uid} res, err := r.db.GetClient().UpdateOne(ctx, filter, bson.M{bsonOpSet: set}) if err != nil { return err } if res.MatchedCount == 0 { return member.ErrNotFound } return nil } func (r *memberRepository) SetBusinessPhoneVerified(ctx context.Context, tenantID, uid, phone string) error { now := time.Now().UTC().UnixMilli() set := bson.M{ member.BSONFieldBusinessPhone: phone, member.BSONFieldBusinessPhoneVerified: true, member.BSONFieldBusinessPhoneVerifiedAt: now, member.BSONFieldUpdateAt: now, } filter := bson.M{member.BSONFieldTenantID: tenantID, member.BSONFieldUID: uid} res, err := r.db.GetClient().UpdateOne(ctx, filter, bson.M{bsonOpSet: set}) if err != nil { return err } if res.MatchedCount == 0 { return member.ErrNotFound } return nil } // Index20260520001UP ensures members collection indexes exist. func (r *memberRepository) Index20260520001UP(ctx context.Context) error { if err := r.db.PopulateMultiIndex(ctx, []string{member.BSONFieldTenantID, member.BSONFieldUID}, []int32{1, 1}, true); err != nil { return err } if err := r.db.PopulateMultiIndex(ctx, []string{member.BSONFieldTenantID, member.BSONFieldZitadelUserID}, []int32{1, 1}, true); err != nil { return err } if err := r.db.PopulateMultiIndex(ctx, []string{member.BSONFieldTenantID, member.BSONFieldZitadelEmail}, []int32{1, 1}, false); err != nil { return err } return r.db.PopulateMultiIndex(ctx, []string{member.BSONFieldTenantID, member.BSONFieldMemberStatus, member.BSONFieldCreateAt}, []int32{1, 1, -1}, false) } var _ domrepo.MemberRepository = (*memberRepository)(nil)