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_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('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
|
ORDER BY created_at DESC
|
||||||
LIMIT sqlc.narg('limit')::INT
|
LIMIT sqlc.narg('limit')::INT
|
||||||
OFFSET sqlc.narg('offset')::INT;
|
OFFSET sqlc.narg('offset')::INT;
|
||||||
|
|
|
||||||
20
docs/docs.go
20
docs/docs.go
|
|
@ -8486,9 +8486,27 @@ const docTemplate = `{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Status filter (ACTIVE, PENDING, SUSPENDED, DEACTIVATED)",
|
"description": "User account status filter (ACTIVE, PENDING, SUSPENDED, DEACTIVATED)",
|
||||||
"name": "status",
|
"name": "status",
|
||||||
"in": "query"
|
"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": {
|
"responses": {
|
||||||
|
|
|
||||||
|
|
@ -8478,9 +8478,27 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Status filter (ACTIVE, PENDING, SUSPENDED, DEACTIVATED)",
|
"description": "User account status filter (ACTIVE, PENDING, SUSPENDED, DEACTIVATED)",
|
||||||
"name": "status",
|
"name": "status",
|
||||||
"in": "query"
|
"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": {
|
"responses": {
|
||||||
|
|
|
||||||
|
|
@ -8017,10 +8017,23 @@ paths:
|
||||||
in: query
|
in: query
|
||||||
name: created_after
|
name: created_after
|
||||||
type: string
|
type: string
|
||||||
- description: Status filter (ACTIVE, PENDING, SUSPENDED, DEACTIVATED)
|
- description: User account status filter (ACTIVE, PENDING, SUSPENDED, DEACTIVATED)
|
||||||
in: query
|
in: query
|
||||||
name: status
|
name: status
|
||||||
type: string
|
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:
|
produces:
|
||||||
- application/json
|
- application/json
|
||||||
responses:
|
responses:
|
||||||
|
|
|
||||||
|
|
@ -386,19 +386,62 @@ WHERE
|
||||||
))
|
))
|
||||||
AND ($4::TIMESTAMPTZ IS NULL OR created_at >= $4::TIMESTAMPTZ)
|
AND ($4::TIMESTAMPTZ IS NULL OR created_at >= $4::TIMESTAMPTZ)
|
||||||
AND ($5::TIMESTAMPTZ IS NULL OR created_at <= $5::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
|
ORDER BY created_at DESC
|
||||||
LIMIT $7::INT
|
LIMIT $10::INT
|
||||||
OFFSET $6::INT
|
OFFSET $9::INT
|
||||||
`
|
`
|
||||||
|
|
||||||
type GetAllUsersParams struct {
|
type GetAllUsersParams struct {
|
||||||
Role pgtype.Text `json:"role"`
|
Role pgtype.Text `json:"role"`
|
||||||
Status pgtype.Text `json:"status"`
|
Status pgtype.Text `json:"status"`
|
||||||
Query pgtype.Text `json:"query"`
|
Query pgtype.Text `json:"query"`
|
||||||
CreatedAfter pgtype.Timestamptz `json:"created_after"`
|
CreatedAfter pgtype.Timestamptz `json:"created_after"`
|
||||||
CreatedBefore pgtype.Timestamptz `json:"created_before"`
|
CreatedBefore pgtype.Timestamptz `json:"created_before"`
|
||||||
Offset pgtype.Int4 `json:"offset"`
|
Country pgtype.Text `json:"country"`
|
||||||
Limit pgtype.Int4 `json:"limit"`
|
Region pgtype.Text `json:"region"`
|
||||||
|
SubscriptionStatus pgtype.Text `json:"subscription_status"`
|
||||||
|
Offset pgtype.Int4 `json:"offset"`
|
||||||
|
Limit pgtype.Int4 `json:"limit"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetAllUsersRow struct {
|
type GetAllUsersRow struct {
|
||||||
|
|
@ -441,6 +484,9 @@ func (q *Queries) GetAllUsers(ctx context.Context, arg GetAllUsersParams) ([]Get
|
||||||
arg.Query,
|
arg.Query,
|
||||||
arg.CreatedAfter,
|
arg.CreatedAfter,
|
||||||
arg.CreatedBefore,
|
arg.CreatedBefore,
|
||||||
|
arg.Country,
|
||||||
|
arg.Region,
|
||||||
|
arg.SubscriptionStatus,
|
||||||
arg.Offset,
|
arg.Offset,
|
||||||
arg.Limit,
|
arg.Limit,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -125,8 +125,11 @@ type UserProfileResponse struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type UserFilter struct {
|
type UserFilter struct {
|
||||||
Role string
|
Role string
|
||||||
Status string
|
Status string
|
||||||
|
Country string
|
||||||
|
Region string
|
||||||
|
SubscriptionStatus string // display filter: ACTIVE, PENDING, Unsubscribed (same as API subscription_status values)
|
||||||
|
|
||||||
Page int64
|
Page int64
|
||||||
PageSize int64
|
PageSize int64
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,9 @@ type UserStore interface {
|
||||||
status *string,
|
status *string,
|
||||||
query *string,
|
query *string,
|
||||||
createdBefore, createdAfter *time.Time,
|
createdBefore, createdAfter *time.Time,
|
||||||
|
country *string,
|
||||||
|
region *string,
|
||||||
|
subscriptionStatus *string,
|
||||||
limit, offset int32,
|
limit, offset int32,
|
||||||
) ([]domain.User, int64, error)
|
) ([]domain.User, int64, error)
|
||||||
ListAccountDeletionRequests(ctx context.Context, filter domain.AccountDeletionRequestFilter) ([]domain.AccountDeletionRequest, int64, error)
|
ListAccountDeletionRequests(ctx context.Context, filter domain.AccountDeletionRequestFilter) ([]domain.AccountDeletionRequest, int64, error)
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,9 @@ func (s *Store) GetAllUsers(
|
||||||
status *string,
|
status *string,
|
||||||
query *string,
|
query *string,
|
||||||
createdBefore, createdAfter *time.Time,
|
createdBefore, createdAfter *time.Time,
|
||||||
|
country *string,
|
||||||
|
region *string,
|
||||||
|
subscriptionStatus *string,
|
||||||
limit, offset int32,
|
limit, offset int32,
|
||||||
) ([]domain.User, int64, error) {
|
) ([]domain.User, int64, error) {
|
||||||
|
|
||||||
|
|
@ -442,12 +445,30 @@ func (s *Store) GetAllUsers(
|
||||||
createdBeforeParam = pgtype.Timestamptz{Time: *createdBefore, Valid: true}
|
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{
|
params := dbgen.GetAllUsersParams{
|
||||||
Role: roleParam,
|
Role: roleParam,
|
||||||
Status: statusParam,
|
Status: statusParam,
|
||||||
Query: queryParam,
|
Query: queryParam,
|
||||||
CreatedAfter: createdAfterParam,
|
CreatedAfter: createdAfterParam,
|
||||||
CreatedBefore: createdBeforeParam,
|
CreatedBefore: createdBeforeParam,
|
||||||
|
Country: countryParam,
|
||||||
|
Region: regionParam,
|
||||||
|
SubscriptionStatus: subscriptionStatusParam,
|
||||||
Limit: pgtype.Int4{
|
Limit: pgtype.Int4{
|
||||||
Int32: limit,
|
Int32: limit,
|
||||||
Valid: true,
|
Valid: true,
|
||||||
|
|
|
||||||
|
|
@ -89,6 +89,21 @@ func (s *Service) GetAllUsers(
|
||||||
query = &filter.Query
|
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)
|
offset := int32(filter.Page * filter.PageSize)
|
||||||
|
|
||||||
return s.userStore.GetAllUsers(
|
return s.userStore.GetAllUsers(
|
||||||
|
|
@ -98,6 +113,9 @@ func (s *Service) GetAllUsers(
|
||||||
query,
|
query,
|
||||||
before,
|
before,
|
||||||
after,
|
after,
|
||||||
|
country,
|
||||||
|
region,
|
||||||
|
subscriptionStatus,
|
||||||
int32(filter.PageSize),
|
int32(filter.PageSize),
|
||||||
offset,
|
offset,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -433,7 +433,10 @@ func (h *Handler) CheckUserPending(c *fiber.Ctx) error {
|
||||||
// @Param page_size query int false "Page size"
|
// @Param page_size query int false "Page size"
|
||||||
// @Param created_before query string false "Created before (RFC3339)"
|
// @Param created_before query string false "Created before (RFC3339)"
|
||||||
// @Param created_after query string false "Created after (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
|
// @Success 200 {object} response.APIResponse
|
||||||
// @Failure 400 {object} response.APIResponse
|
// @Failure 400 {object} response.APIResponse
|
||||||
// @Failure 500 {object} response.APIResponse
|
// @Failure 500 {object} response.APIResponse
|
||||||
|
|
@ -467,14 +470,32 @@ func (h *Handler) GetAllUsers(c *fiber.Ctx) error {
|
||||||
createdAfter = domain.ValidTime{Value: parsed, Valid: true}
|
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{
|
filter := domain.UserFilter{
|
||||||
Role: c.Query("role"),
|
Role: c.Query("role"),
|
||||||
Status: c.Query("status"),
|
Status: c.Query("status"),
|
||||||
Page: int64(c.QueryInt("page", 1) - 1),
|
Country: strings.TrimSpace(c.Query("country")),
|
||||||
PageSize: int64(c.QueryInt("page_size", 10)),
|
Region: strings.TrimSpace(c.Query("region")),
|
||||||
Query: searchString.Value,
|
SubscriptionStatus: subscriptionStatusFilter,
|
||||||
CreatedBefore: createdBefore,
|
Page: int64(c.QueryInt("page", 1) - 1),
|
||||||
CreatedAfter: createdAfter,
|
PageSize: int64(c.QueryInt("page_size", 10)),
|
||||||
|
Query: searchString.Value,
|
||||||
|
CreatedBefore: createdBefore,
|
||||||
|
CreatedAfter: createdAfter,
|
||||||
}
|
}
|
||||||
|
|
||||||
if valErrs, ok := h.validator.Validate(c, filter); !ok {
|
if valErrs, ok := h.validator.Validate(c, filter); !ok {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user