Conversation
…h enforcement - lib/maintenance.ts: new getActiveOrgMaintenanceWindow(orgId) read helper - scripts/payouts/create-payout-batch.ts: skips individual tenants with active OFFLINE windows in the per-org loop - __tests__/enterprise/org-maintenance-window.test.ts: 4 unit tests pin the scoping contract Admin write API, per-org Redis keys, and Novu wiring deferred to a separate follow-up under #746. Part of #746 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ment.organizationId Resolution chain now: explicit arg → plan.organizationId → appointment.organizationId → null. Closes the case where the plan is platform-owned but the booking is org-funded. createConsultationChannel/createSubscriptionChannel updated; DM channels intentionally remain untagged per scope. Part of #674 Part of #746 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…l audit Adds the column + relation + index; writes the value at create time. Org call audit route indexes directly instead of joining through SlotOfAppointment. Migration 20260528_meeting_session_organization_id_denorm applied to Supabase. Part of #674 Part of #746 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- BillingSubscription.renewalReminderSentAt — once-per-cycle gate - NOVU_WORKFLOWS.ORG_LICENSE_RENEWAL_UPCOMING + payload + notifier - generate-subscription-invoices.ts: 7-day-out scan at job entry, claim-then-trigger Migration 20260528_billing_subscription_renewal_reminder applied to Supabase. Part of #746 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New 36-cross-cutting-integrations.md is the single reference for which subsystems are enterprise-wired vs deliberately skipped, with code paths and rationale per row. Covers booking, money, membership, programs, Stream, compliance, operational, and the 25 dashboard routes plus an explicit SKIP list. Indexed from 00-overview.md row 36. Part of #746 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part of #768 Lockdown #2 + #3 — drops ProgramType.PROJECT/RETAINER, PayoutArrangement AOR/EOR, the PROGRAM_TYPE_NOT_AVAILABLE rejection guard in POST /organizations/[orgId]/programs, its error label, and the dedicated test file. CreateBodySchema's discriminated union already rejects unknown program types as "Invalid body" — no need for a dedicated v2 hint now that the enum no longer carries the reserved values. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part of #768 Lockdown #4 — hierarchy fields were schema-only with no UI/walks. Drops both indexes (@@index parentId, @@index rootId), the OrgTree self-relation, and the helper module lib/api/organizations/hierarchy.ts. Cascades: - POST /organizations no longer seeds rootId/depth. - consolidated-invoice-rollup script/route/job removed (the rollup semantics presupposed a parent→child rollup that has no schema basis any more). - ENABLE_CONSOLIDATED_INVOICE flag no longer gates anything; orphan dropped via maintenance-cron cleanup list. Seed at prisma/seedFiles/15a-create-organizations.ts still writes rootId; will be cleaned up in Wave 7 once prisma generate runs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part of #768 Lockdown #26 (Comment 11 amendment) — same anti-pattern as Contract.terms. canSponsor + canHost cover the 4 reachable shapes; no real long-tail capability uses this column. Drop pre-MVP and add typed booleans when an actual capability emerges. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part of #768 Lockdown #10 — terms was 'NET-X, caps, custom clauses'. NET-X already lives in paymentTermsDays; caps live in LicensedSeatConfig / CreditPoolConfig. The 'custom clauses' tail had no concrete reader. Drop pre-MVP; if a customer requests freeform clauses later, file a ContractClause child table at that point. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part of #768, #769 Lockdown #13 — HRIS connector schema was stubbed only. Per #769 vendor strategy, when the first customer asks for HRIS sync we integrate Merge.dev (200+ providers via one unified API; ~$10K-30K/yr) rather than rebuilding per-platform. Removed: - prisma models: HrisConfig, HrisSyncJob, HrisEmployeeMap - enums: HrisProvider, HrisSyncStatus - routes: app/api/organizations/[orgId]/hris/{,csv-upload,sync} - flag: lib/feature-flags.ts ENABLE_HRIS - audit actions: SYSTEM.HRIS_SYNC_STARTED/COMPLETED/FAILED The schema cost of HRIS integration is zero — Merge.dev returns employee directory on demand against the customer's source-of-truth. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes part of #768 (Lockdown #19 / Comment 5) SlotAllocationService.createAppointments was creating new Appointment rows with organizationId=null even when the booking originated from an org-funded membership. The org dashboard's /api/organizations/[orgId]/appointments silently underreported SUBSCRIPTION lazy allocations, CLASS pre-allocated sessions, and CONSULTATION reschedules. Fix: - fetchEventData now resolves organizationId per event type: SUBSCRIPTION → placeholder Appointment Payment.organizationId CLASS → classPlan.organizationId (host wins; per #768 design) CONSULTATION → existing Appointment.organizationId (reschedule keeps tag) WEBINAR → webinarPlan.organizationId (shared-across-attendees) - createAppointments now accepts organizationId and writes it on every appointment.create call. - autoAllocate + manualAllocate thread the resolved value. Test: __tests__/enterprise/appointment-org-stamping.test.ts pins the 8-row resolution matrix. Marketplace WEBINAR registrant attribution stays on Payment.organizationId (intentional — webinars are SHARED). Same for CLASS enrolment: a Wipro learner enrolling in an Infosys-hosted class doesn't override the host's slot tag; their funding is tracked via Payment. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part of #768 Lockdown #11 (highest priority — blocks GST audit): - Schema: drop OrganizationInvoice.items Json; add InvoiceLineItem model with typed columns (description, quantity, unitPricePaise, taxPaise, hsnCode, paymentId) + position for render order + cascade on invoice delete + SetNull on Payment delete. - Writer: POST /organizations/[orgId]/billing-account/invoices creates child rows inside the same transaction via nested-create. - Writer: jobs/billing/generate-subscription-invoices.ts (license cycle cron) uses nested-create for the single auto-generated line. - Reader: invoice PDF route reads invoice.lineItems(orderBy position asc), maps to OrgInvoiceLineItem shape for the renderer. parseLineItems Json adapter removed. Now SQL queries like 'sum line totals by HSN code' or 'list lines linked to Payment X' run as flat joins instead of Json extraction. Wave-1 cascade fixes (clears tsc warnings from dropped enum values and dropped Contract.terms): - Remove PROJECT key from FUNDING_SOURCE_LABEL/TAGLINE/BADGE_CLASS in lib/labels/org-labels.ts. - Remove the PROJECT case in OrgPayerSelector.tsx. - Remove PROJECT + RETAINER rows from programs page PROGRAM_TYPE config. - Drop PROJECT-rejection guard in lib/payments/operations/checkout.ts (the enum no longer carries it). - Drop terms write in POST /organizations/[orgId]/contracts. - BillingPageClient summary schema: drop PROJECT from runtime z.enum. tsc clean (0 non-seed errors). Seed file errors remain for Wave 7. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part of #768 Lockdown #7 — legacy Invoice predated the enterprise stack. Per #768 Comment 1.C.1 it modelled the same flow as OrganizationInvoice with divergent semantics (one invoice per Payment vs. one invoice per cycle). Keeping both invited the 'which one do I query?' trap and doubled GST / IRP maintenance. Per design decision: drop the legacy model and route personal-consultee billing through email-to-support until v1.1 re-introduces a per-Payment invoice surface. Removed: - prisma model Invoice + Payment.invoice back-relation - routes app/api/invoices/{,id}/{,pdf}/route.ts - module lib/payments/payouts/invoice-service.ts (whole helper) - callers: * lib/payments/webhooks/handlers.ts no longer calls createInvoiceFromPayment from the success webhook * dashboard/consultee/[id]/payments/route.ts returns invoices=[] instead of querying Invoice (response shape preserved) - public re-exports from lib/payments/payouts/index.ts PaymentsTab UI renders no invoice rows for personal-card consultees; the org-funded INVOICE flow (OrganizationInvoice) is unaffected. tsc clean (0 non-seed errors). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part of #768 Lockdown #8 — disambiguates the consultant-side 1099-style payout flow from OrganizationPayout (org-share for HOST/HYBRID orgs). Removes the 'which one do I query?' trap per #768 Comment 1.C.2. Schema: - model Payout → model ConsultantPayout (docstring explains the split). - ConsultantProfile.payouts ConsultantPayout[] - ConsultantEarnings.payout ConsultantPayout? - TDSRecord.payout ConsultantPayout? Code: bulk rename across 13 files: - prisma.payout.* → prisma.consultantPayout.* - tx.payout.* → tx.consultantPayout.* - Prisma.PayoutWhereInput → Prisma.ConsultantPayoutWhereInput (and other Prisma input-arg types) UI API-response type `interface Payout` in types/payouts.ts is deliberately preserved — it's a UI/API shape, not the Prisma model, and "Payout" is the right UX vocabulary for consultant earnings. tsc clean (0 non-seed errors). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part of #768 Lockdown #9 — naming convention parity per #768 Comment 1.C.3. Schema renames: - ConsultantEarnings.platformFee → platformFeePaise - ConsultantEarnings.consultantShare → consultantSharePaise - Refund.amount → amountPaise - Dispute.amount → amountPaise All readers/writers updated across: - ConsultantEarnings: dashboard/consultant/[id] route, lib/payments, scripts/payouts, scripts/refunds, lib/api/operators, tests - Refund: lib/api/operators/stats, lib/payments/operations/refund, lib/referrals/service, app/api/webhooks/utils, admin analytics, staff support tickets, actions/maintenance/freeze-appointments, scripts/refunds/{cascade-refund-earnings,reconcile-pending-refunds} - Dispute: app/api/webhooks/utils, app/api/payments/disputes, app/api/admin/maintenance, scripts/disputes/alert-dispute-deadlines Payment.amount + OrganizationEarnings.platformFeePaise (already Paise) are unchanged. tsc clean (0 non-seed errors). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ultantTaxInfo) Part of #768 Lockdown #5 — PAN PII compliance posture parity. Plaintext column dropped; replaced with panEncrypted (Bytes, AES-256-GCM) + panLast4 for the 'PAN ending XXXX' UI. Mirrors ConsultantTaxInfo.panEncrypted. Writers (POST/PATCH org routes) encrypt at write time via encryptPAN helper from lib/payments/tax/pan-crypto. Pre-MVP DB reset means no backfill required. Readers: - org-payout-service TDS-rate derivation now branches on 'panEncrypted is non-null' (treats no-PAN as the higher-rate path). Plaintext decrypt deferred to Form 26Q admin filing. tsc clean (0 non-seed errors). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part of #768, #769 Lockdown items shipped: - #6 OrgBrandingProfile carved out from Organization (1:1 sibling mirroring ConsultantProfile / OrgWorkspaceProfile). Branding columns (logo, bannerImage, primaryColor, secondaryColor, description, industry, website, sizeBucket) moved off the God Model. - #12 WebinarCollaborator + ClassCollaborator permissions Json replaced with typed booleans (canApprovePayment, canViewAnalytics, canEditEvent, canSeeAttendees). - #14/#15 OverageEvent table (append-only, Stripe Meter inspired) + LicensedSeatConfig.maxOveragePerCyclePaise + CreditPoolConfig.maxOveragePerCyclePaise circuit-breaker columns. - #16 Organization.kybVerifiedAt + sumsubApplicantId (Sumsub KYB stub). - #28 OrganizationPlan.config Json split into 4 per-type child models (OrgConsultationPlanConfig, OrgSubscriptionPlanConfig, OrgWebinarPlanConfig, OrgClassPlanConfig). Each has typed columns matching the public plan-type fields. From #769 Comment 4: - Organization.taxJurisdiction TaxJurisdiction @default(IN) + TaxJurisdiction enum prep for the multi-jurisdiction refactor when the first non-India customer signs. Caller cascades (writers + readers updated to brandingProfile + the typed configs): - POST /organizations creates OrgBrandingProfile in same tx when any branding column is set. - POST /organizations/[orgId]/catalog now takes a typed discriminated union over planType (config schema per type) and writes the matching child via nested-create. - branding/[asset]/route.ts upserts on OrgBrandingProfile. - Public org explore page + invitations preview + org-list + auth session payload all read brandingProfile and flatten at the response boundary so the UI shape stays stable. tsc clean (0 non-seed errors). prisma validate clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part of #768 Lockdown #17 — REACHABLE_ORG_FUNDING_PATHS (lib/enterprise/reachable-paths.ts) codifies the 7 post-Programs-v2-drop combinations of (capability, fundingSource, programType). Routes, UI gates, and analytics consult the constant rather than enumerating raw enums; test pin at __tests__/enterprise/reachable-paths.test.ts asserts the matrix and the canonical capabilityOf() resolver. Lockdown #23 — Coming Soon scaffolding: - lib/api/feature-pending.ts:respondFeaturePending(feature) emits the standard 501 + FEATURE_PENDING code; COMING_SOON_FEATURES lists the 8 stubbed v0 surfaces per #768 Comment 6 table. - components/enterprise/ComingSoonBadge.tsx renders the disabled-tooltip control. Single style/copy source so v1.1 reactivation flips in one place. Wiring of the 8 stubbed surfaces into their respective routes is deferred to a follow-up commit (touches multiple routes; this commit delivers the helpers + types). tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part of #768, closes #674 (scope filter loose end) Lockdown #25 — RFX-7 from ENTERPRISE_PRODUCTION_GRADE_CHECKLIST. The serializer wrote "__personal__" while the resolver expected "personal", and the URL middle-state "none" added ambiguity. Renamed the canonical sentinel to "mine": - ORG_FILTER_PERSONAL constant value: "__personal__" → "mine". - serializeScope returns "mine" for personal scope. - resolveOrgScope accepts "mine" canonically and "personal" as legacy alias (deprecation grace; drop in v1.1). UI components (planner, requests, documents, AppointmentsTab) import ORG_FILTER_PERSONAL by name and pass-through, so the rename ripples through without per-page edits. Also: ComingSoonBadge.tsx fix the cn helper import path (@/lib/utils → @/utils/tailwind to match the repo's actual layout). tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Part of #768 Lockdown #7 (Wave 7) — seed updates so `prisma db reset && prisma db seed` succeeds against the new schema: - prisma/seedFiles/12a-create-refunds.ts: amount → amountPaise - prisma/seedFiles/12b-create-disputes.ts: amount → amountPaise - prisma/seedFiles/validation/impossible-cases.ts: amount → amountPaise - prisma/seedFiles/13d-create-invoices.ts: DELETED (legacy Invoice dropped; per-Payment invoicing follow-up routed to email-to-support) - prisma/seed.ts: drop createInvoices() call + import - prisma/seedFiles/15a-create-organizations.ts: * drop HrisProvider + HrisSyncStatus enum imports * drop backfillRootId + rootId/parentId/depth writes * branding fields (industry/website/description) → OrgBrandingProfile nested-create * pan plaintext → panEncrypted via encryptPAN (mirrors writer routes) * Contract.terms Json blocks → typed paymentTermsDays + cap columns * HRIS three-table seeding (hrisConfig, hrisSyncJob, hrisEmployeeMap) deleted entirely * OrganizationInvoice.items Json → InvoiceLineItem nested-create User-facing artifact — docs/enterprise/V0_LOCKDOWN_E2E_TEST_GUIDE.md (~400 lines): - Prerequisites + reset+reseed commands - Test credentials per role + per funding path - 7-path walk (all reachable funding × program combos) - Coming Soon surface inventory (8 surfaces + audit-log demand signal) - DPDP §7 verification flow - OverageEvent + circuit-breaker verification - Booking-org-stamping verification (#19 expected matrix) - Schema lockdown verification (grep contract: each drop landed) - Parked-items list with rationale per follow-up tsc clean across the entire codebase including seeds. prisma validate clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add LedgerAccount / LedgerTransaction / LedgerEntry + postLedgerTxn (Sigma debit == Sigma credit, idempotent on idempotencyKey, balances derived via ledgerBalancePaise). WalletTopUp replaces the per-row WalletEntry; revenue splits move to integer basis points. Removes the three single-entry logs (FundingLedgerEntry / WalletEntry / SettlementLedgerEntry + SettlementKind). Includes scripts/smoke/ledger-smoke.ts. Part of #772 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Every cash event now posts one balanced LedgerTransaction: booking (Dr funding legs / Cr PLATFORM_FEE + CONSULTANT_PAYABLE + ORG_PAYABLE + GST_PAYABLE), top-up (Dr CASH / Cr WALLET), invoice paid (Dr CASH / Cr ORG_RECEIVABLE), payouts (Dr *_PAYABLE / Cr CASH + TDS_PAYABLE), refunds. Wallet routed-settlement (WalletTopUp + cache); GST/TDS compliance; org billing/payout/analytics API routes follow the journal + bps. Part of #772 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Seed writes journal rows (top-ups + booking debits + usage ledger); reconcile-ledgers checks the 7 invariants over the double-entry journal + derived caches (WALLET_BALANCE_DRIFT, LEDGER_TXN_IMBALANCE, EARNINGS_LEDGER_DRIFT, PROGRAM_ASSIGNMENT_ENGAGEMENTS_DRIFT, ACTIVE_SEAT_COUNT_DRIFT, PAYMENT_LEG_SUM_MISMATCH, ORG_PAYOUT_TOTAL_MISMATCH). Collaborators move to revenueShareBps; invoice/overage jobs + cleanup/earnings scripts follow. Full reseed reconciles ok:true, 0 findings. Closes #772 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…y cutover Full renumber into sectioned bands (00-05 foundations, 06-14 money/ledger, 15-19 IAM, 20-25 programs, 30-35 compliance/integrations, 40-44 ops, 50-52 scenarios) + README index. New money band (money model, chart of accounts, postings, wallet, booking->earnings, payouts, invoicing, payment legs, integrity) grounded in lib/payments/ledger + reconcile. Salvaged SSO testing/error-codes -> 15, money vocabulary -> 06, capability x funding matrix -> 02, clustered schema -> 00. Removed audits/history/playbooks/reference; refreshed complete-guide. 50-scenarios: full permutation matrix + 10 worked examples (Wipro, IIT Madras, LearnPro, consulting firm, etc.). All links resolve; 52 mermaid diagrams; zero stale model tokens. Part of #771 Part of #772 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lockdown #17 — programs POST now rejects unreachable (capability x fundingSource x programType) combos via isReachableOrgFundingPath (code UNREACHABLE_FUNDING_PATH), subsuming the hard-coded BOGUS_LICENSE_CREDIT_POOL guard; the create-program dialog disables unreachable program types. Lockdown #23 — prune COMING_SOON_FEATURES to the one genuinely-pending surface (overage charging, #775); SCIM/DPDP-erasure/TDS/Form-26Q already shipped, HRIS dropped; wire ComingSoonBadge on the CHARGE_MEMBER overage control. Part of #768 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lockdown #14/#15 — enforce maxOveragePerCyclePaise by wiring the (previously unused) computeOverage(): sum the cycle's OverageEvent.marginalPaise and fall back to BLOCK (402) once the ceiling is hit; stamp OverageEvent.invoiceLineItemId on settled events in the rollup. Event-only — instant CHARGE_MEMBER charging stays parked (#775/#715). Lockdown #22 — ORG_PROGRAM_CAP_NEAR Novu workflow + notifyOrgProgramCapNear, fired once per cycle on the transition into >=80% of cap, beside the existing 100% exhausted alert. Part of #768 Part of #715 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Schema-freeze batch (PR-3 of 4): TZID columns, engagement rename, overlap-guard column, refund-policy snapshot
…ectness Cancel/reschedule correctness (PR-4 of 4): policy-snapshot refunds, CAS guards, orphan re-drive
The sharp catch (on #843): CREDIT_POOL is genuinely PRICE-metered — recordBookingUtilization spends consumedPaise against the field × 100 (1 credit = ₹1) — so the engagementsPerCycle rename was the wrong direction. Re-renamed to creditBudgetPerCycle with the unit documented; LICENSED_SEAT counts engagements, CREDIT_POOL spends this budget. Also from review: localStartDay/localEndDay columns on weekly availability (the offset rolls the LOCAL day across midnight relative to the UTC day — local-day queries need their own columns; freeze-gated), the shared toLocalMinutes/toLocalDay helper replacing duplicated modular math at both write sites, SlotAllocationService now throws instead of silently nulling the #440 overlap-guard column when a consultant profile can't resolve, the cancel route orders slots by startsAt so the EARLIEST slot decides the refund tier, confirmApprovalStatus re-reads the fresh status before logging CAPTURE_AFTER_TERMINAL_STATE (the pre-read raced the very transition that made the CAS miss), and the sessions→engagements terminology stragglers in the overage calculator + test comments. Gates: tsc clean, 1309/1309 jest, next build green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Enterprise foundation — org CRUD, billing modes, SSO, dashboard
…peer-dep fix(mcp): resolve supabase MCP -32000 crash from missing peer deps
…ds + #677 PM-1 closeout lib/booking/transitions.ts is the consumer-side sibling of the enterprise CAS module: allowed-from maps keyed by target state, baked into updateMany WHERE clauses. Rewires the consultation/subscription PATCH approval flows (item + collection routes), SlotAllocationService.updateEventStatus (with an ALLOCATION_APPROVABLE_FROM self-edge so re-allocation keeps working), the webhook tentative-expiry path, and the auto-complete sweep. Removes requestStatus from both PUT surfaces; reschedule slot flips and webinar/class transitions use explicit allowed-from sets. Also finishes #677 PM-1: the three GH-Actions wrappers' credential warning gates now match the canonical RAZORPAY_SECRET the scripts read, and .env.sample documents the RazorpayX + webhook secrets. lib/validation/limits.ts and the eventMutationLimiter land here because the routes this PR touches consume them; the remaining #831 coverage rides the next PR in the train. Closes #836 Part of #837 Part of #677 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…nds and limiters Checkout locks get per-type TTLs (CLASS 300s — N sessions × M slot writes plus a gateway round-trip outlived the flat 60s) and one checked extendLock renewal before the gateway call that aborts 409 on ownership loss; a timer heartbeat was rejected because serverless freeze makes intervals unreliable. Tentative holds expire after 24 hours instead of 7 days. Every waitlist mutation (decline/skip/expire/leave/mark-booked) is CAS-guarded so double-submits cannot double-fire handleSlotOpening or resurrect terminal entries. For #831: shared .max() bounds on every user-typed string (report route gains a zod schema — it destructured raw unbounded fields), 64KB body caps, the eventMutationLimiter applied to the event POST/PATCH and validate/allocate routes that had zero limiter coverage, and the trial limiter now applies uniformly instead of exempting privileged roles. Closes #831 Closes #832 Closes #833 Part of #834 (code half; the join-table uniqueness rides the schema PR) Part of #837 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…constraint sidecar getTimezoneOffsetMinutes now reads Intl's machine-formatted longOffset token, exact to the instant at DST boundaries; the old toLocaleString string-parsing heuristic drifted around transitions, and date-fns-tz's getTimezoneOffset was probed and rejected because it resolves at calendar-day granularity on transition days. Overnight-slot status had four divergent implementations across formatting, validation, and overlap code; utils/schedule/overnight.ts is now the single rule and everything else delegates. 28 table-driven tests pin IST, both 2026 US DST boundaries, the +14:00 extreme, and the overnight truth table. The #676 A1-A4 CHECK constraints ride a SQL sidecar (prisma/sql/check-constraints.sql + npm run db:constraints) because Prisma 7 has no @@check; they apply at the pre-MVP reset, never against the shared dev DB mid-cycle. Payment amounts use >= 0 because credit-covered and org-sponsored checkouts legitimately write zero. A schema doc-comment marks the SlotOfAppointment↔User join uniqueness as load-bearing for #834. Part of #503 (items 1-2) Part of #676 Part of #834 (schema half) Part of #837 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…udit refresh The race-condition suite gains a real-HTTP harness (BetterAuth session login, concurrent fire, status histograms) and seven scenarios across two new categories: cancel-vs-reschedule, reschedule storm, approve-vs-decline (#836), multi-device login, org onboarding double-submit, webhook bulk replay, and webhook out-of-order delivery. npm run test:chaos:api runs them against a seeded DB + running server; they move no money and clean up after themselves. A local run demonstrated the owed db push: scenarios 9/10/11/13 fail on the shared dev DB solely because Appointment.cancellationPolicySnapshot and organizations.version exist in schema but not in the database; 12/15/16 pass. The chaos runbook records scenarios 9-16 and the updated go/no-go set; the readiness audit gains a dated B2C addendum. ADR 14 records the queue verdict: no Kafka/BullMQ/SQS for launch (all need long-lived consumers Netlify cannot host); GH-Actions crons + after() + sweeper re-drives + Upstash Redis stay; Upstash QStash is the pre-approved escalation behind two named telemetry triggers; the real ceilings are Netlify's 125-concurrent default and Supavisor tier caps. Part of #837 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The owed push landed (data-preserving CreditPoolConfig rename, ledger triggers re-applied); scenarios 9/10/11/13 went green on re-run, joining 12/15/16. Push-before-deploy noted as the production ordering lesson. Part of #837 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…io-9 invariant Review catches on #848. The cancel-vs-reschedule and reschedule-storm scenarios now capture and restore the consultation status and slot states so repeat runs do not consume the seed pool. The org double-submit cleanup also deletes the BillingAccount the creation tx mints — org children cascade, but the org holds that FK, so deleting the org orphaned it (verified: one orphan from the first runs, now swept). Hardening repeat-runs exposed an imprecise assertion: reschedule-first ordering legally admits BOTH actions (reschedule resets to PENDING, and PENDING is cancellable), so "exactly one winner" flaked by interleaving. The test and runbook now state the real invariants — no 5xx, never zero winners, a successful cancel is never resurrected, slots never mix CANCELLED+RESCHEDULED. Verified stable across four consecutive runs. Part of #837 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
fix: B2C CAS transition map — approval/decline/expiry/completion guards + #677 PM-1 closeout
fix: checkout lock budgets + tentative TTL + waitlist CAS + input bounds and limiters
…a-checks fix: exact timezone offsets + canonical overnight resolution + CHECK-constraint sidecar
…-adr test+docs: real-API chaos categories 07/09 + ADR 14 async posture + audit refresh
Rows from models with #780/#781 result extensions are Proxy-backed and carry Symbol(nodejs.util.inspect.custom); spreading or returning them from server components made React's serializer reject the payload ("Only plain objects can be passed to Client Components" — seen on the home page's FeaturedExpertsLoader). structuredClone cannot clone Proxies, so lib/data/serialize.ts walks rows into genuinely plain objects (Dates preserved) and every lib/data return that reaches a client component now passes through it: home, explore-programs, plan-details, and both org-member loaders. explore-experts already mapped fields explicitly and consultant-detail already JSON-roundtrips; both left as-is. Verified: home, /explore/programs, /explore/experts, and both plan-detail pages render with zero serialization errors in the server log; tsc clean; 1337/1337 tests pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Race Condition Tests workflow runs `npm run test:race` with no app server, and the master-runner auto-discovers the new 07/09 categories — which failed CI on the #848 merge push. Every real-API scenario now calls ensureServerOrSkip() first and exits 0 with a SKIP line when the target is unreachable (same semantics as the missing-fixture SKIPs); the pre-launch staging run sets CHAOS_BASE_URL and runs for real. Verified both paths: live server → scenarios pass; unreachable target → clean SKIP. Part of #837 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#476 (merged to dev via #655) put a Redis-backed cron lock on every job entry, so lib/redis now loads — and throws — at import time in any workflow that does not provide UPSTASH_REDIS_REST_URL/TOKEN. Only some workflow files were updated then; the other 30 began dying one by one as their schedules fired post-#655 (Send Waitlist Reminders and Reconcile Slot Availability were the first observed). This mirrors the env block reconcile-payment-status.yml already carries into every job workflow that lacked it. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…chaos-ci-skip fix: RSC boundary serialization + chaos CI skip guard
…ons) Closes the two implementable gaps from CONCURRENCY_AND_LOAD_REPORT.md: The report's Action 1 advice (connection_limit=1) predates the pg driver adapter — with @prisma/adapter-pg the relevant knob is pg.Pool's max, which defaults to 10 clients PER function instance (125 concurrent Netlify invocations x 10 would dwarf Supavisor's client cap). PG_POOL_MAX makes it configurable; unset keeps the pg default for long-lived local dev/jobs; serverless deploys set 1-2. Verified live with PG_POOL_MAX=1. DATABASE_URL itself was confirmed transaction-mode (port 6543, pgbouncer=true). load-tests/smoke.js targets the three real read hot paths (health, slot availability, consultant search) with BetterAuth login in setup and the report's thresholds; the report's draft referenced endpoints that do not exist and its booking-race k6 script duplicated what the race-condition suite already covers properly, so both were adapted out. The workflow is workflow_dispatch-only against an operator-supplied staging URL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…knob chore: k6 smoke test + serverless pg pool-size knob
✅ Deploy Preview for familiarise ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces extensive updates to enterprise billing, compliance, and multi-tenant features, including new documentation, configuration updates, and a comprehensive suite of regression tests for GST splits, TDS calculations, SCIM authentication, and anti-lockout gates. It also integrates DPDP Act consent checks into Stream chat user upserts and adds organization tagging to Stream channels and meetings. The code review feedback highlights several improvement opportunities: replacing the platform-dependent shell grep helper in the audit actions test with a native Node.js implementation, restoring overridden global.fetch and process.env values in test teardowns to prevent test pollution, and optimizing the N+1 query pattern in the batch Stream user upsert by batch-fetching consent artifacts.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
|
|
||
| import { execSync } from "child_process"; | ||
| import path from "path"; | ||
|
|
||
| const REPO_ROOT = path.resolve(__dirname, "..", ".."); | ||
|
|
||
| function rg(pattern: string, paths: string[]): string { | ||
| // Use grep -rEn so we get file:line:body and ERE alternation. | ||
| // The -h flag is omitted intentionally — we want filenames in output. | ||
| try { | ||
| return execSync( | ||
| `grep -rEn ${JSON.stringify(pattern)} ${paths.map((p) => JSON.stringify(p)).join(" ")}`, | ||
| { cwd: REPO_ROOT, encoding: "utf8" }, | ||
| ); | ||
| } catch (err) { | ||
| // grep returns non-zero on no-matches, which is the success case here. | ||
| const status = (err as { status?: number }).status; | ||
| if (status === 1) return ""; | ||
| throw err; | ||
| } | ||
| } |
There was a problem hiding this comment.
The rg function uses execSync to run grep, which is platform-dependent and will fail on Windows environments where grep is not available in the shell. To ensure local tests can run reliably across all developer platforms (macOS, Linux, Windows), consider implementing a native Node.js recursive file search using fs.readdirSync and fs.readFileSync with a regular expression.
function rg(pattern: string, paths: string[]): string {
const fs = require("fs");
const regexStr = pattern.replace("[[:space:]]*", "\\s*");
const regex = new RegExp(regexStr);
const matches: string[] = [];
function search(dir: string) {
const files = fs.readdirSync(dir);
for (const file of files) {
const fullPath = path.join(dir, file);
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
search(fullPath);
} else if (stat.isFile() && /\.(ts|tsx|js|jsx)$/.test(file)) {
const content = fs.readFileSync(fullPath, "utf8");
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
if (regex.test(lines[i])) {
matches.push(path.relative(REPO_ROOT, fullPath) + ":" + (i + 1) + ":" + lines[i]);
}
}
}
}
}
for (const p of paths) {
const fullPath = path.resolve(REPO_ROOT, p);
if (fs.existsSync(fullPath)) {
const stat = fs.statSync(fullPath);
if (stat.isDirectory()) {
search(fullPath);
} else {
const content = fs.readFileSync(fullPath, "utf8");
const lines = content.split("\n");
for (let i = 0; i < lines.length; i++) {
if (regex.test(lines[i])) {
matches.push(path.relative(REPO_ROOT, fullPath) + ":" + (i + 1) + ":" + lines[i]);
}
}
}
}
}
return matches.join("\n");
}| afterEach(() => { | ||
| process.env = OLD_ENV; | ||
| jest.restoreAllMocks(); | ||
| }); |
There was a problem hiding this comment.
Manually overriding global.fetch without restoring it can leak mock behavior to other test suites running in the same process, leading to flaky tests or unexpected failures. Consider saving the original global.fetch and restoring it in the afterEach block.
const ORIGINAL_FETCH = global.fetch;
afterEach(() => {
process.env = OLD_ENV;
global.fetch = ORIGINAL_FETCH;
jest.restoreAllMocks();
});| beforeEach(() => { | ||
| jest.clearAllMocks(); | ||
| process.env.RAZORPAY_KEY_ID = "k"; | ||
| process.env.RAZORPAY_KEY_SECRET = "s"; | ||
| process.env.RAZORPAYX_ACCOUNT_NUMBER = "acc"; | ||
| (global as unknown as { fetch: unknown }).fetch = jest | ||
| .fn() | ||
| .mockResolvedValue({ ok: true, json: async () => ({ id: "pout_x" }) }); | ||
| cp.updateMany.mockResolvedValue({ count: 1 }); // claim APPROVED→PROCESSING | ||
| ce.updateMany.mockResolvedValue({ count: 0 }); | ||
| }); |
There was a problem hiding this comment.
Modifying process.env and overriding global.fetch without restoring them in an afterEach or afterAll block can pollute other test suites in the same test run. Consider saving their original values and restoring them after each test.
const ORIGINAL_FETCH = global.fetch;
const ORIGINAL_ENV = { ...process.env };
beforeEach(() => {
jest.clearAllMocks();
process.env.RAZORPAY_KEY_ID = "k";
process.env.RAZORPAY_KEY_SECRET = "s";
process.env.RAZORPAYX_ACCOUNT_NUMBER = "acc";
(global as unknown as { fetch: unknown }).fetch = jest
.fn()
.mockResolvedValue({ ok: true, json: async () => ({ id: "pout_x" }) });
cp.updateMany.mockResolvedValue({ count: 1 }); // claim APPROVED→PROCESSING
ce.updateMany.mockResolvedValue({ count: 0 });
});
afterEach(() => {
global.fetch = ORIGINAL_FETCH;
process.env = ORIGINAL_ENV;
});| // consenters only. The signup auth hook stamps consent at account | ||
| // creation so this should only filter when a user explicitly | ||
| // withdraws via the in-app /consent route. | ||
| const consentResults = await Promise.all( | ||
| users.map(async (u) => ({ | ||
| user: u, | ||
| hasConsent: await checkConsent({ | ||
| userId: u.id, | ||
| purposeCode: "STREAM_DATA_PROCESSING", |
There was a problem hiding this comment.
Executing checkConsent individually for each user in a loop using Promise.all results in an N+1 query pattern. If the batch size of users is large, this will cause significant database load and increased latency. Consider optimizing this by batch-fetching the active consent artifacts for all user IDs in a single query.

Production release following the #653 precedent. 526 commits, the bulk being:
toPlain), chaos CI skip guard, Upstash secrets for all 30 job workflows that were crashing post-Enterprise foundation — org CRUD, billing modes, SSO, dashboard #655.PG_POOL_MAXserverless pool knob.Pre-flight verified: production's
DATABASE_URLresolves to the same Supabase project (postgres.pzmbxqdgibfkhjwzeprf) that received today's schema push + ledger triggers, so deploy-after-push ordering holds.PG_POOL_MAX=1set in the Netlify production context alongside this release.Dev at merge time: required checks green, 1337/1337 Jest, race-condition workflow green, all seven implemented chaos scenarios passing, three previously failing crons re-dispatched green.
🤖 Generated with Claude Code