Skip to content

Enterprise remaining features — consolidated tracker #661

@teetangh

Description

@teetangh

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:
    1. Query all OrganizationProfiles where billingMode = "INVOICED_MONTHLY" and status = "ACTIVE"
    2. 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
    3. 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.tsNew
  • 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:
      1. Create a credit note row linked to the original invoice
      2. 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.tsNew
  • 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

  • POST /api/organizations with kind = PROVIDER succeeds (currently 501)
  • POST /api/organizations/[orgId]/members with role = ORG_CONSULTANT succeeds (currently 501)
  • /api/organizations/[orgId]/payouts returns real data (currently 501)
  • /api/organizations/[orgId]/payout-account CRUD works (currently 501)
  • /api/organizations/[orgId]/consultants returns ORG_CONSULTANT members (currently 501)
  • Dashboard: consultants + payouts pages render real data

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

  • Set up a test Okta/Azure AD instance
  • Register a SAML provider via the SSO admin UI
  • Verify domain-check returns the correct ssoEndpoint + ssoBody
  • Verify the signin page redirects to the IdP
  • Verify callback creates User + Member + OrganizationMemberProfile
  • Verify customSession membership sync runs correctly
  • Verify requireOrgAccess passes for the SSO-provisioned user
  • Test OIDC flow with the same checklist

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    EnterpriseEnterprise tier — B2B org features, Architecture 4enhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions