Expose subscription_status on user profile responses instead of active_subscription.
Users see ACTIVE, PENDING, or Unsubscribed via new batch and single SQL helpers; Swagger refreshed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
1e62510321
commit
49bcc22d0d
|
|
@ -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
|
||||
|
|
|
|||
49
docs/docs.go
49
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": {
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user