feat(ops): use 'reset' quota status for plan-usage reset and invalidate quota cache
When ops staff reset a tenant's current-plan usage, the existing 'refunded' terminal status conflated user-initiated refunds with ops-driven resets, and tenants still saw the old balance in the top-nav chip until the quota-summary cache TTL expired. - Introduce a dedicated 'reset' status for quota_reservations, separate from 'refunded'. Both the initial and an incremental migration widen the CHECK constraint to allow it; the down migration folds 'reset' rows back into 'refunded' before tightening the constraint. - resetQuotaReservations / resetAIPointReservations now write 'reset' without touching refunded_amount, and stop cascading into ai_point_usage_logs (carried no audit value for ops resets). - AdminUserService gains an optional cache dependency and calls invalidateQuotaSummaryCache(tenant_id) immediately after a successful reset so the tenant's top-nav chip refreshes on the next request without waiting on TTL. - Wire the existing appCache into AdminUserService at boot. - Drop the now-unused ai_point_usage_logs field from the reset response and from the ops-web AdminUsersView type. - Add a unit test covering the cache-key invalidation contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -91,7 +91,6 @@ type ResetPlanUsageResult struct {
|
||||
ArticleAmount int64
|
||||
AIPointReservations int64
|
||||
AIPointsAmount int64
|
||||
AIPointUsageLogs int64
|
||||
ArticleWindowStart time.Time
|
||||
ArticleWindowEnd time.Time
|
||||
AIPointsWindowStart time.Time
|
||||
@@ -576,13 +575,12 @@ LIMIT 1`, userID).Scan(&tenantID); err != nil {
|
||||
}
|
||||
|
||||
if aiEnd.After(aiStart) {
|
||||
count, amount, logCount, err := resetAIPointReservations(ctx, tx, tenantID, aiStart, aiEnd)
|
||||
count, amount, err := resetAIPointReservations(ctx, tx, tenantID, aiStart, aiEnd)
|
||||
if err != nil {
|
||||
return nil, ResetPlanUsageResult{}, err
|
||||
}
|
||||
result.AIPointReservations = count
|
||||
result.AIPointsAmount = amount
|
||||
result.AIPointUsageLogs = logCount
|
||||
}
|
||||
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
@@ -600,12 +598,11 @@ func resetQuotaReservations(ctx context.Context, tx pgx.Tx, tenantID int64, quot
|
||||
row := tx.QueryRow(ctx, `
|
||||
WITH reset AS (
|
||||
UPDATE quota_reservations
|
||||
SET status = 'refunded',
|
||||
refunded_amount = reserved_amount,
|
||||
SET status = 'reset',
|
||||
updated_at = NOW()
|
||||
WHERE tenant_id = $1
|
||||
AND quota_type = $2
|
||||
AND status IN ('pending', 'confirmed')
|
||||
AND status = 'confirmed'
|
||||
AND created_at >= $3
|
||||
AND created_at < $4
|
||||
RETURNING reserved_amount
|
||||
@@ -622,43 +619,28 @@ FROM reset`, tenantID, quotaType, startAt.UTC(), endAt.UTC())
|
||||
return count, amount, nil
|
||||
}
|
||||
|
||||
func resetAIPointReservations(ctx context.Context, tx pgx.Tx, tenantID int64, startAt, endAt time.Time) (int64, int64, int64, error) {
|
||||
func resetAIPointReservations(ctx context.Context, tx pgx.Tx, tenantID int64, startAt, endAt time.Time) (int64, int64, error) {
|
||||
row := tx.QueryRow(ctx, `
|
||||
WITH reset AS (
|
||||
UPDATE quota_reservations
|
||||
SET status = 'refunded',
|
||||
refunded_amount = reserved_amount,
|
||||
SET status = 'reset',
|
||||
updated_at = NOW()
|
||||
WHERE tenant_id = $1
|
||||
AND quota_type = 'ai_points'
|
||||
AND status IN ('pending', 'confirmed')
|
||||
AND status = 'confirmed'
|
||||
AND created_at >= $2
|
||||
AND created_at < $3
|
||||
RETURNING id, reserved_amount
|
||||
),
|
||||
logs AS (
|
||||
UPDATE ai_point_usage_logs l
|
||||
SET status = 'refunded',
|
||||
error_message = 'ops reset current plan usage',
|
||||
completed_at = COALESCE(l.completed_at, NOW()),
|
||||
updated_at = NOW()
|
||||
FROM reset r
|
||||
WHERE l.tenant_id = $1
|
||||
AND l.quota_reservation_id = r.id
|
||||
AND l.status IN ('pending', 'completed')
|
||||
RETURNING l.id
|
||||
RETURNING reserved_amount
|
||||
)
|
||||
SELECT (SELECT COUNT(*) FROM reset)::BIGINT,
|
||||
COALESCE((SELECT SUM(reserved_amount) FROM reset), 0)::BIGINT,
|
||||
(SELECT COUNT(*) FROM logs)::BIGINT`, tenantID, startAt.UTC(), endAt.UTC())
|
||||
COALESCE((SELECT SUM(reserved_amount) FROM reset), 0)::BIGINT`, tenantID, startAt.UTC(), endAt.UTC())
|
||||
|
||||
var reservationCount int64
|
||||
var amount int64
|
||||
var logCount int64
|
||||
if err := row.Scan(&reservationCount, &amount, &logCount); err != nil {
|
||||
return 0, 0, 0, err
|
||||
if err := row.Scan(&reservationCount, &amount); err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return reservationCount, amount, logCount, nil
|
||||
return reservationCount, amount, nil
|
||||
}
|
||||
|
||||
func (r *AdminUserRepository) ChangeRole(ctx context.Context, userID int64, tenantRole, workspaceRole string) (*AdminUserRecord, error) {
|
||||
|
||||
Reference in New Issue
Block a user