diff --git a/db/query/subscriptions.sql b/db/query/subscriptions.sql index 349a6d9..dcdd32a 100644 --- a/db/query/subscriptions.sql +++ b/db/query/subscriptions.sql @@ -61,28 +61,38 @@ FROM user_subscriptions us JOIN subscription_plans sp ON sp.id = us.plan_id WHERE us.id = $1; --- name: ListActiveSubscriptionsByUserIDs :many --- One ACTIVE, non-expired row per user (latest expires_at wins), same rules as GetActiveSubscriptionByUserID. -SELECT DISTINCT ON (us.user_id) - us.user_id, - us.id, - us.plan_id, - us.starts_at, - us.expires_at, - us.status, - us.auto_renew, - us.payment_method, - sp.name AS plan_name, - sp.duration_value, - sp.duration_unit, - sp.price, - sp.currency -FROM user_subscriptions us -JOIN subscription_plans sp ON sp.id = us.plan_id -WHERE us.user_id = ANY($1::bigint[]) - AND us.status = 'ACTIVE' - AND us.expires_at > CURRENT_TIMESTAMP -ORDER BY us.user_id, us.expires_at DESC; +-- Display status for admin user lists: ACTIVE (non-expired), else latest PENDING, else Unsubscribed. +-- name: ListSubscriptionDisplayStatusesByUserIDs :many +WITH input AS ( + SELECT unnest($1::bigint[])::bigint AS user_id +) +SELECT + input.user_id, + COALESCE( + (SELECT us.status::text FROM user_subscriptions us + WHERE us.user_id = input.user_id + AND us.status = 'ACTIVE' AND us.expires_at > CURRENT_TIMESTAMP + ORDER BY us.expires_at DESC LIMIT 1), + (SELECT us.status::text FROM user_subscriptions us + WHERE us.user_id = input.user_id + AND us.status = 'PENDING' + ORDER BY us.created_at DESC LIMIT 1), + 'Unsubscribed' + )::text AS subscription_status +FROM input; + +-- name: GetSubscriptionDisplayStatusByUserID :one +SELECT COALESCE( + (SELECT us.status::text FROM user_subscriptions us + WHERE us.user_id = $1 + AND us.status = 'ACTIVE' AND us.expires_at > CURRENT_TIMESTAMP + ORDER BY us.expires_at DESC LIMIT 1), + (SELECT us.status::text FROM user_subscriptions us + WHERE us.user_id = $1 + AND us.status = 'PENDING' + ORDER BY us.created_at DESC LIMIT 1), + 'Unsubscribed' +)::text AS subscription_status; -- name: GetActiveSubscriptionByUserID :one SELECT diff --git a/docs/docs.go b/docs/docs.go index 32f77fe..e0f7cd0 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -8436,7 +8436,7 @@ const docTemplate = `{ }, "/api/v1/users": { "get": { - "description": "Get users with optional filters. Each user includes active_subscription: an object when they have a current ACTIVE, non-expired plan, otherwise null.", + "description": "Get users with optional filters. Each user includes subscription_status: ACTIVE, PENDING, or Unsubscribed.", "consumes": [ "application/json" ], @@ -11294,9 +11294,6 @@ const docTemplate = `{ "domain.UserProfileResponse": { "type": "object", "properties": { - "active_subscription": { - "$ref": "#/definitions/domain.UserSubscriptionSummary" - }, "age_group": { "type": "string" }, @@ -11384,6 +11381,9 @@ const docTemplate = `{ "status": { "$ref": "#/definitions/domain.UserStatus" }, + "subscription_status": { + "type": "string" + }, "updated_at": { "type": "string" } @@ -11404,47 +11404,6 @@ const docTemplate = `{ "UserStatusDeactivated" ] }, - "domain.UserSubscriptionSummary": { - "type": "object", - "properties": { - "auto_renew": { - "type": "boolean" - }, - "currency": { - "type": "string" - }, - "duration_unit": { - "type": "string" - }, - "duration_value": { - "type": "integer" - }, - "expires_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "payment_method": { - "type": "string" - }, - "plan_id": { - "type": "integer" - }, - "plan_name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "starts_at": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, "domain.UserSummary": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index 441eac1..7c0943c 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -8428,7 +8428,7 @@ }, "/api/v1/users": { "get": { - "description": "Get users with optional filters. Each user includes active_subscription: an object when they have a current ACTIVE, non-expired plan, otherwise null.", + "description": "Get users with optional filters. Each user includes subscription_status: ACTIVE, PENDING, or Unsubscribed.", "consumes": [ "application/json" ], @@ -11286,9 +11286,6 @@ "domain.UserProfileResponse": { "type": "object", "properties": { - "active_subscription": { - "$ref": "#/definitions/domain.UserSubscriptionSummary" - }, "age_group": { "type": "string" }, @@ -11376,6 +11373,9 @@ "status": { "$ref": "#/definitions/domain.UserStatus" }, + "subscription_status": { + "type": "string" + }, "updated_at": { "type": "string" } @@ -11396,47 +11396,6 @@ "UserStatusDeactivated" ] }, - "domain.UserSubscriptionSummary": { - "type": "object", - "properties": { - "auto_renew": { - "type": "boolean" - }, - "currency": { - "type": "string" - }, - "duration_unit": { - "type": "string" - }, - "duration_value": { - "type": "integer" - }, - "expires_at": { - "type": "string" - }, - "id": { - "type": "integer" - }, - "payment_method": { - "type": "string" - }, - "plan_id": { - "type": "integer" - }, - "plan_name": { - "type": "string" - }, - "price": { - "type": "number" - }, - "starts_at": { - "type": "string" - }, - "status": { - "type": "string" - } - } - }, "domain.UserSummary": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index 1fbe3a4..5e718b2 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -1145,8 +1145,6 @@ definitions: type: object domain.UserProfileResponse: properties: - active_subscription: - $ref: '#/definitions/domain.UserSubscriptionSummary' age_group: type: string birth_day: @@ -1206,6 +1204,8 @@ definitions: $ref: '#/definitions/domain.Role' status: $ref: '#/definitions/domain.UserStatus' + subscription_status: + type: string updated_at: type: string type: object @@ -1221,33 +1221,6 @@ definitions: - UserStatusActive - UserStatusSuspended - UserStatusDeactivated - domain.UserSubscriptionSummary: - properties: - auto_renew: - type: boolean - currency: - type: string - duration_unit: - type: string - duration_value: - type: integer - expires_at: - type: string - id: - type: integer - payment_method: - type: string - plan_id: - type: integer - plan_name: - type: string - price: - type: number - starts_at: - type: string - status: - type: string - type: object domain.UserSummary: properties: active_users: @@ -8017,8 +7990,8 @@ paths: get: consumes: - application/json - description: 'Get users with optional filters. Each user includes active_subscription: - an object when they have a current ACTIVE, non-expired plan, otherwise null.' + description: 'Get users with optional filters. Each user includes subscription_status: + ACTIVE, PENDING, or Unsubscribed.' parameters: - description: Role filter in: query diff --git a/gen/db/subscriptions.sql.go b/gen/db/subscriptions.sql.go index c482b17..05255e6 100644 --- a/gen/db/subscriptions.sql.go +++ b/gen/db/subscriptions.sql.go @@ -365,6 +365,27 @@ func (q *Queries) GetExpiringSubscriptions(ctx context.Context) ([]GetExpiringSu return items, nil } +const GetSubscriptionDisplayStatusByUserID = `-- name: GetSubscriptionDisplayStatusByUserID :one +SELECT COALESCE( + (SELECT us.status::text FROM user_subscriptions us + WHERE us.user_id = $1 + AND us.status = 'ACTIVE' AND us.expires_at > CURRENT_TIMESTAMP + ORDER BY us.expires_at DESC LIMIT 1), + (SELECT us.status::text FROM user_subscriptions us + WHERE us.user_id = $1 + AND us.status = 'PENDING' + ORDER BY us.created_at DESC LIMIT 1), + 'Unsubscribed' +)::text AS subscription_status +` + +func (q *Queries) GetSubscriptionDisplayStatusByUserID(ctx context.Context, userID int64) (string, error) { + row := q.db.QueryRow(ctx, GetSubscriptionDisplayStatusByUserID, userID) + var subscription_status string + err := row.Scan(&subscription_status) + return subscription_status, err +} + const GetSubscriptionPlanByID = `-- name: GetSubscriptionPlanByID :one SELECT id, name, description, duration_value, duration_unit, price, currency, is_active, created_at, updated_at FROM subscription_plans WHERE id = $1 ` @@ -578,70 +599,42 @@ func (q *Queries) ListActiveSubscriptionPlans(ctx context.Context) ([]Subscripti return items, nil } -const ListActiveSubscriptionsByUserIDs = `-- name: ListActiveSubscriptionsByUserIDs :many -SELECT DISTINCT ON (us.user_id) - us.user_id, - us.id, - us.plan_id, - us.starts_at, - us.expires_at, - us.status, - us.auto_renew, - us.payment_method, - sp.name AS plan_name, - sp.duration_value, - sp.duration_unit, - sp.price, - sp.currency -FROM user_subscriptions us -JOIN subscription_plans sp ON sp.id = us.plan_id -WHERE us.user_id = ANY($1::bigint[]) - AND us.status = 'ACTIVE' - AND us.expires_at > CURRENT_TIMESTAMP -ORDER BY us.user_id, us.expires_at DESC +const ListSubscriptionDisplayStatusesByUserIDs = `-- name: ListSubscriptionDisplayStatusesByUserIDs :many +WITH input AS ( + SELECT unnest($1::bigint[])::bigint AS user_id +) +SELECT + input.user_id, + COALESCE( + (SELECT us.status::text FROM user_subscriptions us + WHERE us.user_id = input.user_id + AND us.status = 'ACTIVE' AND us.expires_at > CURRENT_TIMESTAMP + ORDER BY us.expires_at DESC LIMIT 1), + (SELECT us.status::text FROM user_subscriptions us + WHERE us.user_id = input.user_id + AND us.status = 'PENDING' + ORDER BY us.created_at DESC LIMIT 1), + 'Unsubscribed' + )::text AS subscription_status +FROM input ` -type ListActiveSubscriptionsByUserIDsRow struct { - UserID int64 `json:"user_id"` - ID int64 `json:"id"` - PlanID int64 `json:"plan_id"` - StartsAt pgtype.Timestamptz `json:"starts_at"` - ExpiresAt pgtype.Timestamptz `json:"expires_at"` - Status string `json:"status"` - AutoRenew bool `json:"auto_renew"` - PaymentMethod pgtype.Text `json:"payment_method"` - PlanName string `json:"plan_name"` - DurationValue int32 `json:"duration_value"` - DurationUnit string `json:"duration_unit"` - Price pgtype.Numeric `json:"price"` - Currency string `json:"currency"` +type ListSubscriptionDisplayStatusesByUserIDsRow struct { + UserID int64 `json:"user_id"` + SubscriptionStatus string `json:"subscription_status"` } -// One ACTIVE, non-expired row per user (latest expires_at wins), same rules as GetActiveSubscriptionByUserID. -func (q *Queries) ListActiveSubscriptionsByUserIDs(ctx context.Context, dollar_1 []int64) ([]ListActiveSubscriptionsByUserIDsRow, error) { - rows, err := q.db.Query(ctx, ListActiveSubscriptionsByUserIDs, dollar_1) +// Display status for admin user lists: ACTIVE (non-expired), else latest PENDING, else Unsubscribed. +func (q *Queries) ListSubscriptionDisplayStatusesByUserIDs(ctx context.Context, dollar_1 []int64) ([]ListSubscriptionDisplayStatusesByUserIDsRow, error) { + rows, err := q.db.Query(ctx, ListSubscriptionDisplayStatusesByUserIDs, dollar_1) if err != nil { return nil, err } defer rows.Close() - var items []ListActiveSubscriptionsByUserIDsRow + var items []ListSubscriptionDisplayStatusesByUserIDsRow for rows.Next() { - var i ListActiveSubscriptionsByUserIDsRow - if err := rows.Scan( - &i.UserID, - &i.ID, - &i.PlanID, - &i.StartsAt, - &i.ExpiresAt, - &i.Status, - &i.AutoRenew, - &i.PaymentMethod, - &i.PlanName, - &i.DurationValue, - &i.DurationUnit, - &i.Price, - &i.Currency, - ); err != nil { + var i ListSubscriptionDisplayStatusesByUserIDsRow + if err := rows.Scan(&i.UserID, &i.SubscriptionStatus); err != nil { return nil, err } items = append(items, i) diff --git a/internal/domain/subscriptions.go b/internal/domain/subscriptions.go index 7bc9fe8..d5e7228 100644 --- a/internal/domain/subscriptions.go +++ b/internal/domain/subscriptions.go @@ -56,54 +56,6 @@ type UserSubscription struct { Currency *string } -// UserSubscriptionSummary is the active subscription attached to admin user list responses (GET /users). -type UserSubscriptionSummary struct { - ID int64 `json:"id"` - PlanID int64 `json:"plan_id"` - PlanName string `json:"plan_name"` - Status string `json:"status"` - StartsAt time.Time `json:"starts_at"` - ExpiresAt time.Time `json:"expires_at"` - AutoRenew bool `json:"auto_renew"` - PaymentMethod *string `json:"payment_method,omitempty"` - DurationValue int32 `json:"duration_value"` - DurationUnit string `json:"duration_unit"` - Price float64 `json:"price"` - Currency string `json:"currency"` -} - -// Summary returns a copy safe for JSON embedding; nil if receiver is nil. -func (us *UserSubscription) Summary() *UserSubscriptionSummary { - if us == nil { - return nil - } - s := &UserSubscriptionSummary{ - ID: us.ID, - PlanID: us.PlanID, - Status: us.Status, - StartsAt: us.StartsAt, - ExpiresAt: us.ExpiresAt, - AutoRenew: us.AutoRenew, - PaymentMethod: us.PaymentMethod, - } - if us.PlanName != nil { - s.PlanName = *us.PlanName - } - if us.DurationValue != nil { - s.DurationValue = *us.DurationValue - } - if us.DurationUnit != nil { - s.DurationUnit = *us.DurationUnit - } - if us.Price != nil { - s.Price = *us.Price - } - if us.Currency != nil { - s.Currency = *us.Currency - } - return s -} - type CreateSubscriptionPlanInput struct { Name string Description *string diff --git a/internal/domain/user.go b/internal/domain/user.go index 9529f2c..b5f3616 100644 --- a/internal/domain/user.go +++ b/internal/domain/user.go @@ -121,7 +121,7 @@ type UserProfileResponse struct { CreatedAt time.Time `json:"created_at"` UpdatedAt *time.Time `json:"updated_at,omitempty"` - ActiveSubscription *UserSubscriptionSummary `json:"active_subscription"` + SubscriptionStatus string `json:"subscription_status"` } type UserFilter struct { diff --git a/internal/ports/subscriptions.go b/internal/ports/subscriptions.go index 6a83c71..2f93d3b 100644 --- a/internal/ports/subscriptions.go +++ b/internal/ports/subscriptions.go @@ -18,7 +18,8 @@ type SubscriptionStore interface { CreateUserSubscription(ctx context.Context, input domain.CreateUserSubscriptionInput) (*domain.UserSubscription, error) GetUserSubscriptionByID(ctx context.Context, id int64) (*domain.UserSubscription, error) GetActiveSubscriptionByUserID(ctx context.Context, userID int64) (*domain.UserSubscription, error) - ListActiveSubscriptionsByUserIDs(ctx context.Context, userIDs []int64) (map[int64]*domain.UserSubscription, error) + ListSubscriptionDisplayStatusesByUserIDs(ctx context.Context, userIDs []int64) (map[int64]string, error) + GetSubscriptionDisplayStatusByUserID(ctx context.Context, userID int64) (string, error) GetUserSubscriptionHistory(ctx context.Context, userID int64, limit, offset int32) ([]domain.UserSubscription, error) HasActiveSubscription(ctx context.Context, userID int64) (bool, error) CancelUserSubscription(ctx context.Context, id int64) error diff --git a/internal/repository/subscriptions.go b/internal/repository/subscriptions.go index 007a30b..9c30299 100644 --- a/internal/repository/subscriptions.go +++ b/internal/repository/subscriptions.go @@ -157,39 +157,25 @@ func (s *Store) GetActiveSubscriptionByUserID(ctx context.Context, userID int64) }, nil } -func (s *Store) ListActiveSubscriptionsByUserIDs(ctx context.Context, userIDs []int64) (map[int64]*domain.UserSubscription, error) { +func (s *Store) ListSubscriptionDisplayStatusesByUserIDs(ctx context.Context, userIDs []int64) (map[int64]string, error) { if len(userIDs) == 0 { - return map[int64]*domain.UserSubscription{}, nil + return map[int64]string{}, nil } - rows, err := s.queries.ListActiveSubscriptionsByUserIDs(ctx, userIDs) + rows, err := s.queries.ListSubscriptionDisplayStatusesByUserIDs(ctx, userIDs) if err != nil { return nil, err } - out := make(map[int64]*domain.UserSubscription, len(rows)) + out := make(map[int64]string, len(rows)) for _, r := range rows { - dv := r.DurationValue - du := r.DurationUnit - pn := r.PlanName - cur := r.Currency - out[r.UserID] = &domain.UserSubscription{ - ID: r.ID, - UserID: r.UserID, - PlanID: r.PlanID, - StartsAt: r.StartsAt.Time, - ExpiresAt: r.ExpiresAt.Time, - Status: r.Status, - AutoRenew: r.AutoRenew, - PaymentMethod: fromPgText(r.PaymentMethod), - PlanName: &pn, - DurationValue: &dv, - DurationUnit: &du, - Price: float64Ptr(fromPgNumeric(r.Price)), - Currency: &cur, - } + out[r.UserID] = r.SubscriptionStatus } return out, nil } +func (s *Store) GetSubscriptionDisplayStatusByUserID(ctx context.Context, userID int64) (string, error) { + return s.queries.GetSubscriptionDisplayStatusByUserID(ctx, userID) +} + func (s *Store) GetUserSubscriptionHistory(ctx context.Context, userID int64, limit, offset int32) ([]domain.UserSubscription, error) { subs, err := s.queries.GetUserSubscriptionHistory(ctx, dbgen.GetUserSubscriptionHistoryParams{ UserID: userID, diff --git a/internal/services/subscriptions/service.go b/internal/services/subscriptions/service.go index f0e5500..57d4b4d 100644 --- a/internal/services/subscriptions/service.go +++ b/internal/services/subscriptions/service.go @@ -103,13 +103,19 @@ func (s *Service) GetSubscriptionByID(ctx context.Context, id int64) (*domain.Us return sub, nil } +// GetActiveSubscription returns the ACTIVE, non-expired subscription for the user. func (s *Service) GetActiveSubscription(ctx context.Context, userID int64) (*domain.UserSubscription, error) { return s.store.GetActiveSubscriptionByUserID(ctx, userID) } -// ListActiveSubscriptionsForUserIDs returns the current ACTIVE, non-expired subscription per user (latest expiry). -func (s *Service) ListActiveSubscriptionsForUserIDs(ctx context.Context, userIDs []int64) (map[int64]*domain.UserSubscription, error) { - return s.store.ListActiveSubscriptionsByUserIDs(ctx, userIDs) +// ListSubscriptionDisplayStatusesForUserIDs returns ACTIVE, PENDING, or Unsubscribed per user_id (admin list). +func (s *Service) ListSubscriptionDisplayStatusesForUserIDs(ctx context.Context, userIDs []int64) (map[int64]string, error) { + return s.store.ListSubscriptionDisplayStatusesByUserIDs(ctx, userIDs) +} + +// GetSubscriptionDisplayStatusForUserID returns ACTIVE, PENDING, or Unsubscribed for one user. +func (s *Service) GetSubscriptionDisplayStatusForUserID(ctx context.Context, userID int64) (string, error) { + return s.store.GetSubscriptionDisplayStatusByUserID(ctx, userID) } func (s *Service) GetSubscriptionHistory(ctx context.Context, userID int64, limit, offset int32) ([]domain.UserSubscription, error) { diff --git a/internal/web_server/handlers/user.go b/internal/web_server/handlers/user.go index 9721852..03bcd99 100644 --- a/internal/web_server/handlers/user.go +++ b/internal/web_server/handlers/user.go @@ -423,7 +423,7 @@ func (h *Handler) CheckUserPending(c *fiber.Ctx) error { // GetAllUsers godoc // @Summary Get all users -// @Description Get users with optional filters. Each user includes active_subscription: an object when they have a current ACTIVE, non-expired plan, otherwise null. +// @Description Get users with optional filters. Each user includes subscription_status: ACTIVE, PENDING, or Unsubscribed. // @Tags user // @Accept json // @Produce json @@ -503,9 +503,9 @@ func (h *Handler) GetAllUsers(c *fiber.Ctx) error { for i, u := range users { userIDs[i] = u.ID } - activeSubs, err := h.subscriptionsSvc.ListActiveSubscriptionsForUserIDs(c.Context(), userIDs) + subStatuses, err := h.subscriptionsSvc.ListSubscriptionDisplayStatusesForUserIDs(c.Context(), userIDs) if err != nil { - h.mongoLoggerSvc.Error("failed to batch-load active subscriptions for user list", + h.mongoLoggerSvc.Error("failed to batch-load subscription display status for user list", zap.Int("status_code", fiber.StatusInternalServerError), zap.Error(err), zap.Time("timestamp", time.Now())) @@ -551,9 +551,11 @@ func (h *Handler) GetAllUsers(c *fiber.Ctx) error { if !u.BirthDay.IsZero() { bd = u.BirthDay.Format("2006-01-02") } - var activeSub *domain.UserSubscriptionSummary - if sub, ok := activeSubs[u.ID]; ok { - activeSub = sub.Summary() + var subStatus string + if s, ok := subStatuses[u.ID]; ok { + subStatus = s + } else { + subStatus = "Unsubscribed" } mapped = append(mapped, domain.UserProfileResponse{ @@ -585,7 +587,7 @@ func (h *Handler) GetAllUsers(c *fiber.Ctx) error { PreferredLanguage: u.PreferredLanguage, CreatedAt: u.CreatedAt, UpdatedAt: u.UpdatedAt, - ActiveSubscription: activeSub, + SubscriptionStatus: subStatus, }) } @@ -1405,6 +1407,17 @@ func (h *Handler) GetUserProfile(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user profile:"+err.Error()) } + subscriptionStatus, err := h.subscriptionsSvc.GetSubscriptionDisplayStatusForUserID(c.Context(), user.ID) + if err != nil { + h.mongoLoggerSvc.Error("Failed to get subscription display status for profile", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve subscription status:"+err.Error()) + } + lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) if err != nil { if err != authentication.ErrRefreshTokenNotFound { @@ -1448,6 +1461,7 @@ func (h *Handler) GetUserProfile(c *fiber.Ctx) error { PreferredLanguage: user.PreferredLanguage, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, + SubscriptionStatus: subscriptionStatus, } return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil) } @@ -1502,6 +1516,17 @@ func (h *Handler) AdminProfile(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve user profile:"+err.Error()) } + subscriptionStatus, err := h.subscriptionsSvc.GetSubscriptionDisplayStatusForUserID(c.Context(), user.ID) + if err != nil { + h.mongoLoggerSvc.Error("Failed to get subscription display status for admin profile", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve subscription status:"+err.Error()) + } + lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) if err != nil { if err != authentication.ErrRefreshTokenNotFound { @@ -1537,6 +1562,7 @@ func (h *Handler) AdminProfile(c *fiber.Ctx) error { PreferredLanguage: user.PreferredLanguage, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, + SubscriptionStatus: subscriptionStatus, } // Ensure birthday is included and formatted if !user.BirthDay.IsZero() { @@ -1621,6 +1647,21 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get users: "+err.Error()) } + userIDs := make([]int64, len(users)) + for i, u := range users { + userIDs[i] = u.ID + } + subStatuses, err := h.subscriptionsSvc.ListSubscriptionDisplayStatusesForUserIDs(c.Context(), userIDs) + if err != nil { + h.mongoLoggerSvc.Error("SearchUserByNameOrPhone - failed to load subscription status", + zap.Any("request", req), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get subscription info: "+err.Error()) + } + res := make([]domain.UserProfileResponse, 0, len(users)) for _, user := range users { lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) @@ -1637,6 +1678,11 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error { lastLogin = &user.CreatedAt } + subStatus := "Unsubscribed" + if s, ok := subStatuses[user.ID]; ok { + subStatus = s + } + // var orgID *int64 // if user.OrganizationID.Valid { // orgID = &user.OrganizationID.Value @@ -1669,6 +1715,7 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error { PreferredLanguage: user.PreferredLanguage, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, + SubscriptionStatus: subStatus, }) } @@ -1711,6 +1758,17 @@ func (h *Handler) GetUserByID(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to get user: "+err.Error()) } + subscriptionStatus, err := h.subscriptionsSvc.GetSubscriptionDisplayStatusForUserID(c.Context(), user.ID) + if err != nil { + h.mongoLoggerSvc.Error("Failed to get subscription display status for user by id", + zap.Int64("userID", userID), + zap.Int("status_code", fiber.StatusInternalServerError), + zap.Error(err), + zap.Time("timestamp", time.Now()), + ) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to retrieve subscription status: "+err.Error()) + } + lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID) if err != nil && err != authentication.ErrRefreshTokenNotFound { h.mongoLoggerSvc.Error("Failed to get user last login", @@ -1765,6 +1823,7 @@ func (h *Handler) GetUserByID(c *fiber.Ctx) error { PreferredLanguage: user.PreferredLanguage, CreatedAt: user.CreatedAt, UpdatedAt: user.UpdatedAt, + SubscriptionStatus: subscriptionStatus, } return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil)