PROVIDER Organizations — Technical Implementation Plan
What is a PROVIDER org?
A PROVIDER organization is a consultant agency that hosts multiple consultants under its umbrella. Unlike BUYER orgs (schools/corporates that consume sessions), PROVIDER orgs supply services — the org recruits consultants, manages their catalog, and captures a slice of every booking via a 3-way revenue split:
Payment (100%)
├── Platform fee (10% default, configurable per org)
├── Org retain (5% default, configurable per org)
└── Consultant payout (85% default, configurable per org)
└── Can be overridden per-consultant via customConsultantPayoutRate
Example: A ₹10,000 consultation booked through a PROVIDER org consultant:
- Platform: ₹1,000 (10%)
- Org: ₹500 (5%)
- Consultant: ₹8,500 (85%)
Current state
PR #655 (feature/enterprise) shipped the complete PROVIDER foundation:
Already built (gated behind ENABLE_PROVIDER_ORGS=false)
Schema — all models exist in prisma/schema.prisma:
OrganizationProfile.kind supports PROVIDER and HYBRID values
OrganizationProfile has platformCommissionRate (0.10), orgRetainRate (0.05), consultantPayoutRate (0.85), autoApproveConsultants
OrganizationMemberProfile.customConsultantPayoutRate — per-consultant rate override
OrganizationPayoutAccount — org's bank account for receiving payouts
OrganizationPayout — payout history (parallels ConsultantPayout)
OrganizationEarnings — org's share per payment (parallels ConsultantEarnings)
OrgMemberRole includes ORG_CONSULTANT (rank 40) and ORG_SUPPORT (rank 30)
API routes — all return 501 when flag is off:
POST /api/organizations — rejects kind=PROVIDER or kind=HYBRID
POST /api/organizations/[orgId]/members — rejects role=ORG_CONSULTANT or ORG_SUPPORT
PATCH /api/organizations/[orgId]/members/[memberId] — rejects role changes to ORG_CONSULTANT/ORG_SUPPORT
POST /api/organizations/[orgId]/invitations — rejects ORG_CONSULTANT/ORG_SUPPORT invitation roles
POST /api/organizations/invitations/accept — rejects acceptance of gated roles
GET /api/organizations/[orgId]/consultants — lists ORG_CONSULTANT members
GET/POST /api/organizations/[orgId]/payouts — org payout list + batch creation
GET/PUT/DELETE /api/organizations/[orgId]/payout-account — bank account CRUD
Dashboard pages:
/dashboard/organization/[orgId]/consultants/page.tsx — renders "Provider tier required" lock card when 501
/dashboard/organization/[orgId]/payouts/page.tsx — same lock card
- Sidebar conditionally shows Consultants + Payouts nav items for PROVIDER/HYBRID orgs
Role gating:
PROVIDER_GATED_ROLES = ["ORG_CONSULTANT", "ORG_SUPPORT"] checked in 4 route files
- Role hierarchy already ranks ORG_CONSULTANT (40) and ORG_SUPPORT (30) between ORG_MANAGER (60) and ORG_LEARNER (20)
NOT yet built
- 3-way earnings split —
lib/payments/payouts/earnings-service.ts has zero PROVIDER logic
- Org payout batch creation — POST payouts returns 501 even when flag is on
- Payout processing pipeline — no
OrganizationPayout row creation or gateway disbursement
- Payout account encryption — currently base64 stub, needs real encryption
- Seed data — no PROVIDER orgs, no ORG_CONSULTANT members seeded
- PROVIDER onboarding — no signup path for agency owners (ORG_ADMIN onboarding only creates BUYER)
- Consultant application/approval workflow —
autoApproveConsultants field exists but not enforced
- HYBRID org behavior — no explicit handling for orgs that are both BUYER and PROVIDER
Implementation plan
Phase 1: Flip the flag + verify existing 501-gated routes (1-2 hours)
Objective: Set ENABLE_PROVIDER_ORGS=true and verify all gated routes become functional.
Steps:
- Set
ENABLE_PROVIDER_ORGS=true in .env
- Test each currently-gated route:
POST /api/organizations with kind: "PROVIDER" → should succeed
POST /api/organizations/[orgId]/members with role: "ORG_CONSULTANT" → should succeed
POST /api/organizations/[orgId]/invitations with role: "ORG_CONSULTANT" → should succeed
POST /api/organizations/invitations/accept for ORG_CONSULTANT invitation → should succeed
GET /api/organizations/[orgId]/consultants → should return ORG_CONSULTANT members
GET /api/organizations/[orgId]/payouts → should return (empty) payouts list
GET/PUT/DELETE /api/organizations/[orgId]/payout-account → should CRUD
- Verify dashboard pages render real data instead of lock cards
- Verify sidebar shows Consultants + Payouts nav for PROVIDER orgs
- Test rate-sum validation: PATCH org with rates that don't sum to 1.0 → should 400
Files touched: .env only (no code changes for this phase)
Phase 2: 3-way earnings split (4-6 hours)
Objective: When a payment is made for a booking through a PROVIDER org consultant, create both ConsultantEarnings (consultant's share) AND OrganizationEarnings (org's share) with the correct split.
File: lib/payments/payouts/earnings-service.ts
Implementation:
In createEarningsFromPayment(), after the existing platform fee calculation:
// After calculating platformFee and before creating ConsultantEarnings:
// Check if this payment is tagged to a PROVIDER/HYBRID org
if (payment.organizationProfileId) {
const orgProfile = await prisma.organizationProfile.findUnique({
where: { id: payment.organizationProfileId },
select: { kind: true, platformCommissionRate: true, orgRetainRate: true, consultantPayoutRate: true },
});
if (orgProfile && (orgProfile.kind === "PROVIDER" || orgProfile.kind === "HYBRID")) {
// Check for per-consultant rate override
const memberProfile = await prisma.organizationMemberProfile.findFirst({
where: {
organizationProfileId: orgProfile.id,
consultantProfileId: consultantProfile.id,
role: "ORG_CONSULTANT",
status: "ACTIVE",
},
select: { customConsultantPayoutRate: true },
});
const effectiveConsultantRate = memberProfile?.customConsultantPayoutRate ?? orgProfile.consultantPayoutRate;
const effectiveOrgRate = orgProfile.orgRetainRate;
const effectivePlatformRate = orgProfile.platformCommissionRate;
// Calculate splits
const grossAmount = payment.originalAmount; // pre-discount amount
const platformFee = Math.round(grossAmount * effectivePlatformRate);
const orgShare = Math.round(grossAmount * effectiveOrgRate);
const consultantShare = grossAmount - platformFee - orgShare; // remainder to consultant
// Create OrganizationEarnings
await prisma.organizationEarnings.create({
data: {
organizationProfileId: orgProfile.id,
paymentId: payment.id,
grossAmount,
platformFee,
orgShare,
refundedAmount: 0,
status: "PENDING",
holdUntil: calculateHoldUntil(), // reuse existing hold logic
currency: payment.currency,
},
});
// Create ConsultantEarnings with the reduced consultant share
// (instead of the standard 80/20 split)
await prisma.consultantEarnings.create({
data: {
consultantProfileId: consultantProfile.id,
paymentId: payment.id,
grossAmount,
platformFee,
consultantShare,
refundedAmount: 0,
status: "PENDING",
holdUntil: calculateHoldUntil(),
currency: payment.currency,
},
});
return; // Skip the standard BUYER earnings path
}
}
// ... existing BUYER earnings logic (unchanged)
Edge cases:
- Collaborator splits (webinars/classes with multiple consultants): the org's share stays fixed; the consultant share is split among collaborators proportionally (same as existing collaborator split logic)
customConsultantPayoutRate overrides only the consultant's share; org + platform shares adjust accordingly (or: org share stays fixed, consultant+platform adjust — decide policy)
- When
ENABLE_PROVIDER_ORGS is false but org.kind === PROVIDER (shouldn't happen since creation is gated): fall through to BUYER path
Also update:
refundEarnings() — when refunding a payment with OrganizationEarnings, reverse the org earnings proportionally
- Webhook
handlePaymentSuccess() — no changes needed (it calls createEarningsFromPayment which handles the branching)
Phase 3: Org payout batch creation + processing (4-6 hours)
Objective: Allow org owners to create payout batches for accumulated org earnings, and process them via the existing payout pipeline.
New file: lib/payments/payouts/org-payout-service.ts
export async function createOrgPayoutBatch(organizationProfileId: string): Promise<string> {
// 1. Find all OrganizationEarnings where status = "READY" and holdUntil < now()
// 2. Sum the orgShare amounts
// 3. Create an OrganizationPayout row with the total
// 4. Update all included earnings to status = "PAID" and link payoutId
// 5. Return the payout ID
}
export async function processOrgPayout(payoutId: string): Promise<void> {
// 1. Load the OrganizationPayout + linked OrganizationPayoutAccount
// 2. Call Razorpay Contacts + Fund Account API (RazorpayX) OR
// create a manual bank transfer record
// 3. Update payout status to PROCESSING → COMPLETED (via webhook) or FAILED
}
Wire into: POST /api/organizations/[orgId]/payouts — replace the 501 stub with:
const batchId = await createOrgPayoutBatch(access.org.id);
return NextResponse.json({ batchId }, { status: 201 });
Dashboard update: /dashboard/organization/[orgId]/payouts/page.tsx — replace the lock card with:
- Payout history table (similar to admin payouts page)
- "Create payout batch" button (ORG_OWNER only)
- Payout status badges (PENDING/PROCESSING/COMPLETED/FAILED)
- Period date range display
Phase 4: Payout account encryption (2-3 hours)
Objective: Replace the base64 stub with real encryption for bank account numbers.
Current state: app/api/organizations/[orgId]/payout-account/route.ts line 71:
const accountNumberEncrypted = Buffer.from(accountNumber).toString("base64");
Implementation:
- Use
libsodium-wrappers (already in package.json) for symmetric encryption
- Server-side encryption key from
PAYOUT_ENCRYPTION_KEY env var
encryptAccountNumber(accountNumber) → returns encrypted + last4
decryptAccountNumber(encrypted) → returns plaintext (for admin view / payout processing)
- The encrypted value is stored in
accountNumberEncrypted; accountNumberLast4 stays plaintext for display
Phase 5: Consultant application/approval workflow (3-4 hours)
Objective: When a consultant wants to join a PROVIDER org, the org can auto-approve or require manual review.
Schema field: OrganizationProfile.autoApproveConsultants (already exists, default false)
Implementation:
- When
POST /api/organizations/[orgId]/members adds an ORG_CONSULTANT:
- If
autoApproveConsultants === true: set status = ACTIVE immediately
- If
autoApproveConsultants === false: set status = PENDING
- Same logic for invitation acceptance with
role = ORG_CONSULTANT
- Dashboard members page: show "Pending" badge for PENDING consultants + "Approve" button (ORG_ADMIN+)
- New API:
PATCH /api/organizations/[orgId]/members/[id] with status: "ACTIVE" to approve
The PATCH route already supports status changes — just need the frontend UI for the approval flow.
Phase 6: PROVIDER onboarding + seed data (2-3 hours)
Objective: Allow agency owners to sign up as PROVIDER orgs and seed realistic PROVIDER test data.
Onboarding:
- The existing ORG_ADMIN onboarding creates BUYER orgs. Extend
OrgAdminOrgSetupStep to show a kind selector (BUYER / PROVIDER) when the flag is on.
- When PROVIDER is selected, hide the billing mode selector (PROVIDER orgs don't use TAG_ONLY/SEAT_PACK/INVOICED_MONTHLY — they receive payouts, not invoices)
- Show rate configuration fields (platform/org/consultant %) instead
Seed data (prisma/seedFiles/15a-create-organizations.ts):
- Add PROVIDER org creation when
ENABLE_PROVIDER_ORGS === "true"
- Seed ORG_CONSULTANT members linked to existing ConsultantProfiles
- Seed OrganizationPayoutAccount with test bank details
- Seed OrganizationEarnings for a few payments
- Seed OrganizationPayout with COMPLETED status
Phase 7: HYBRID org behavior (1-2 hours)
Objective: Define and implement how HYBRID orgs work — they are both BUYER and PROVIDER simultaneously.
Use case: A university that both sponsors training for students (BUYER) AND hosts its professors as consultants (PROVIDER).
Implementation:
- HYBRID orgs see ALL nav items: Learners + Consultants + Billing + Payouts + Credits
- Both billing modes AND payout flows are active
- Members can be ORG_LEARNER (consuming sessions) or ORG_CONSULTANT (providing sessions)
- Earnings split applies to bookings made through ORG_CONSULTANT members
- Standard BUYER billing applies to bookings by ORG_LEARNER members
Most of this is already handled by the isProviderOrHybrid / isBuyerOrHybrid checks in the dashboard layout. The main work is verifying the earnings split and payout flows handle HYBRID correctly.
Verification checklist
Dependencies
Phase 1 (flip flag) → no deps, first step
Phase 2 (earnings split) → depends on Phase 1
Phase 3 (payout batch) → depends on Phase 2
Phase 4 (encryption) → independent, can parallelize with Phase 2
Phase 5 (approval workflow) → depends on Phase 1
Phase 6 (onboarding + seed) → depends on Phase 1
Phase 7 (HYBRID) → depends on Phase 2 + Phase 3
Estimated total effort
~18-24 hours of focused implementation, split across 7 phases. Phase 2 (earnings split) is the most critical and complex piece.
References
PROVIDER Organizations — Technical Implementation Plan
What is a PROVIDER org?
A PROVIDER organization is a consultant agency that hosts multiple consultants under its umbrella. Unlike BUYER orgs (schools/corporates that consume sessions), PROVIDER orgs supply services — the org recruits consultants, manages their catalog, and captures a slice of every booking via a 3-way revenue split:
Example: A ₹10,000 consultation booked through a PROVIDER org consultant:
Current state
PR #655 (
feature/enterprise) shipped the complete PROVIDER foundation:Already built (gated behind
ENABLE_PROVIDER_ORGS=false)Schema — all models exist in
prisma/schema.prisma:OrganizationProfile.kindsupportsPROVIDERandHYBRIDvaluesOrganizationProfilehasplatformCommissionRate(0.10),orgRetainRate(0.05),consultantPayoutRate(0.85),autoApproveConsultantsOrganizationMemberProfile.customConsultantPayoutRate— per-consultant rate overrideOrganizationPayoutAccount— org's bank account for receiving payoutsOrganizationPayout— payout history (parallelsConsultantPayout)OrganizationEarnings— org's share per payment (parallelsConsultantEarnings)OrgMemberRoleincludesORG_CONSULTANT(rank 40) andORG_SUPPORT(rank 30)API routes — all return 501 when flag is off:
POST /api/organizations— rejectskind=PROVIDERorkind=HYBRIDPOST /api/organizations/[orgId]/members— rejectsrole=ORG_CONSULTANTorORG_SUPPORTPATCH /api/organizations/[orgId]/members/[memberId]— rejects role changes to ORG_CONSULTANT/ORG_SUPPORTPOST /api/organizations/[orgId]/invitations— rejects ORG_CONSULTANT/ORG_SUPPORT invitation rolesPOST /api/organizations/invitations/accept— rejects acceptance of gated rolesGET /api/organizations/[orgId]/consultants— lists ORG_CONSULTANT membersGET/POST /api/organizations/[orgId]/payouts— org payout list + batch creationGET/PUT/DELETE /api/organizations/[orgId]/payout-account— bank account CRUDDashboard pages:
/dashboard/organization/[orgId]/consultants/page.tsx— renders "Provider tier required" lock card when 501/dashboard/organization/[orgId]/payouts/page.tsx— same lock cardRole gating:
PROVIDER_GATED_ROLES = ["ORG_CONSULTANT", "ORG_SUPPORT"]checked in 4 route filesNOT yet built
lib/payments/payouts/earnings-service.tshas zero PROVIDER logicOrganizationPayoutrow creation or gateway disbursementautoApproveConsultantsfield exists but not enforcedImplementation plan
Phase 1: Flip the flag + verify existing 501-gated routes (1-2 hours)
Objective: Set
ENABLE_PROVIDER_ORGS=trueand verify all gated routes become functional.Steps:
ENABLE_PROVIDER_ORGS=truein.envPOST /api/organizationswithkind: "PROVIDER"→ should succeedPOST /api/organizations/[orgId]/memberswithrole: "ORG_CONSULTANT"→ should succeedPOST /api/organizations/[orgId]/invitationswithrole: "ORG_CONSULTANT"→ should succeedPOST /api/organizations/invitations/acceptfor ORG_CONSULTANT invitation → should succeedGET /api/organizations/[orgId]/consultants→ should return ORG_CONSULTANT membersGET /api/organizations/[orgId]/payouts→ should return (empty) payouts listGET/PUT/DELETE /api/organizations/[orgId]/payout-account→ should CRUDFiles touched:
.envonly (no code changes for this phase)Phase 2: 3-way earnings split (4-6 hours)
Objective: When a payment is made for a booking through a PROVIDER org consultant, create both
ConsultantEarnings(consultant's share) ANDOrganizationEarnings(org's share) with the correct split.File:
lib/payments/payouts/earnings-service.tsImplementation:
In
createEarningsFromPayment(), after the existing platform fee calculation:Edge cases:
customConsultantPayoutRateoverrides only the consultant's share; org + platform shares adjust accordingly (or: org share stays fixed, consultant+platform adjust — decide policy)ENABLE_PROVIDER_ORGSis false butorg.kind === PROVIDER(shouldn't happen since creation is gated): fall through to BUYER pathAlso update:
refundEarnings()— when refunding a payment with OrganizationEarnings, reverse the org earnings proportionallyhandlePaymentSuccess()— no changes needed (it callscreateEarningsFromPaymentwhich handles the branching)Phase 3: Org payout batch creation + processing (4-6 hours)
Objective: Allow org owners to create payout batches for accumulated org earnings, and process them via the existing payout pipeline.
New file:
lib/payments/payouts/org-payout-service.tsWire into:
POST /api/organizations/[orgId]/payouts— replace the 501 stub with:Dashboard update:
/dashboard/organization/[orgId]/payouts/page.tsx— replace the lock card with:Phase 4: Payout account encryption (2-3 hours)
Objective: Replace the base64 stub with real encryption for bank account numbers.
Current state:
app/api/organizations/[orgId]/payout-account/route.tsline 71:Implementation:
libsodium-wrappers(already in package.json) for symmetric encryptionPAYOUT_ENCRYPTION_KEYenv varencryptAccountNumber(accountNumber)→ returns encrypted + last4decryptAccountNumber(encrypted)→ returns plaintext (for admin view / payout processing)accountNumberEncrypted;accountNumberLast4stays plaintext for displayPhase 5: Consultant application/approval workflow (3-4 hours)
Objective: When a consultant wants to join a PROVIDER org, the org can auto-approve or require manual review.
Schema field:
OrganizationProfile.autoApproveConsultants(already exists, default false)Implementation:
POST /api/organizations/[orgId]/membersadds an ORG_CONSULTANT:autoApproveConsultants === true: setstatus = ACTIVEimmediatelyautoApproveConsultants === false: setstatus = PENDINGrole = ORG_CONSULTANTPATCH /api/organizations/[orgId]/members/[id]withstatus: "ACTIVE"to approveThe PATCH route already supports status changes — just need the frontend UI for the approval flow.
Phase 6: PROVIDER onboarding + seed data (2-3 hours)
Objective: Allow agency owners to sign up as PROVIDER orgs and seed realistic PROVIDER test data.
Onboarding:
OrgAdminOrgSetupStepto show akindselector (BUYER / PROVIDER) when the flag is on.Seed data (
prisma/seedFiles/15a-create-organizations.ts):ENABLE_PROVIDER_ORGS === "true"Phase 7: HYBRID org behavior (1-2 hours)
Objective: Define and implement how HYBRID orgs work — they are both BUYER and PROVIDER simultaneously.
Use case: A university that both sponsors training for students (BUYER) AND hosts its professors as consultants (PROVIDER).
Implementation:
Most of this is already handled by the
isProviderOrHybrid/isBuyerOrHybridchecks in the dashboard layout. The main work is verifying the earnings split and payout flows handle HYBRID correctly.Verification checklist
POST /api/organizationswithkind=PROVIDERsucceeds when flag is onPOST /api/organizations/[orgId]/memberswithrole=ORG_CONSULTANTsucceedsconsultantProfileIdon OrganizationMemberProfilecustomConsultantPayoutRateoverrides the default when setautoApproveConsultants=false→ new ORG_CONSULTANT starts as PENDINGDependencies
Estimated total effort
~18-24 hours of focused implementation, split across 7 phases. Phase 2 (earnings split) is the most critical and complex piece.
References
lib/feature-flags.ts— theENABLE_PROVIDER_ORGSflaglib/payments/payouts/earnings-service.ts— where the 3-way split goesdocs/enterprise/decisions/2026-04-10-pr2-enterprise-foundation.md— ADR