Skip to content

admin: GET /admin/promos/audit + /admin/promos/stats for promo lifecycle visibility - #59

Merged
mastermanas805 merged 2 commits into
masterfrom
feat/promo-issuance-audit-feed-fresh
May 13, 2026
Merged

mastermanas805 merged 2 commits into
masterfrom
feat/promo-issuance-audit-feed-fresh

Conversation

@mastermanas805

Copy link
Copy Markdown
Member

Summary

  • Adds GET /<admin-prefix>/promos/audit — paginated lifecycle feed (issued / redeemed / expired) joined to users for team email, with ?since, ?issued_by_email, ?event_type, ?limit, ?offset filters. Live SQL each call (no cache).
  • Adds GET /<admin-prefix>/promos/stats — totals + redemption rate + top_issuers + top_codes_by_redemption leaderboards. Redis-cached 5 min via cache.GetOrSet (singleflight + fail-open, same posture as team/summary).
  • Both endpoints sit behind the existing unguessable-prefix + RequireAdmin gates; neither is in the public OpenAPI spec (per the comment in openapi.go).
  • Extends internal/models/admin_promo_codes.go with ListPromoAuditEvents (CTE that unions the three lifecycle branches) and ComputePromoStats.

Freshness contract

  • /audit: uncached, ground truth per call.
  • /stats: 5-minute Redis cache (admin:promos:stats), Cache-Control: private, max-age=300. Polled by the founder dashboard tile; "5 min stale" is the right tradeoff for a numeric aggregate that drives no mutating UX.

Test plan

  • Issue + redeem + expire codes → audit returns the expected lifecycle events
  • /stats redemption_rate matches issued/redeemed exactly (rounded 4dp)
  • Cache window verified via miniredis.FastForward — second call within TTL returns cached payload; call after FastForward(PromoStatsCacheTTL + 1s) reflects the newly-inserted code
  • ?issued_by_email=X excludes events from other issuers
  • Non-admin caller → 403 with canonical agent_action
  • Invalid ?event_type → 400 (not silent empty list)
  • make test-unit green (all packages)

…cle visibility

Consolidates issued / redeemed / expired events from admin_promo_codes
into a single audit feed and a totals tile. The /audit endpoint is
live SQL each call (admins need to see "issued at 3 sec ago"); /stats
is Redis-cached 5 min via cache.GetOrSet — same singleflight + fail-open
posture as team/summary and billing/usage. Both endpoints are gated by
the existing unguessable-prefix + RequireAdmin allowlist; neither
appears in the public OpenAPI spec.
…-audit-feed-fresh

# Conflicts:
#	internal/router/router.go
@mastermanas805
mastermanas805 merged commit 629e0a9 into master May 13, 2026
mastermanas805 added a commit that referenced this pull request May 14, 2026
…obby_plus copy (#107)

Wave FIX-H wraps up the BugBash B36 backup-correctness items so customer-
facing restore stops being one accidental retry away from a destroyed
database.

#57/#Q45 — restore replay guard
  Adds models.HasInflightRestore; a second POST /restore for the same
  resource while a prior row is pending/running returns 409
  restore_in_progress + AgentActionRestoreInflight. Fail-CLOSED on DB
  error (concurrent pg_restore --clean races itself).

#58/#A2 — restore to a new DB
  Accepts optional target_resource_id. Worker restores into the target
  (same team only); audit row carries both source + target ids. Skips
  the destructive-ack ceremony because the agent already opted into a
  fresh database.

#59 — backup integrity
  Migration 043 adds a nullable sha256 TEXT column to resource_backups.
  Worker stamps the digest at finalize; restore handler verifies before
  pg_restore. NULL on legacy rows is logged + accepted (fail-open on
  pre-043 data).

#64/#Q46 — cross-tenant 404
  GetBackupByIDForTeam joins resources to scope by team; cross-tenant
  backup_id guess now returns 404 backup_not_found instead of
  400 backup_resource_mismatch. Matches FIX-B tenant-isolation posture.

#65/#Q47 — refund quota on failure
  New internal endpoint POST /internal/teams/:id/backup-quota/refund
  (WORKER_INTERNAL_JWT_SECRET HS256, fail-closed when unset). The
  worker calls this when a MANUAL backup fails terminally so the
  team's daily manual-backups counter is credited back.

#66/#Q48 — hobby agent_action points to Hobby Plus
  AgentActionRestoreRequiresHobbyPlus added. The Hobby-tier restore 402
  now nudges Hobby Plus ($19/mo, restore enabled) instead of skipping
  the customer past the cheapest restore-enabled plan onto Pro ($49).

#67/#Q49 — destructive ack required for in-place restore
  In-place restore (no target_resource_id) now requires
  destructive_acknowledgment: true in the body. pg_restore --clean drops
  every table — refusing without an explicit ack prevents an agent
  testing a backup from wiping a live customer DB.

#Q50 — RPO/RTO on /capabilities
  Adds rpo_minutes + rto_minutes per tier in plans.yaml (anonymous/free
  = 0/0, hobby/hobby_plus = 1440/30, pro/team = 60/15) wired into the
  /api/v1/capabilities matrix via plans.Registry.RPOMinutes/RTOMinutes
  (added in instant.dev/common/plans#13).

Tests
  - +5 backup_test.go cases: ReplayBlocked, TargetNewDB,
    RequiresDestructiveAck, HobbyAgentActionPointsToHobbyPlus,
    CrossTenantBackupID_404
  - All prior backup tests updated to include destructive_acknowledgment
  - Contract test enforces the four new agent_action constants

DO NOT TOUCH list respected — no edits to email/, dpop.go, circuit/.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
mastermanas805 added a commit that referenced this pull request May 21, 2026
…obby_plus copy

Wave FIX-H wraps up the BugBash B36 backup-correctness items so customer-
facing restore stops being one accidental retry away from a destroyed
database.

#57/#Q45 — restore replay guard
  Adds models.HasInflightRestore; a second POST /restore for the same
  resource while a prior row is pending/running returns 409
  restore_in_progress + AgentActionRestoreInflight. Fail-CLOSED on DB
  error (concurrent pg_restore --clean races itself).

#58/#A2 — restore to a new DB
  Accepts optional target_resource_id. Worker restores into the target
  (same team only); audit row carries both source + target ids. Skips
  the destructive-ack ceremony because the agent already opted into a
  fresh database.

#59 — backup integrity
  Migration 043 adds a nullable sha256 TEXT column to resource_backups.
  Worker stamps the digest at finalize; restore handler verifies before
  pg_restore. NULL on legacy rows is logged + accepted (fail-open on
  pre-043 data).

#64/#Q46 — cross-tenant 404
  GetBackupByIDForTeam joins resources to scope by team; cross-tenant
  backup_id guess now returns 404 backup_not_found instead of
  400 backup_resource_mismatch. Matches FIX-B tenant-isolation posture.

#65/#Q47 — refund quota on failure
  New internal endpoint POST /internal/teams/:id/backup-quota/refund
  (WORKER_INTERNAL_JWT_SECRET HS256, fail-closed when unset). The
  worker calls this when a MANUAL backup fails terminally so the
  team's daily manual-backups counter is credited back.

#66/#Q48 — hobby agent_action points to Hobby Plus
  AgentActionRestoreRequiresHobbyPlus added. The Hobby-tier restore 402
  now nudges Hobby Plus ($19/mo, restore enabled) instead of skipping
  the customer past the cheapest restore-enabled plan onto Pro ($49).

#67/#Q49 — destructive ack required for in-place restore
  In-place restore (no target_resource_id) now requires
  destructive_acknowledgment: true in the body. pg_restore --clean drops
  every table — refusing without an explicit ack prevents an agent
  testing a backup from wiping a live customer DB.

#Q50 — RPO/RTO on /capabilities
  Adds rpo_minutes + rto_minutes per tier in plans.yaml (anonymous/free
  = 0/0, hobby/hobby_plus = 1440/30, pro/team = 60/15) wired into the
  /api/v1/capabilities matrix via plans.Registry.RPOMinutes/RTOMinutes
  (added in instant.dev/common/plans#13).

Tests
  - +5 backup_test.go cases: ReplayBlocked, TargetNewDB,
    RequiresDestructiveAck, HobbyAgentActionPointsToHobbyPlus,
    CrossTenantBackupID_404
  - All prior backup tests updated to include destructive_acknowledgment
  - Contract test enforces the four new agent_action constants

DO NOT TOUCH list respected — no edits to email/, dpop.go, circuit/.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant