Enterprise Remaining Features
Consolidated tracker for all deferred enterprise work from PR #655 (feature/enterprise). The PR ships the core enterprise foundation (schema, APIs, dashboard, ORG_ADMIN onboarding, 3 billing modes, SSO, checkout integration, refund routing). These items are follow-up work that builds on that foundation.
Replaces closed issues: #656, #657, #658, #659, #660
1. Inngest Monthly Invoice Generation Cron
Priority: High
Complexity: Low (the logic already exists)
The manual POST /api/organizations/[orgId]/billing/generate-invoice endpoint works end-to-end: filters SUCCEEDED + ORG_INVOICED payments, nets out refunds, creates an OrganizationInvoice. This task wraps it in an automated Inngest scheduled function.
Implementation plan
- New file:
lib/inngest/functions/generate-org-invoices.ts
- Schedule: Runs on the 1st of each month at 00:00 UTC
- Logic:
- Query all OrganizationProfiles where
billingMode = "INVOICED_MONTHLY" and status = "ACTIVE"
- For each org, call the same aggregation logic from
generate-invoice/route.ts:
- Find unbilled SUCCEEDED ORG_INVOICED payments
- Net out refunds per payment
- Skip orgs with no billable charges
- Create OrganizationInvoice with status SENT, dueDate = paymentTermsDays from now
- Fire-and-forget email to
org.billingEmail with the invoice summary
- Wire into:
app/api/inngest/route.ts (register the function)
- Testing: Manual trigger via Inngest dashboard or
POST /api/inngest/send
Files
lib/inngest/functions/generate-org-invoices.ts — New
app/api/inngest/route.ts — Register the function
- Extract shared logic from
app/api/organizations/[orgId]/billing/generate-invoice/route.ts into lib/payments/operations/org-invoicing.ts
2. Post-Invoice Refund Credit-Note System
Priority: Medium
Complexity: Medium
When an ORG_INVOICED payment is refunded AFTER it was already rolled into a PAID invoice, the current code only unbills the payment (billableToOrgInvoiceId = null) but does NOT update the OrganizationInvoice.amount or items. The invoice totals drift from reality.
Implementation plan
- New model:
OrganizationCreditNote (or extend OrganizationInvoice with a creditNoteForId FK)
- Fields:
originalInvoiceId, amount, reason, items (JSON), status, createdAt
- Refund handler update (
app/api/payments/refunds/route.ts):
- When refunding an ORG_INVOICED payment that is attached to a PAID invoice:
- Create a credit note row linked to the original invoice
- Optionally adjust the original invoice's
amount field (or leave it immutable and let the credit note be a separate ledger entry)
- When attached to an unpaid invoice: continue current behavior (unbill)
- Dashboard update (
billing/page.tsx):
- Show credit notes in the invoices table (with a "Credit Note" badge)
- Net credit notes into the outstanding balance calculation
- Billing summary + credit-limit enforcement: subtract credit note amounts from exposure
Files
prisma/schema.prisma — Add OrganizationCreditNote model (or extend existing)
app/api/payments/refunds/route.ts — ORG_INVOICED refund handler
app/api/organizations/[orgId]/billing/route.ts — Summary calculation
app/dashboard/organization/[orgId]/billing/page.tsx — UI
3. Wizard Cleanup for Abandoned Org Creation
Priority: Low
Complexity: Low
The org setup wizard creates the Organization on step 1 "Next" (needed for file uploads in step 3). Abandoned wizards leave partially-configured orgs + uploaded assets.
Implementation plan
Option A (recommended): Cleanup cron
- Inngest scheduled function runs daily
- Finds Organizations created > 24h ago with OrganizationProfile fields still at defaults (no members beyond owner, no plans, no payments)
- Soft-deletes by setting
status = DEACTIVATED
- Deletes uploaded assets from Supabase
organizations/ bucket
Option B: Draft status
- Add
DRAFT to OrganizationStatus enum
- Wizard creates org with
status = DRAFT
- Review step's "Launch" flips to
ACTIVE
requireOrgAccess skips DRAFT orgs
- Dashboard listing excludes DRAFT
- Same cleanup cron for stale DRAFTs
Files
lib/inngest/functions/cleanup-abandoned-orgs.ts — New
- OR:
prisma/schema.prisma + app/dashboard/organization/create/page.tsx (Option B)
4. CSV Bulk Invite Parser
Priority: Low
Complexity: Low
The org invite flow currently accepts comma/newline-separated emails. A CSV parser would support bulk onboarding from HR systems.
Implementation plan
- UI: Add a "Upload CSV" button next to the textarea in:
app/dashboard/organization/[orgId]/invitations/page.tsx
app/dashboard/organization/create/components/InviteTeamStep.tsx
app/form/onboarding/components/OrgAdminOrgSetupStep.tsx
- Parser: Client-side CSV parsing (first row = headers, expect "email" column, optional "name" + "role")
- Validation: Same per-email zod validation + dedup
- No backend changes needed — the existing
POST /api/organizations/[orgId]/invitations endpoint handles one invite at a time; the client fires them in parallel via Promise.allSettled
Files
- New component:
components/shared/CsvEmailUploader.tsx
- Wire into the 3 pages above
5. PROVIDER Org Implementation — Flip ENABLE_PROVIDER_ORGS
Priority: Deferred (first paying PROVIDER customer)
Complexity: High
The schema, role enum (ORG_CONSULTANT, ORG_SUPPORT), and code paths all exist but are gated by ENABLE_PROVIDER_ORGS (default false). When flipped:
Verification checklist
3-way earnings split implementation
lib/payments/payouts/earnings-service.ts — add PROVIDER branch:
- When
Payment.organizationProfileId is set AND org.kind = PROVIDER:
- Read
platformCommissionRate, orgRetainRate, consultantPayoutRate from OrganizationProfile
- Create
ConsultantEarnings (consultant's share)
- Create
OrganizationEarnings (org's share)
- Platform fee = payment.amount * platformCommissionRate
- This branch is currently dead code because PROVIDER orgs can't be created
Payout batch for orgs
- Extend
lib/payments/payouts/payout-service.ts to handle OrganizationPayout creation
- Or create a new
lib/payments/payouts/org-payout-service.ts
Files
lib/feature-flags.ts — flip flag
lib/payments/payouts/earnings-service.ts — 3-way split
lib/payments/payouts/payout-service.ts or new org variant
- All currently-501-gated route handlers
- E2E test prompt update
6. Invoice PDF Rendering
Priority: Medium
Complexity: Medium
Related: Issue #438
OrganizationInvoice.pdfUrl is currently always null. Need a PDF generation service for downloadable invoices.
Implementation plan
- Use
@react-pdf/renderer (already in package.json)
- New component:
lib/pdf/OrgInvoicePdf.tsx — React PDF template with:
- Org branding (logo, name, address)
- Invoice number, date, due date
- Line items table
- Tax breakdown (CGST/SGST if India)
- Total amount
- Payment status
- API route:
GET /api/organizations/[orgId]/billing/invoices/[invoiceId]/pdf
- Renders the PDF on demand
- Optionally caches to Supabase and sets
pdfUrl
- Dashboard: "Download PDF" button on each invoice row
7. SSO End-to-End Verification with Real IdP
Priority: High (for enterprise customers with SSO requirements)
Complexity: Medium
PR #655 wired SSO config normalization, membership sync, and the correct BetterAuth endpoint. But it has NOT been tested against a real IdP (Okta, Azure AD, Google Workspace).
Verification plan
Dependencies
#1 (Inngest cron) → no deps, can start immediately
#2 (Credit notes) → needs schema migration
#3 (Wizard cleanup) → needs #1 (Inngest) or standalone cron
#4 (CSV parser) → no deps
#5 (PROVIDER) → needs first PROVIDER customer or explicit decision to enable
#6 (Invoice PDF) → no deps, relates to Issue #438
#7 (SSO verification) → needs test IdP access
References
Enterprise Remaining Features
Consolidated tracker for all deferred enterprise work from PR #655 (
feature/enterprise). The PR ships the core enterprise foundation (schema, APIs, dashboard, ORG_ADMIN onboarding, 3 billing modes, SSO, checkout integration, refund routing). These items are follow-up work that builds on that foundation.Replaces closed issues: #656, #657, #658, #659, #660
1. Inngest Monthly Invoice Generation Cron
Priority: High
Complexity: Low (the logic already exists)
The manual
POST /api/organizations/[orgId]/billing/generate-invoiceendpoint works end-to-end: filters SUCCEEDED + ORG_INVOICED payments, nets out refunds, creates an OrganizationInvoice. This task wraps it in an automated Inngest scheduled function.Implementation plan
lib/inngest/functions/generate-org-invoices.tsbillingMode = "INVOICED_MONTHLY"andstatus = "ACTIVE"generate-invoice/route.ts:org.billingEmailwith the invoice summaryapp/api/inngest/route.ts(register the function)POST /api/inngest/sendFiles
lib/inngest/functions/generate-org-invoices.ts— Newapp/api/inngest/route.ts— Register the functionapp/api/organizations/[orgId]/billing/generate-invoice/route.tsintolib/payments/operations/org-invoicing.ts2. Post-Invoice Refund Credit-Note System
Priority: Medium
Complexity: Medium
When an ORG_INVOICED payment is refunded AFTER it was already rolled into a PAID invoice, the current code only unbills the payment (
billableToOrgInvoiceId = null) but does NOT update theOrganizationInvoice.amountoritems. The invoice totals drift from reality.Implementation plan
OrganizationCreditNote(or extend OrganizationInvoice with acreditNoteForIdFK)originalInvoiceId,amount,reason,items(JSON),status,createdAtapp/api/payments/refunds/route.ts):amountfield (or leave it immutable and let the credit note be a separate ledger entry)billing/page.tsx):Files
prisma/schema.prisma— Add OrganizationCreditNote model (or extend existing)app/api/payments/refunds/route.ts— ORG_INVOICED refund handlerapp/api/organizations/[orgId]/billing/route.ts— Summary calculationapp/dashboard/organization/[orgId]/billing/page.tsx— UI3. Wizard Cleanup for Abandoned Org Creation
Priority: Low
Complexity: Low
The org setup wizard creates the Organization on step 1 "Next" (needed for file uploads in step 3). Abandoned wizards leave partially-configured orgs + uploaded assets.
Implementation plan
Option A (recommended): Cleanup cron
status = DEACTIVATEDorganizations/bucketOption B: Draft status
DRAFTtoOrganizationStatusenumstatus = DRAFTACTIVErequireOrgAccessskips DRAFT orgsFiles
lib/inngest/functions/cleanup-abandoned-orgs.ts— Newprisma/schema.prisma+app/dashboard/organization/create/page.tsx(Option B)4. CSV Bulk Invite Parser
Priority: Low
Complexity: Low
The org invite flow currently accepts comma/newline-separated emails. A CSV parser would support bulk onboarding from HR systems.
Implementation plan
app/dashboard/organization/[orgId]/invitations/page.tsxapp/dashboard/organization/create/components/InviteTeamStep.tsxapp/form/onboarding/components/OrgAdminOrgSetupStep.tsxPOST /api/organizations/[orgId]/invitationsendpoint handles one invite at a time; the client fires them in parallel viaPromise.allSettledFiles
components/shared/CsvEmailUploader.tsx5. PROVIDER Org Implementation — Flip ENABLE_PROVIDER_ORGS
Priority: Deferred (first paying PROVIDER customer)
Complexity: High
The schema, role enum (
ORG_CONSULTANT,ORG_SUPPORT), and code paths all exist but are gated byENABLE_PROVIDER_ORGS(defaultfalse). When flipped:Verification checklist
POST /api/organizationswithkind = PROVIDERsucceeds (currently 501)POST /api/organizations/[orgId]/memberswithrole = ORG_CONSULTANTsucceeds (currently 501)/api/organizations/[orgId]/payoutsreturns real data (currently 501)/api/organizations/[orgId]/payout-accountCRUD works (currently 501)/api/organizations/[orgId]/consultantsreturns ORG_CONSULTANT members (currently 501)3-way earnings split implementation
lib/payments/payouts/earnings-service.ts— add PROVIDER branch:Payment.organizationProfileIdis set ANDorg.kind = PROVIDER:platformCommissionRate,orgRetainRate,consultantPayoutRatefrom OrganizationProfileConsultantEarnings(consultant's share)OrganizationEarnings(org's share)Payout batch for orgs
lib/payments/payouts/payout-service.tsto handleOrganizationPayoutcreationlib/payments/payouts/org-payout-service.tsFiles
lib/feature-flags.ts— flip flaglib/payments/payouts/earnings-service.ts— 3-way splitlib/payments/payouts/payout-service.tsor new org variant6. Invoice PDF Rendering
Priority: Medium
Complexity: Medium
Related: Issue #438
OrganizationInvoice.pdfUrlis currently alwaysnull. Need a PDF generation service for downloadable invoices.Implementation plan
@react-pdf/renderer(already in package.json)lib/pdf/OrgInvoicePdf.tsx— React PDF template with:GET /api/organizations/[orgId]/billing/invoices/[invoiceId]/pdfpdfUrl7. SSO End-to-End Verification with Real IdP
Priority: High (for enterprise customers with SSO requirements)
Complexity: Medium
PR #655 wired SSO config normalization, membership sync, and the correct BetterAuth endpoint. But it has NOT been tested against a real IdP (Okta, Azure AD, Google Workspace).
Verification plan
ssoEndpoint+ssoBodycustomSessionmembership sync runs correctlyrequireOrgAccesspasses for the SSO-provisioned userDependencies
References
feature/enterprise) — the foundation this builds ondocs/enterprise/decisions/2026-04-10-pr2-enterprise-foundation.md— ADRprompts/enterprise-tests/e2e-enterprise-agent-001-org-lifecycle.md— E2E test prompt