Add country, region, and subscription_status filters to GET /users.
Filtering matches user profile country/region (case-insensitive trim) and derived subscription state in SQL so pagination totals stay correct. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
49bcc22d0d
commit
062b1f6151
|
|
@ -196,6 +196,46 @@ WHERE
|
|||
))
|
||||
AND (sqlc.narg('created_after')::TIMESTAMPTZ IS NULL OR created_at >= sqlc.narg('created_after')::TIMESTAMPTZ)
|
||||
AND (sqlc.narg('created_before')::TIMESTAMPTZ IS NULL OR created_at <= sqlc.narg('created_before')::TIMESTAMPTZ)
|
||||
AND (sqlc.narg('country')::TEXT IS NULL OR LOWER(TRIM(COALESCE(country, ''))) = LOWER(TRIM(sqlc.narg('country')::TEXT)))
|
||||
AND (sqlc.narg('region')::TEXT IS NULL OR LOWER(TRIM(COALESCE(region, ''))) = LOWER(TRIM(sqlc.narg('region')::TEXT)))
|
||||
AND (
|
||||
sqlc.narg('subscription_status')::TEXT IS NULL
|
||||
OR (
|
||||
sqlc.narg('subscription_status')::TEXT = 'ACTIVE'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM user_subscriptions us
|
||||
WHERE us.user_id = users.id
|
||||
AND us.status = 'ACTIVE'
|
||||
AND us.expires_at > CURRENT_TIMESTAMP
|
||||
)
|
||||
)
|
||||
OR (
|
||||
sqlc.narg('subscription_status')::TEXT = 'PENDING'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM user_subscriptions us
|
||||
WHERE us.user_id = users.id
|
||||
AND us.status = 'ACTIVE'
|
||||
AND us.expires_at > CURRENT_TIMESTAMP
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM user_subscriptions us
|
||||
WHERE us.user_id = users.id AND us.status = 'PENDING'
|
||||
)
|
||||
)
|
||||
OR (
|
||||
sqlc.narg('subscription_status')::TEXT = 'Unsubscribed'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM user_subscriptions us
|
||||
WHERE us.user_id = users.id
|
||||
AND us.status = 'ACTIVE'
|
||||
AND us.expires_at > CURRENT_TIMESTAMP
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM user_subscriptions us
|
||||
WHERE us.user_id = users.id AND us.status = 'PENDING'
|
||||
)
|
||||
)
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT sqlc.narg('limit')::INT
|
||||
OFFSET sqlc.narg('offset')::INT;
|
||||
|
|
|
|||
20
docs/docs.go
20
docs/docs.go
|
|
@ -8486,9 +8486,27 @@ const docTemplate = `{
|
|||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Status filter (ACTIVE, PENDING, SUSPENDED, DEACTIVATED)",
|
||||
"description": "User account status filter (ACTIVE, PENDING, SUSPENDED, DEACTIVATED)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Country filter (case-insensitive match on stored value)",
|
||||
"name": "country",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Region filter (case-insensitive match on stored value)",
|
||||
"name": "region",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Derived subscription filter: ACTIVE, PENDING, or Unsubscribed (matches response subscription_status semantics)",
|
||||
"name": "subscription_status",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
|
|
|||
|
|
@ -8478,9 +8478,27 @@
|
|||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Status filter (ACTIVE, PENDING, SUSPENDED, DEACTIVATED)",
|
||||
"description": "User account status filter (ACTIVE, PENDING, SUSPENDED, DEACTIVATED)",
|
||||
"name": "status",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Country filter (case-insensitive match on stored value)",
|
||||
"name": "country",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Region filter (case-insensitive match on stored value)",
|
||||
"name": "region",
|
||||
"in": "query"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Derived subscription filter: ACTIVE, PENDING, or Unsubscribed (matches response subscription_status semantics)",
|
||||
"name": "subscription_status",
|
||||
"in": "query"
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
|
|
|
|||
|
|
@ -8017,10 +8017,23 @@ paths:
|
|||
in: query
|
||||
name: created_after
|
||||
type: string
|
||||
- description: Status filter (ACTIVE, PENDING, SUSPENDED, DEACTIVATED)
|
||||
- description: User account status filter (ACTIVE, PENDING, SUSPENDED, DEACTIVATED)
|
||||
in: query
|
||||
name: status
|
||||
type: string
|
||||
- description: Country filter (case-insensitive match on stored value)
|
||||
in: query
|
||||
name: country
|
||||
type: string
|
||||
- description: Region filter (case-insensitive match on stored value)
|
||||
in: query
|
||||
name: region
|
||||
type: string
|
||||
- description: 'Derived subscription filter: ACTIVE, PENDING, or Unsubscribed
|
||||
(matches response subscription_status semantics)'
|
||||
in: query
|
||||
name: subscription_status
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
|
|
|
|||
|
|
@ -386,9 +386,49 @@ WHERE
|
|||
))
|
||||
AND ($4::TIMESTAMPTZ IS NULL OR created_at >= $4::TIMESTAMPTZ)
|
||||
AND ($5::TIMESTAMPTZ IS NULL OR created_at <= $5::TIMESTAMPTZ)
|
||||
AND ($6::TEXT IS NULL OR LOWER(TRIM(COALESCE(country, ''))) = LOWER(TRIM($6::TEXT)))
|
||||
AND ($7::TEXT IS NULL OR LOWER(TRIM(COALESCE(region, ''))) = LOWER(TRIM($7::TEXT)))
|
||||
AND (
|
||||
$8::TEXT IS NULL
|
||||
OR (
|
||||
$8::TEXT = 'ACTIVE'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM user_subscriptions us
|
||||
WHERE us.user_id = users.id
|
||||
AND us.status = 'ACTIVE'
|
||||
AND us.expires_at > CURRENT_TIMESTAMP
|
||||
)
|
||||
)
|
||||
OR (
|
||||
$8::TEXT = 'PENDING'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM user_subscriptions us
|
||||
WHERE us.user_id = users.id
|
||||
AND us.status = 'ACTIVE'
|
||||
AND us.expires_at > CURRENT_TIMESTAMP
|
||||
)
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM user_subscriptions us
|
||||
WHERE us.user_id = users.id AND us.status = 'PENDING'
|
||||
)
|
||||
)
|
||||
OR (
|
||||
$8::TEXT = 'Unsubscribed'
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM user_subscriptions us
|
||||
WHERE us.user_id = users.id
|
||||
AND us.status = 'ACTIVE'
|
||||
AND us.expires_at > CURRENT_TIMESTAMP
|
||||
)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM user_subscriptions us
|
||||
WHERE us.user_id = users.id AND us.status = 'PENDING'
|
||||
)
|
||||
)
|
||||
)
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $7::INT
|
||||
OFFSET $6::INT
|
||||
LIMIT $10::INT
|
||||
OFFSET $9::INT
|
||||
`
|
||||
|
||||
type GetAllUsersParams struct {
|
||||
|
|
@ -397,6 +437,9 @@ type GetAllUsersParams struct {
|
|||
Query pgtype.Text `json:"query"`
|
||||
CreatedAfter pgtype.Timestamptz `json:"created_after"`
|
||||
CreatedBefore pgtype.Timestamptz `json:"created_before"`
|
||||
Country pgtype.Text `json:"country"`
|
||||
Region pgtype.Text `json:"region"`
|
||||
SubscriptionStatus pgtype.Text `json:"subscription_status"`
|
||||
Offset pgtype.Int4 `json:"offset"`
|
||||
Limit pgtype.Int4 `json:"limit"`
|
||||
}
|
||||
|
|
@ -441,6 +484,9 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
|
|||
arg.Query,
|
||||
arg.CreatedAfter,
|
||||
arg.CreatedBefore,
|
||||
arg.Country,
|
||||
arg.Region,
|
||||
arg.SubscriptionStatus,
|
||||
arg.Offset,
|
||||
arg.Limit,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -127,6 +127,9 @@ type UserProfileResponse struct {
|
|||
type UserFilter struct {
|
||||
Role string
|
||||
Status string
|
||||
Country string
|
||||
Region string
|
||||
SubscriptionStatus string // display filter: ACTIVE, PENDING, Unsubscribed (same as API subscription_status values)
|
||||
|
||||
Page int64
|
||||
PageSize int64
|
||||
|
|
|
|||
|
|
@ -55,6 +55,9 @@ type UserStore interface {
|
|||
status *string,
|
||||
query *string,
|
||||
createdBefore, createdAfter *time.Time,
|
||||
country *string,
|
||||
region *string,
|
||||
subscriptionStatus *string,
|
||||
limit, offset int32,
|
||||
) ([]domain.User, int64, error)
|
||||
ListAccountDeletionRequests(ctx context.Context, filter domain.AccountDeletionRequestFilter) ([]domain.AccountDeletionRequest, int64, error)
|
||||
|
|
|
|||
|
|
@ -414,6 +414,9 @@ func (s *Store) GetAllUsers(
|
|||
status *string,
|
||||
query *string,
|
||||
createdBefore, createdAfter *time.Time,
|
||||
country *string,
|
||||
region *string,
|
||||
subscriptionStatus *string,
|
||||
limit, offset int32,
|
||||
) ([]domain.User, int64, error) {
|
||||
|
||||
|
|
@ -442,12 +445,30 @@ func (s *Store) GetAllUsers(
|
|||
createdBeforeParam = pgtype.Timestamptz{Time: *createdBefore, Valid: true}
|
||||
}
|
||||
|
||||
var countryParam pgtype.Text
|
||||
if country != nil && *country != "" {
|
||||
countryParam = pgtype.Text{String: *country, Valid: true}
|
||||
}
|
||||
|
||||
var regionParam pgtype.Text
|
||||
if region != nil && *region != "" {
|
||||
regionParam = pgtype.Text{String: *region, Valid: true}
|
||||
}
|
||||
|
||||
var subscriptionStatusParam pgtype.Text
|
||||
if subscriptionStatus != nil && *subscriptionStatus != "" {
|
||||
subscriptionStatusParam = pgtype.Text{String: *subscriptionStatus, Valid: true}
|
||||
}
|
||||
|
||||
params := dbgen.GetAllUsersParams{
|
||||
Role: roleParam,
|
||||
Status: statusParam,
|
||||
Query: queryParam,
|
||||
CreatedAfter: createdAfterParam,
|
||||
CreatedBefore: createdBeforeParam,
|
||||
Country: countryParam,
|
||||
Region: regionParam,
|
||||
SubscriptionStatus: subscriptionStatusParam,
|
||||
Limit: pgtype.Int4{
|
||||
Int32: limit,
|
||||
Valid: true,
|
||||
|
|
|
|||
|
|
@ -89,6 +89,21 @@ func (s *Service) GetAllUsers(
|
|||
query = &filter.Query
|
||||
}
|
||||
|
||||
var country *string
|
||||
if filter.Country != "" {
|
||||
country = &filter.Country
|
||||
}
|
||||
|
||||
var region *string
|
||||
if filter.Region != "" {
|
||||
region = &filter.Region
|
||||
}
|
||||
|
||||
var subscriptionStatus *string
|
||||
if filter.SubscriptionStatus != "" {
|
||||
subscriptionStatus = &filter.SubscriptionStatus
|
||||
}
|
||||
|
||||
offset := int32(filter.Page * filter.PageSize)
|
||||
|
||||
return s.userStore.GetAllUsers(
|
||||
|
|
@ -98,6 +113,9 @@ func (s *Service) GetAllUsers(
|
|||
query,
|
||||
before,
|
||||
after,
|
||||
country,
|
||||
region,
|
||||
subscriptionStatus,
|
||||
int32(filter.PageSize),
|
||||
offset,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -433,7 +433,10 @@ func (h *Handler) CheckUserPending(c *fiber.Ctx) error {
|
|||
// @Param page_size query int false "Page size"
|
||||
// @Param created_before query string false "Created before (RFC3339)"
|
||||
// @Param created_after query string false "Created after (RFC3339)"
|
||||
// @Param status query string false "Status filter (ACTIVE, PENDING, SUSPENDED, DEACTIVATED)"
|
||||
// @Param status query string false "User account status filter (ACTIVE, PENDING, SUSPENDED, DEACTIVATED)"
|
||||
// @Param country query string false "Country filter (case-insensitive match on stored value)"
|
||||
// @Param region query string false "Region filter (case-insensitive match on stored value)"
|
||||
// @Param subscription_status query string false "Derived subscription filter: ACTIVE, PENDING, or Unsubscribed (matches response subscription_status semantics)"
|
||||
// @Success 200 {object} response.APIResponse
|
||||
// @Failure 400 {object} response.APIResponse
|
||||
// @Failure 500 {object} response.APIResponse
|
||||
|
|
@ -467,9 +470,27 @@ func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
|
|||
createdAfter = domain.ValidTime{Value: parsed, Valid: true}
|
||||
}
|
||||
|
||||
subscriptionStatusQuery := strings.TrimSpace(c.Query("subscription_status"))
|
||||
var subscriptionStatusFilter string
|
||||
if subscriptionStatusQuery != "" {
|
||||
switch strings.ToUpper(subscriptionStatusQuery) {
|
||||
case "ACTIVE":
|
||||
subscriptionStatusFilter = "ACTIVE"
|
||||
case "PENDING":
|
||||
subscriptionStatusFilter = "PENDING"
|
||||
case "UNSUBSCRIBED":
|
||||
subscriptionStatusFilter = "Unsubscribed"
|
||||
default:
|
||||
return fiber.NewError(fiber.StatusBadRequest, `Invalid subscription_status; use ACTIVE, PENDING, or Unsubscribed`)
|
||||
}
|
||||
}
|
||||
|
||||
filter := domain.UserFilter{
|
||||
Role: c.Query("role"),
|
||||
Status: c.Query("status"),
|
||||
Country: strings.TrimSpace(c.Query("country")),
|
||||
Region: strings.TrimSpace(c.Query("region")),
|
||||
SubscriptionStatus: subscriptionStatusFilter,
|
||||
Page: int64(c.QueryInt("page", 1) - 1),
|
||||
PageSize: int64(c.QueryInt("page_size", 10)),
|
||||
Query: searchString.Value,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user