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
|
JOIN subscription_plans sp ON sp.id = us.plan_id
|
||||||
WHERE us.id = $1;
|
WHERE us.id = $1;
|
||||||
|
|
||||||
-- name: ListActiveSubscriptionsByUserIDs :many
|
-- Display status for admin user lists: ACTIVE (non-expired), else latest PENDING, else Unsubscribed.
|
||||||
-- One ACTIVE, non-expired row per user (latest expires_at wins), same rules as GetActiveSubscriptionByUserID.
|
-- name: ListSubscriptionDisplayStatusesByUserIDs :many
|
||||||
SELECT DISTINCT ON (us.user_id)
|
WITH input AS (
|
||||||
us.user_id,
|
SELECT unnest($1::bigint[])::bigint AS user_id
|
||||||
us.id,
|
)
|
||||||
us.plan_id,
|
SELECT
|
||||||
us.starts_at,
|
input.user_id,
|
||||||
us.expires_at,
|
COALESCE(
|
||||||
us.status,
|
(SELECT us.status::text FROM user_subscriptions us
|
||||||
us.auto_renew,
|
WHERE us.user_id = input.user_id
|
||||||
us.payment_method,
|
AND us.status = 'ACTIVE' AND us.expires_at > CURRENT_TIMESTAMP
|
||||||
sp.name AS plan_name,
|
ORDER BY us.expires_at DESC LIMIT 1),
|
||||||
sp.duration_value,
|
(SELECT us.status::text FROM user_subscriptions us
|
||||||
sp.duration_unit,
|
WHERE us.user_id = input.user_id
|
||||||
sp.price,
|
AND us.status = 'PENDING'
|
||||||
sp.currency
|
ORDER BY us.created_at DESC LIMIT 1),
|
||||||
FROM user_subscriptions us
|
'Unsubscribed'
|
||||||
JOIN subscription_plans sp ON sp.id = us.plan_id
|
)::text AS subscription_status
|
||||||
WHERE us.user_id = ANY($1::bigint[])
|
FROM input;
|
||||||
AND us.status = 'ACTIVE'
|
|
||||||
AND us.expires_at > CURRENT_TIMESTAMP
|
-- name: GetSubscriptionDisplayStatusByUserID :one
|
||||||
ORDER BY us.user_id, us.expires_at DESC;
|
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
|
-- name: GetActiveSubscriptionByUserID :one
|
||||||
SELECT
|
SELECT
|
||||||
|
|
|
||||||
49
docs/docs.go
49
docs/docs.go
|
|
@ -8436,7 +8436,7 @@ const docTemplate = `{
|
||||||
},
|
},
|
||||||
"/api/v1/users": {
|
"/api/v1/users": {
|
||||||
"get": {
|
"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": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -11294,9 +11294,6 @@ const docTemplate = `{
|
||||||
"domain.UserProfileResponse": {
|
"domain.UserProfileResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"active_subscription": {
|
|
||||||
"$ref": "#/definitions/domain.UserSubscriptionSummary"
|
|
||||||
},
|
|
||||||
"age_group": {
|
"age_group": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -11384,6 +11381,9 @@ const docTemplate = `{
|
||||||
"status": {
|
"status": {
|
||||||
"$ref": "#/definitions/domain.UserStatus"
|
"$ref": "#/definitions/domain.UserStatus"
|
||||||
},
|
},
|
||||||
|
"subscription_status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"updated_at": {
|
"updated_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
|
@ -11404,47 +11404,6 @@ const docTemplate = `{
|
||||||
"UserStatusDeactivated"
|
"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": {
|
"domain.UserSummary": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -8428,7 +8428,7 @@
|
||||||
},
|
},
|
||||||
"/api/v1/users": {
|
"/api/v1/users": {
|
||||||
"get": {
|
"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": [
|
"consumes": [
|
||||||
"application/json"
|
"application/json"
|
||||||
],
|
],
|
||||||
|
|
@ -11286,9 +11286,6 @@
|
||||||
"domain.UserProfileResponse": {
|
"domain.UserProfileResponse": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"active_subscription": {
|
|
||||||
"$ref": "#/definitions/domain.UserSubscriptionSummary"
|
|
||||||
},
|
|
||||||
"age_group": {
|
"age_group": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|
@ -11376,6 +11373,9 @@
|
||||||
"status": {
|
"status": {
|
||||||
"$ref": "#/definitions/domain.UserStatus"
|
"$ref": "#/definitions/domain.UserStatus"
|
||||||
},
|
},
|
||||||
|
"subscription_status": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"updated_at": {
|
"updated_at": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
|
@ -11396,47 +11396,6 @@
|
||||||
"UserStatusDeactivated"
|
"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": {
|
"domain.UserSummary": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
||||||
|
|
@ -1145,8 +1145,6 @@ definitions:
|
||||||
type: object
|
type: object
|
||||||
domain.UserProfileResponse:
|
domain.UserProfileResponse:
|
||||||
properties:
|
properties:
|
||||||
active_subscription:
|
|
||||||
$ref: '#/definitions/domain.UserSubscriptionSummary'
|
|
||||||
age_group:
|
age_group:
|
||||||
type: string
|
type: string
|
||||||
birth_day:
|
birth_day:
|
||||||
|
|
@ -1206,6 +1204,8 @@ definitions:
|
||||||
$ref: '#/definitions/domain.Role'
|
$ref: '#/definitions/domain.Role'
|
||||||
status:
|
status:
|
||||||
$ref: '#/definitions/domain.UserStatus'
|
$ref: '#/definitions/domain.UserStatus'
|
||||||
|
subscription_status:
|
||||||
|
type: string
|
||||||
updated_at:
|
updated_at:
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
|
@ -1221,33 +1221,6 @@ definitions:
|
||||||
- UserStatusActive
|
- UserStatusActive
|
||||||
- UserStatusSuspended
|
- UserStatusSuspended
|
||||||
- UserStatusDeactivated
|
- 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:
|
domain.UserSummary:
|
||||||
properties:
|
properties:
|
||||||
active_users:
|
active_users:
|
||||||
|
|
@ -8017,8 +7990,8 @@ paths:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
- application/json
|
- application/json
|
||||||
description: 'Get users with optional filters. Each user includes active_subscription:
|
description: 'Get users with optional filters. Each user includes subscription_status:
|
||||||
an object when they have a current ACTIVE, non-expired plan, otherwise null.'
|
ACTIVE, PENDING, or Unsubscribed.'
|
||||||
parameters:
|
parameters:
|
||||||
- description: Role filter
|
- description: Role filter
|
||||||
in: query
|
in: query
|
||||||
|
|
|
||||||
|
|
@ -365,6 +365,27 @@ func (q *Queries) GetExpiringSubscriptions(ctx context.Context) ([]GetExpiringSu
|
||||||
return items, nil
|
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
|
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
|
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
|
return items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
const ListActiveSubscriptionsByUserIDs = `-- name: ListActiveSubscriptionsByUserIDs :many
|
const ListSubscriptionDisplayStatusesByUserIDs = `-- name: ListSubscriptionDisplayStatusesByUserIDs :many
|
||||||
SELECT DISTINCT ON (us.user_id)
|
WITH input AS (
|
||||||
us.user_id,
|
SELECT unnest($1::bigint[])::bigint AS user_id
|
||||||
us.id,
|
)
|
||||||
us.plan_id,
|
SELECT
|
||||||
us.starts_at,
|
input.user_id,
|
||||||
us.expires_at,
|
COALESCE(
|
||||||
us.status,
|
(SELECT us.status::text FROM user_subscriptions us
|
||||||
us.auto_renew,
|
WHERE us.user_id = input.user_id
|
||||||
us.payment_method,
|
AND us.status = 'ACTIVE' AND us.expires_at > CURRENT_TIMESTAMP
|
||||||
sp.name AS plan_name,
|
ORDER BY us.expires_at DESC LIMIT 1),
|
||||||
sp.duration_value,
|
(SELECT us.status::text FROM user_subscriptions us
|
||||||
sp.duration_unit,
|
WHERE us.user_id = input.user_id
|
||||||
sp.price,
|
AND us.status = 'PENDING'
|
||||||
sp.currency
|
ORDER BY us.created_at DESC LIMIT 1),
|
||||||
FROM user_subscriptions us
|
'Unsubscribed'
|
||||||
JOIN subscription_plans sp ON sp.id = us.plan_id
|
)::text AS subscription_status
|
||||||
WHERE us.user_id = ANY($1::bigint[])
|
FROM input
|
||||||
AND us.status = 'ACTIVE'
|
|
||||||
AND us.expires_at > CURRENT_TIMESTAMP
|
|
||||||
ORDER BY us.user_id, us.expires_at DESC
|
|
||||||
`
|
`
|
||||||
|
|
||||||
type ListActiveSubscriptionsByUserIDsRow struct {
|
type ListSubscriptionDisplayStatusesByUserIDsRow struct {
|
||||||
UserID int64 `json:"user_id"`
|
UserID int64 `json:"user_id"`
|
||||||
ID int64 `json:"id"`
|
SubscriptionStatus string `json:"subscription_status"`
|
||||||
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"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// One ACTIVE, non-expired row per user (latest expires_at wins), same rules as GetActiveSubscriptionByUserID.
|
// Display status for admin user lists: ACTIVE (non-expired), else latest PENDING, else Unsubscribed.
|
||||||
func (q *Queries) ListActiveSubscriptionsByUserIDs(ctx context.Context, dollar_1 []int64) ([]ListActiveSubscriptionsByUserIDsRow, error) {
|
func (q *Queries) ListSubscriptionDisplayStatusesByUserIDs(ctx context.Context, dollar_1 []int64) ([]ListSubscriptionDisplayStatusesByUserIDsRow, error) {
|
||||||
rows, err := q.db.Query(ctx, ListActiveSubscriptionsByUserIDs, dollar_1)
|
rows, err := q.db.Query(ctx, ListSubscriptionDisplayStatusesByUserIDs, dollar_1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer rows.Close()
|
defer rows.Close()
|
||||||
var items []ListActiveSubscriptionsByUserIDsRow
|
var items []ListSubscriptionDisplayStatusesByUserIDsRow
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var i ListActiveSubscriptionsByUserIDsRow
|
var i ListSubscriptionDisplayStatusesByUserIDsRow
|
||||||
if err := rows.Scan(
|
if err := rows.Scan(&i.UserID, &i.SubscriptionStatus); err != nil {
|
||||||
&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 {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
items = append(items, i)
|
items = append(items, i)
|
||||||
|
|
|
||||||
|
|
@ -56,54 +56,6 @@ type UserSubscription struct {
|
||||||
Currency *string
|
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 {
|
type CreateSubscriptionPlanInput struct {
|
||||||
Name string
|
Name string
|
||||||
Description *string
|
Description *string
|
||||||
|
|
|
||||||
|
|
@ -121,7 +121,7 @@ type UserProfileResponse struct {
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
UpdatedAt *time.Time `json:"updated_at,omitempty"`
|
||||||
|
|
||||||
ActiveSubscription *UserSubscriptionSummary `json:"active_subscription"`
|
SubscriptionStatus string `json:"subscription_status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserFilter struct {
|
type UserFilter struct {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,8 @@ type SubscriptionStore interface {
|
||||||
CreateUserSubscription(ctx context.Context, input domain.CreateUserSubscriptionInput) (*domain.UserSubscription, error)
|
CreateUserSubscription(ctx context.Context, input domain.CreateUserSubscriptionInput) (*domain.UserSubscription, error)
|
||||||
GetUserSubscriptionByID(ctx context.Context, id int64) (*domain.UserSubscription, error)
|
GetUserSubscriptionByID(ctx context.Context, id int64) (*domain.UserSubscription, error)
|
||||||
GetActiveSubscriptionByUserID(ctx context.Context, userID 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)
|
GetUserSubscriptionHistory(ctx context.Context, userID int64, limit, offset int32) ([]domain.UserSubscription, error)
|
||||||
HasActiveSubscription(ctx context.Context, userID int64) (bool, error)
|
HasActiveSubscription(ctx context.Context, userID int64) (bool, error)
|
||||||
CancelUserSubscription(ctx context.Context, id int64) error
|
CancelUserSubscription(ctx context.Context, id int64) error
|
||||||
|
|
|
||||||
|
|
@ -157,39 +157,25 @@ func (s *Store) GetActiveSubscriptionByUserID(ctx context.Context, userID int64)
|
||||||
}, nil
|
}, 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 {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
out := make(map[int64]*domain.UserSubscription, len(rows))
|
out := make(map[int64]string, len(rows))
|
||||||
for _, r := range rows {
|
for _, r := range rows {
|
||||||
dv := r.DurationValue
|
out[r.UserID] = r.SubscriptionStatus
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return out, nil
|
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) {
|
func (s *Store) GetUserSubscriptionHistory(ctx context.Context, userID int64, limit, offset int32) ([]domain.UserSubscription, error) {
|
||||||
subs, err := s.queries.GetUserSubscriptionHistory(ctx, dbgen.GetUserSubscriptionHistoryParams{
|
subs, err := s.queries.GetUserSubscriptionHistory(ctx, dbgen.GetUserSubscriptionHistoryParams{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
|
|
|
||||||
|
|
@ -103,13 +103,19 @@ func (s *Service) GetSubscriptionByID(ctx context.Context, id int64) (*domain.Us
|
||||||
return sub, nil
|
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) {
|
func (s *Service) GetActiveSubscription(ctx context.Context, userID int64) (*domain.UserSubscription, error) {
|
||||||
return s.store.GetActiveSubscriptionByUserID(ctx, userID)
|
return s.store.GetActiveSubscriptionByUserID(ctx, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListActiveSubscriptionsForUserIDs returns the current ACTIVE, non-expired subscription per user (latest expiry).
|
// ListSubscriptionDisplayStatusesForUserIDs returns ACTIVE, PENDING, or Unsubscribed per user_id (admin list).
|
||||||
func (s *Service) ListActiveSubscriptionsForUserIDs(ctx context.Context, userIDs []int64) (map[int64]*domain.UserSubscription, error) {
|
func (s *Service) ListSubscriptionDisplayStatusesForUserIDs(ctx context.Context, userIDs []int64) (map[int64]string, error) {
|
||||||
return s.store.ListActiveSubscriptionsByUserIDs(ctx, userIDs)
|
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) {
|
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
|
// GetAllUsers godoc
|
||||||
// @Summary Get all users
|
// @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
|
// @Tags user
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Produce json
|
// @Produce json
|
||||||
|
|
@ -503,9 +503,9 @@ func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
|
||||||
for i, u := range users {
|
for i, u := range users {
|
||||||
userIDs[i] = u.ID
|
userIDs[i] = u.ID
|
||||||
}
|
}
|
||||||
activeSubs, err := h.subscriptionsSvc.ListActiveSubscriptionsForUserIDs(c.Context(), userIDs)
|
subStatuses, err := h.subscriptionsSvc.ListSubscriptionDisplayStatusesForUserIDs(c.Context(), userIDs)
|
||||||
if err != nil {
|
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.Int("status_code", fiber.StatusInternalServerError),
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
zap.Time("timestamp", time.Now()))
|
zap.Time("timestamp", time.Now()))
|
||||||
|
|
@ -551,9 +551,11 @@ func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
|
||||||
if !u.BirthDay.IsZero() {
|
if !u.BirthDay.IsZero() {
|
||||||
bd = u.BirthDay.Format("2006-01-02")
|
bd = u.BirthDay.Format("2006-01-02")
|
||||||
}
|
}
|
||||||
var activeSub *domain.UserSubscriptionSummary
|
var subStatus string
|
||||||
if sub, ok := activeSubs[u.ID]; ok {
|
if s, ok := subStatuses[u.ID]; ok {
|
||||||
activeSub = sub.Summary()
|
subStatus = s
|
||||||
|
} else {
|
||||||
|
subStatus = "Unsubscribed"
|
||||||
}
|
}
|
||||||
|
|
||||||
mapped = append(mapped, domain.UserProfileResponse{
|
mapped = append(mapped, domain.UserProfileResponse{
|
||||||
|
|
@ -585,7 +587,7 @@ func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
|
||||||
PreferredLanguage: u.PreferredLanguage,
|
PreferredLanguage: u.PreferredLanguage,
|
||||||
CreatedAt: u.CreatedAt,
|
CreatedAt: u.CreatedAt,
|
||||||
UpdatedAt: u.UpdatedAt,
|
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())
|
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)
|
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != authentication.ErrRefreshTokenNotFound {
|
if err != authentication.ErrRefreshTokenNotFound {
|
||||||
|
|
@ -1448,6 +1461,7 @@ func (h *Handler) GetUserProfile(c *fiber.Ctx) error {
|
||||||
PreferredLanguage: user.PreferredLanguage,
|
PreferredLanguage: user.PreferredLanguage,
|
||||||
CreatedAt: user.CreatedAt,
|
CreatedAt: user.CreatedAt,
|
||||||
UpdatedAt: user.UpdatedAt,
|
UpdatedAt: user.UpdatedAt,
|
||||||
|
SubscriptionStatus: subscriptionStatus,
|
||||||
}
|
}
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "User profile retrieved successfully", res, nil)
|
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())
|
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)
|
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err != authentication.ErrRefreshTokenNotFound {
|
if err != authentication.ErrRefreshTokenNotFound {
|
||||||
|
|
@ -1537,6 +1562,7 @@ func (h *Handler) AdminProfile(c *fiber.Ctx) error {
|
||||||
PreferredLanguage: user.PreferredLanguage,
|
PreferredLanguage: user.PreferredLanguage,
|
||||||
CreatedAt: user.CreatedAt,
|
CreatedAt: user.CreatedAt,
|
||||||
UpdatedAt: user.UpdatedAt,
|
UpdatedAt: user.UpdatedAt,
|
||||||
|
SubscriptionStatus: subscriptionStatus,
|
||||||
}
|
}
|
||||||
// Ensure birthday is included and formatted
|
// Ensure birthday is included and formatted
|
||||||
if !user.BirthDay.IsZero() {
|
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())
|
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))
|
res := make([]domain.UserProfileResponse, 0, len(users))
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
|
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
|
||||||
|
|
@ -1637,6 +1678,11 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
|
||||||
lastLogin = &user.CreatedAt
|
lastLogin = &user.CreatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
subStatus := "Unsubscribed"
|
||||||
|
if s, ok := subStatuses[user.ID]; ok {
|
||||||
|
subStatus = s
|
||||||
|
}
|
||||||
|
|
||||||
// var orgID *int64
|
// var orgID *int64
|
||||||
// if user.OrganizationID.Valid {
|
// if user.OrganizationID.Valid {
|
||||||
// orgID = &user.OrganizationID.Value
|
// orgID = &user.OrganizationID.Value
|
||||||
|
|
@ -1669,6 +1715,7 @@ func (h *Handler) SearchUserByNameOrPhone(c *fiber.Ctx) error {
|
||||||
PreferredLanguage: user.PreferredLanguage,
|
PreferredLanguage: user.PreferredLanguage,
|
||||||
CreatedAt: user.CreatedAt,
|
CreatedAt: user.CreatedAt,
|
||||||
UpdatedAt: user.UpdatedAt,
|
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())
|
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)
|
lastLogin, err := h.authSvc.GetLastLogin(c.Context(), user.ID)
|
||||||
if err != nil && err != authentication.ErrRefreshTokenNotFound {
|
if err != nil && err != authentication.ErrRefreshTokenNotFound {
|
||||||
h.mongoLoggerSvc.Error("Failed to get user last login",
|
h.mongoLoggerSvc.Error("Failed to get user last login",
|
||||||
|
|
@ -1765,6 +1823,7 @@ func (h *Handler) GetUserByID(c *fiber.Ctx) error {
|
||||||
PreferredLanguage: user.PreferredLanguage,
|
PreferredLanguage: user.PreferredLanguage,
|
||||||
CreatedAt: user.CreatedAt,
|
CreatedAt: user.CreatedAt,
|
||||||
UpdatedAt: user.UpdatedAt,
|
UpdatedAt: user.UpdatedAt,
|
||||||
|
SubscriptionStatus: subscriptionStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil)
|
return response.WriteJSON(c, fiber.StatusOK, "User retrieved successfully", res, nil)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user