Skip to content

feat(billing): admin plan grants with duration/reason, 4-way checkout terms - #200

Merged
ihabkhaled merged 13 commits into
mainfrom
worktree-admin-plan-grants-checkout
Sep 2, 2026
Merged

feat(billing): admin plan grants with duration/reason, 4-way checkout terms#200
ihabkhaled merged 13 commits into
mainfrom
worktree-admin-plan-grants-checkout

Conversation

@ihabkhaled

Copy link
Copy Markdown
Owner

Summary

  • Admin plan grants now take a bounded duration (1-60 months, PLAN_GRANT_DURATION_INVALID on typos like 240) and a required reason (PLAN_GRANT_REASON_REQUIRED), attributed to the admin who made the grant and reused the existing lazy-expiry mechanism (findEffectiveForUser) with no new expiry sweep.
  • Self-service checkout and the public pricing page now offer 4 terms — 1/3/6/12 months (MONTHLY/QUARTERLY/SEMIANNUAL/YEARLY) — with a 10% discount on the 3- and 6-month terms, computed once at seed time and stored as immutable price rows, never derived at checkout.
  • Extracted PlansRepository.assignDefaultPlan to preserve the unattributed/unlimited semantics of a brand-new signup's default plan grant, now that assignUserToPlan exclusively means an attributed, time-limited admin grant.
  • Added a dedicated backfill seeder (plan-quarterly-semiannual-pricing.seeder.cjs) so the two new price rows actually reach already-seeded installs, not just fresh databases — this must run before the 4-way checkout terms are selectable anywhere but a brand-new install.

Release notes

Admins can now grant a user a plan for a specific number of months (1-60) with a required reason, visible as an audit trail — this replaces the old "assign forever, no record of why" behavior. Anyone checking out can now choose to pay for 1, 3, 6, or 12 months up front, with a 10% discount on the 3- and 6-month options (12 months keeps its existing ~16.7% discount). The admin plan-price editor can now publish prices for all four terms, not just monthly/yearly.

Remaining work

  • Manual browser UAT (admin grant flow end to end, all four checkout terms rendering real prices) was not run in this environment (no Docker/browser access) — recommend running it before merge.
  • The in-app /billing upgrade page and the public marketing pricing page both show the discount now; a few cosmetic/deferred items were parked in the final review (documented in the PR's linked plan/spec) as non-blocking: the public-pricing fallback catalog (offline-only path) still lists just 2 terms, and the admin assign-plan dialog doesn't hide its duration/reason fields when the target plan happens to be a trial plan (the server already silently ignores both fields on that path).

Test plan

  • auth-service: npx tsgo --noEmit && npm run lint && npm run test (782/782 tests)
  • payment-service: npx tsgo --noEmit && npm run lint && npm run test && npm run build
  • shared-types / shared-utilities: build + unit tests
  • frontend: npx tsgo --noEmit && npm run lint && npm run test && npm run build (2609/2609 tests)
  • Every task in the 17-task implementation plan passed an individual spec+quality review; the whole branch passed a final cross-task review plus one scoped fix-and-re-review round (seeder backfill gap, admin price editor, docs delta, billing-card discount display)
  • Full plan and spec: docs/superpowers/specs/2026-09-01-admin-plan-grants-and-multi-month-checkout-design.md, docs/superpowers/plans/2026-09-01-admin-plan-grants-and-multi-month-checkout.md

ihabkhaled and others added 13 commits September 2, 2026 00:40
… SEMIANNUAL

Replaces the binary YEARLY-or-else-add-1-month branching in
resolvePeriodEndMs with a 4-way lookup (MONTHS_BY_BILLING_INTERVAL) driving
addCalendarMonths from @claw/shared-utilities. Unifies YEARLY onto the same
calendar-clamping logic MONTHLY already used, fixing a latent bug where a
29-February start silently rolled into March instead of clamping to 28
February.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ance

Extracts assignDefaultPlan to preserve unattributed/unlimited signup-plan
semantics now that assignUserToPlan requires an admin grant's duration and
reason.
Migrates resolvePlanPrice, usePricingToggle/usePublicPricing, PlanTierCard
and PricingSection from a boolean isYearly toggle to the BillingInterval
enum (MONTHLY/QUARTERLY/SEMIANNUAL/YEARLY), and swaps PricingSection's
hand-rolled two-button toggle for the existing generic BillingIntervalToggle.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Selecting a plan in the admin users table now opens AssignPlanDialog
(Task 15) instead of assigning immediately: useUserTableState composes
a new useAssignPlanDialogState sub-hook to stay under the 50-line hook
guideline, and UserTable's onAssignPlan prop grows to
(userId, planId, durationMonths, grantReason). plansRepository.assignUser
and useAdminUserMutations' handleAssignPlan/useAssignPlanMutation carry
the new arguments through to the API; grantReason is deliberately
omitted from the structured log line since it may hold free-text about
a user's account. Adds the dialog's 8 admin.assignPlan* i18n keys, with
real translations, to all 13 locales.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…tor, docs delta

- CRITICAL: add plan-quarterly-semiannual-pricing.seeder.cjs to backfill
  QUARTERLY/SEMIANNUAL PlanPriceVersion rows on already-seeded installs
  (plan-catalog v2's run-once guard meant they never ran there); also
  null-guards upsertPrices against a null monthlyMinor.
- IMPORTANT: plan-price-editor.tsx interval picker now maps
  BILLING_INTERVAL_ORDER (4 options) instead of hardcoding MONTHLY/YEARLY.
- IMPORTANT: substantive docs delta — auth-service CLAUDE.md (grant split +
  backfill seeder), payment-service CLAUDE.md (resolvePeriodEndMs 4-way fix),
  docs/business/plan-allowances.md (checkout-term discount formula).
- MINOR: billing-plan-card.tsx shows the discountBadge for QUARTERLY/
  SEMIANNUAL, mutually exclusive with the existing yearly-saving line.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@ihabkhaled
ihabkhaled merged commit 42c3fdd into main Sep 2, 2026
110 checks passed
@ihabkhaled
ihabkhaled deleted the worktree-admin-plan-grants-checkout branch September 2, 2026 07:33
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