1const n=`--- 2title: "Credit System PRD" 3author: "ShiftNex AI Team" 4status: "Approved" 5version: "2.0" 6last_updated: "2024-12-28" 7--- 8 9# Credit System PRD 10 11## Table of Contents 121. [Overview](#overview) 132. [Database Schema](#database-schema) 143. [Edge Functions](#edge-functions) 154. [Frontend Architecture](#frontend-architecture) 165. [Stripe Integration](#stripe-integration) 176. [Business Logic](#business-logic) 187. [User Flows](#user-flows) 198. [Security & Permissions](#security--permissions) 209. [Testing Strategy](#testing-strategy) 2110. [Appendices](#appendices) 22 23--- 24 25## 1. Overview 26 27### Purpose 28Credit facility management system for healthcare staffing operations, providing organizations with credit lines for payroll processing, invoice management, and payment method handling through Stripe integration. 29 30### Key Capabilities 31- **Credit Application Workflow**: 7-step application process with KYC/AML compliance 32- **Credit Limit Management**: Configurable limits with utilization tracking 33- **Transaction Tracking**: Real-time balance and transaction history 34- **Stripe Integration**: Payment methods, invoices, and payment processing 35- **Invoice Management**: Consolidated invoice creation and payment 36- **Bank Account Verification**: ACH/Plaid verification flows 37- **Multi-payment Method Support**: Credit cards and bank accounts 38 39### System Architecture 40 41\`\`\` 42âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 43â CREDIT SYSTEM ARCHITECTURE â 44ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ⤠45â â 46â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 47â â Frontend Layer â â 48â â âââââââââââââââââââ âââââââââââââââââââ â â 49â â â CreditApplication â â CreditAccountPageâ â â 50â â â (7-step wizard) â â (Dashboard) â â â 51â â âââââââââââââââââââ âââââââââââââââââââ â â 52â â âââââââââââââââââââ âââââââââââââââââââ â â 53â â â PayInvoicesDialogâ âFinancePaymentMethodsâ â â 54â â âââââââââââââââââââ âââââââââââââââââââ â â 55â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 56â â â 57â â¼ â 58â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 59â â Context Layer â â 60â â âââââââââââââââââââââââââââââââââââââââââââââââââââââââ â â 61â â â StripeBillingContext â â â 62â â â - Payment methods (cards, bank accounts) â â â 63â â â - Invoices with line items â â â 64â â â - Current balance â â â 65â â âââââââââââââââââââââââââââââââââââââââââââââââââââââââ â â 66â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 67â â â 68â â¼ â 69â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 70â â Edge Functions â â 71â â ââââââââââââââââââ ââââââââââââââââââ ââââââââââââââââ â â 72â â âcreate-stripe- â â get-stripe- â âcreate-setup- â â â 73â â â customer â â billing â â intent â â â 74â â ââââââââââââââââââ ââââââââââââââââââ ââââââââââââââââ â â 75â â ââââââââââââââââââ ââââââââââââââââââ ââââââââââââââââ â â 76â â âprocess-payroll-â âcreate-consol- â âget-invoice- â â â 77â â â payments â âidated-invoices â â amounts â â â 78â â ââââââââââââââââââ ââââââââââââââââââ ââââââââââââââââ â â 79â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 80â â â 81â â¼ â 82â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 83â â External Services â â 84â â âââââââââââââââââââ âââââââââââââââââââ â â 85â â â Stripe API â â Plaid API â â â 86â â â - Customers â â - Bank linking â â â 87â â â - Invoices â â - Verification â â â 88â â â - Payments â â â â â 89â â âââââââââââââââââââ âââââââââââââââââââ â â 90â ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 91â â 92âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 93\`\`\` 94 95--- 96 97## 2. Database Schema 98 99### 2.1 org_billing Table 100 101Primary table mapping organizations to Stripe customers. 102 103\`\`\`sql 104CREATE TABLE public.org_billing ( 105 id UUID NOT NULL DEFAULT gen_random_uuid() PRIMARY KEY, 106 organization_id UUID REFERENCES organizations(id) UNIQUE NOT NULL, 107 stripe_customer_id TEXT NOT NULL, 108 created_at TIMESTAMPTZ DEFAULT now(), 109 updated_at TIMESTAMPTZ DEFAULT now() 110); 111
112-- Indexes 113CREATE UNIQUE INDEX idx_org_billing_organization_id ON org_billing(organization_id); 114CREATE INDEX idx_org_billing_stripe_customer_id ON org_billing(stripe_customer_id); 115 116-- RLS Policies 117ALTER TABLE org_billing ENABLE ROW LEVEL SECURITY; 118 119CREATE POLICY "Org admins can view their billing" 120ON org_billing FOR SELECT 121USING ( 122 organization_id IN ( 123 SELECT organization_id FROM org_associations 124 WHERE user_id = auth.uid() AND role = 'admin' AND status = 'active' 125 ) 126); 127\`\`\` 128 129### 2.2 orders Table (Invoice Reference) 130 131Orders link to Stripe invoices after invoicing. 132 133\`\`\`sql 134-- Invoice linkage columns in orders table 135ALTER TABLE public.orders ADD COLUMN IF NOT EXISTS invoice_id TEXT; 136ALTER TABLE public.orders ADD COLUMN IF NOT EXISTS invoiced_at TIMESTAMPTZ; 137 138CREATE INDEX idx_orders_invoice_id ON orders(invoice_id) WHERE invoice_id IS NOT NULL; 139\`\`\` 140 141### 2.3 Credit Application Data Model (Frontend) 142 143\`\`\`typescript
144// src/pages/CreditApplication.tsx 145export interface CreditApplicationData { 146 // Step 1: Location & Authorization 147 applicationType?: 'organization' | 'location'; 148 selectedLocationId?: string; 149 facilityManagerId?: string; 150 authorizedByName?: string; 151 authorizedByTitle?: string; 152 153 // Step 2: Business Information & Compliance 154 legalBusinessName?: string; 155 dbaName?: string; 156 ein?: string; 157 businessAddress?: Address; 158 yearsInBusiness?: number; 159 naicsCode?: string; 160 industryType?: string; 161 businessType?: string; 162 annualRevenue?: number; 163 isShiftnexPrimary?: boolean; 164 shiftnexUsagePercentage?: number; 165 166 // Step 3: Financial Information 167 weeklyPayrollSpend?: number; 168 monthlyPayrollSpend?: number; 169 existingCreditFacilities?: string[]; 170 bankingRelationships?: string[]; 171 financialDocuments?: string[]; 172 cashFlowSeasonality?: boolean; 173 174 // Step 4: Beneficial Ownership (KYC) 175 beneficialOwners?: BeneficialOwner[]; 176 controllingPerson?: BeneficialOwner; 177 hasControllingPerson?: boolean; 178 179 // Step 5: Signer Information 180 signerName?: string; 181 signerTitle?: string; 182 signerDob?: string; 183 signerSsn?: string; 184 signerAuthority?: string; 185 personalGuaranteeRequired?: boolean; 186 personalGuarantee?: boolean; 187 188 // Step 6: Payment Method 189 paymentMethod?: PaymentMethodData; 190 interestRateStructure?: InterestRateTerms; 191 requestedCreditLimit?: number; 192 proposedCreditLimit?: number; 193 194 // Step 7: Review & Compliance 195 regulatoryDisclosures?: DisclosureAcceptance[]; 196 partnerBankName?: string; 197 electronicSignature?: string; 198 termsAccepted?: boolean; 199 200 // Metadata 201 submittedAt?: string; 202 applicationId?: string; 203} 204 205interface BeneficialOwner { 206 id: string; 207 name: string; 208 address: Address; 209 dateOfBirth: string; 210 ssnLast4: string; 211 ownershipPercentage: number; 212 identityDocuments?: string[]; 213} 214 215interface Address { 216 street: string; 217 city: string; 218 state: string; 219 zipCode: string; 220} 221\`\`\` 222 223### 2.4 Credit Account Data Model 224 225\`\`\`typescript
226// src/hooks/useCreditAccount.ts 227interface CreditAccount { 228 accountId: string; 229 creditLimit: number; 230 availableBalance: number; 231 currentBalance: number; 232 statementBalance: number; 233 utilization: number; // Percentage 234 lastPaymentDate: string; 235 lastPaymentAmount: number; 236 nextPaymentDate: string; 237 nextPaymentAmount: number; 238 autoPayEnabled: boolean; 239 accountStatus: 'active' | 'suspended' | 'closed'; 240 monthlySpend: number; 241 monthlyPayments: number; 242 averageBalance: number; 243 daysUntilDue: number; 244} 245 246interface CreditTransaction { 247 id: string; 248 date: string; 249 type: 'draw' | 'payment' | 'fee' | 'interest'; 250 amount: number; 251 description: string; 252 balance: number; 253 reference?: string; 254} 255\`\`\` 256 257### 2.5 Application Status Model 258 259\`\`\`typescript
260// src/hooks/useCreditApplicationStatus.ts 261type CreditApplicationStatus = 262 | 'no_application' 263 | 'pending_approval' 264 | 'approved' 265 | 'rejected'; 266 267interface CreditApplicationStatusData { 268 status: CreditApplicationStatus; 269 applicationId?: string; 270 submittedAt?: string; 271 reviewedAt?: string; 272 approvedAmount?: number; 273 rejectionReason?: string; 274 organizationId: string; 275} 276\`\`\` 277 278--- 279 280## 3. Edge Functions 281 282### 3.1 create-stripe-customer 283 284Creates a Stripe customer during organization onboarding. 285 286\`\`\` 287POST /functions/v1/create-stripe-customer 288Authorization: Bearer <STATIC_AUTH_KEY> 289\`\`\` 290 291#### Request Interface 292\`\`\`typescript 293interface CreateStripeCustomerRequest { 294 organization_id: string; 295 organization_name: string; 296 contact_email: string; 297 address?: string; 298 address2?: string; 299 city?: string; 300 state?: string; 301 zip?: string; 302} 303\`\`\` 304 305#### Response Interface 306\`\`\`typescript 307interface CreateStripeCustomerResponse { 308 success: boolean; 309 stripe_customer_id: string; 310 message: string; 311} 312\`\`\` 313 314#### Implementation Flow 315\`\`\` 316âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 317â create-stripe-customer â 318ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ⤠319â â 320â âââââââââââââââ âââââââââââââââââââââ â 321â â Verify Auth ââââ>â Validate Required â â 322â â Key â â Fields â â 323â âââââââââââââââ âââââââââââââââââââââ â 324â â â 325â â¼ â 326â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 327â â Stripe API: Create Customer â â 328â â - name: organization_name â â 329â â - email: contact_email â â 330â â - metadata: { organization_id, source } â â 331â â - address: { line1, city, state, postal_code, country } â â 332â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 333â â â 334â â¼ â 335â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 336â â Wait 25 seconds (timing requirement) â â 337â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 338â â â 339â â¼ â 340â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 341â â Upsert org_billing record with stripe_customer_id â â 342â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 343â â 344âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 345\`\`\` 346 347--- 348 349### 3.2 get-stripe-billing 350 351Fetches comprehensive billing data from Stripe. 352 353\`\`\` 354POST /functions/v1/get-stripe-billing 355Authorization: Bearer <JWT_TOKEN> 356\`\`\` 357 358#### Request Interface 359\`\`\`typescript 360interface GetStripeBillingRequest { 361 organization_id: string; 362} 363\`\`\` 364 365#### Response Interface 366\`\`\`typescript 367interface GetStripeBillingResponse { 368 paymentMethods: PaymentMethod[]; 369 invoices: Invoice[]; 370 currentBalance: number; 371 customerId?: string; 372 message?: string; 373} 374 375interface PaymentMethod { 376 id: string; 377 type: 'card' | 'us_bank_account'; 378 last4: string; 379 brand: string;
380 expMonth?: number; 381 expYear?: number; 382 bankName?: string; 383 accountType?: string; 384 isDefault: boolean; 385 status?: string; 386} 387 388interface Invoice { 389 id: string; 390 number: string; 391 date: string; 392 amount: number; 393 amount_due: number; 394 amount_remaining: number; 395 status: 'paid' | 'pending' | 'overdue' | 'unpaid'; 396 description: string; 397 downloadUrl?: string; 398 lineItems: LineItem[]; 399 orderDetails?: OrderDetails; 400 orderId?: string; 401 provider_amount?: number; 402 network_fee_amount?: number; 403 network_fee_percentage?: number; 404 regular_hours?: number; 405 regular_amount?: number; 406 overtime_hours?: number; 407 overtime_amount?: number; 408} 409 410interface LineItem { 411 id: string; 412 description: string; 413 amount: number; 414 quantity: number; 415 order_id?: string; 416 type?: 'provider_payment' | 'network_fee' | 'other'; 417} 418\`\`\` 419 420#### Implementation Details 421\`\`\`typescript 422// Parallel Stripe API calls for performance 423const [customerResponse, paymentMethodsResponse, bankAccountsResponse, setupIntentsResponse] = 424 await Promise.all([ 425 fetch(\`https://api.stripe.com/v1/customers/\${customerId}\`, options), 426 fetch(\`https://api.stripe.com/v1/payment_methods?customer=\${customerId}&type=card\`, options), 427 fetch(\`https://api.stripe.com/v1/payment_methods?customer=\${customerId}&type=us_bank_account\`, options), 428 fetch(\`https://api.stripe.com/v1/setup_intents?customer=\${customerId}\`, options) 429 ]); 430 431// Paginated invoice fetching (up to 1000 invoices) 432const fetchAllInvoices = async () => { 433 const allInvoices = []; 434 let hasMore = true; 435 let startingAfter = null; 436 437 while (hasMore && allInvoices.length < 1000) { 438 const response = await fetch( 439 \`https://api.stripe.com/v1/invoices?customer=\${customerId}&limit=100\${ 440 startingAfter ? \`&starting_after=\${startingAfter}\` : '' 441 }&expand[]=data.lines.data&expand[]=data.payment_intent\`, 442 options 443 ); 444 const data = await response.json(); 445 allInvoices.push(...data.data); 446 hasMore = data.has_more; 447 startingAfter = data.data[data.data.length - 1]?.id; 448 } 449 return allInvoices; 450}; 451\`\`\` 452 453--- 454 455### 3.3 create-setup-intent 456 457Creates Stripe SetupIntent for adding payment methods. 458 459\`\`\` 460POST /functions/v1/create-setup-intent 461Authorization: Bearer <JWT_TOKEN> 462\`\`\` 463 464#### Request Interface 465\`\`\`typescript 466interface CreateSetupIntentRequest { 467 organization_id: string; 468 payment_method_type?: 'card' | 'us_bank_account'; // Default: 'card' 469} 470\`\`\` 471 472#### Response Interface 473\`\`\`typescript 474interface CreateSetupIntentResponse { 475 client_secret: string; 476 setup_intent_id: string; 477} 478\`\`\` 479 480#### Error Codes 481| Code | Description | 482|------|-------------| 483| 401 | Unauthorized - Invalid token | 484| 400 | Bad Request - Missing org ID or no Stripe customer | 485| 403 | Forbidden - Not admin for organization | 486| 500 | Internal Server Error - Stripe API failure | 487 488--- 489 490### 3.4 process-payroll-payments 491 492Processes payments for multiple invoices. 493 494\`\`\` 495POST /functions/v1/process-payroll-payments 496Authorization: Bearer <JWT_TOKEN> 497\`\`\` 498 499#### Request Interface 500\`\`\`typescript 501interface ProcessPaymentRequest { 502 organization_id: string; 503 invoice_ids: string[]; 504 payment_method_id: string; 505} 506\`\`\` 507 508#### Response Interface 509\`\`\`typescript 510interface ProcessPaymentResponse { 511 success: boolean; 512 results: PaymentResult[]; 513 summary: { 514 total: number; 515 successful: number; 516 failed: number; 517 }; 518} 519 520interface PaymentResult { 521 invoice_id: string; 522 success: boolean; 523 amount?: number; 524 status?: string; 525 processing?: boolean; 526 message?: string; 527 error?: string; 528} 529\`\`\` 530 531#### Payment Flow 532\`\`\` 533âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 534â process-payroll-payments â 535ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ⤠536â â 537â For each invoice_id in invoice_ids: â 538â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 539â â 1. GET /v1/invoices/{invoice_id} â â 540â â ââ> Check status, get amount_due â â 541â â â â 542â â 2. Skip if status === 'paid' â â 543â â â â 544â â 3. POST /v1/invoices/{invoice_id}/pay â â 545â â body: { payment_method: payment_method_id } â â 546â â â â 547â â 4. Handle response: â â 548â â - 'paid': Success, payment complete â â 549â â - 'open': Processing (3-5 days for bank transfers) â â 550â â - other: Failed, capture error message â â 551â âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ â 552â â 553â Return: { success, results, summary } â 554â â 555âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 556\`\`\` 557 558--- 559 560### 3.5 create-consolidated-invoices 561 562Creates Stripe invoices for approved shifts (system cron job). 563 564\`\`\` 565POST /functions/v1/create-consolidated-invoices 566x-system-auth: <SYSTEM_AUTH_KEY> 567\`\`\` 568 569#### Request Interface 570\`\`\`typescript 571interface CreateConsolidatedInvoicesRequest { 572 olderThanHours?: number; // Default: 48 573} 574\`\`\` 575 576#### Response Interface 577\`\`\`typescript 578interface CreateConsolidatedInvoicesResponse { 579 success: boolean; 580 invoices_created: number; 581 orders_processed: number; 582 results: LocationResult[]; 583} 584 585interface LocationResult { 586 locationId: string; 587 locationName: string; 588 invoiceId?: string; 589 ordersCount: number; 590 error?: string; 591} 592\`\`\` 593 594#### Business Logic 595\`\`\`typescript 596// Groups approved shifts by location 597// Calculates network fees based on order type: 598// - 'shared': 10% markup 599// - 'managed': 30% markup 600// - 'private': No fee (excluded from invoicing) 601 602// Week boundary calculation (Sunday-Saturday) 603const currentWeekStart = new Date(); 604currentWeekStart.setDate(now.getDate() - now.getDay()); 605currentWeekStart.setHours(0, 0, 0, 0); 606 607// Only process shifts from completed weekly periods 608// AND older than 48 hours 609const effectiveCutoff = Math.min(cutoff48Hours, currentWeekStart); 610\`\`\` 611 612--- 613 614### 3.6 get-invoice-amounts 615 616Fetches current invoice amounts and statuses. 617 618\`\`\` 619POST /functions/v1/get-invoice-amounts 620Authorization: Bearer <JWT_TOKEN> 621\`\`\` 622 623#### Request Interface 624\`\`\`typescript 625interface GetInvoiceAmountsRequest { 626 invoice_ids: string[]; 627} 628\`\`\` 629 630#### Response Interface 631\`\`\`typescript 632interface GetInvoiceAmountsResponse { 633 invoices: Record<string, { 634 amount_due: number; 635 status: string; 636 }>; 637} 638\`\`\` 639 640--- 641 642## 4. Frontend Architecture 643 644### 4.1 Component Tree 645 646\`\`\` 647src/ 648âââ pages/ 649â âââ CreditApplication.tsx # 7-step application wizard 650â âââ CreditAccountPage.tsx # Credit account dashboard 651â âââ Finance.tsx # Finance overview (invoices) 652â 653âââ components/credit/ 654â âââ Application Steps/ 655â â âââ CreditWelcome.tsx # Start page 656â â âââ LocationAuthorization.tsx # Step 1 657â â âââ BusinessInformation.tsx # Step 2 658â â âââ FinancialInformation.tsx # Step 3 659â â âââ BeneficialOwnership.tsx # Step 4 (KYC) 660â â âââ SignerInformation.tsx # Step 5 661â â âââ PaymentMethodSelection.tsx # Step 6 662â â âââ ReviewSubmit.tsx # Step 7 663â â âââ SuccessPage.tsx # Confirmation 664â â 665â âââ UI Components/ 666â â âââ ProgressBar.tsx # Step progress indicator 667â â âââ StepContainer.tsx # Step wrapper 668â â âââ EligibilityCheck.tsx # Pre-qualification 669â â âââ PaymentMethodSchema.ts # Validation schema 670â â 671â âââ Status Views/ 672â â âââ NoApplicationView.tsx # No application state 673â â âââ PendingApprovalView.tsx # Awaiting review 674â â âââ RejectedApplicationView.tsx # Rejected state 675â â âââ ApplicationStatusDemo.tsx # Demo state switcher 676â â 677â âââ Account Management/ 678â â âââ PaymentMethodSummary.tsx # Default payment display 679â â âââ PaymentDialog.tsx # Make payment modal 680â â âââ TransactionsList.tsx # Transaction history (mobile) 681â â âââ TransactionsTable.tsx # Transaction history (desktop) 682â â âââ TransactionFilters.tsx # Filter controls 683â â 684âââ components/finance/ 685â âââ FinanceDues.tsx # Outstanding amounts 686â âââ FinanceBalance.tsx # Account balance 687â âââ FinancePaymentMethods.tsx # Payment method list 688â âââ FacilityInvoices.tsx # Invoice list by facility 689â âââ InvoiceDetail.tsx # Single invoice view 690â âââ PayInvoicesDialog.tsx # Pay invoices modal 691â âââ FinancePeriodCost.tsx # Cost by period 692â âââ FinanceSharedServices.tsx # Shared service costs 693â âââ FinanceManagedServices.tsx # Managed service costs 694â âââ FinanceSubscriptions.tsx # Subscription management 695â 696âââ contexts/ 697â âââ StripeBillingContext.tsx # Billing data provider 698â 699âââ hooks/ 700â âââ useCreditAccount.ts # Credit account data 701â âââ useCreditApplicationStatus.ts # Application status 702â 703âââ components/ 704 âââ BankAccountVerification.tsx # ACH verification flow 705\`\`\` 706 707### 4.2 StripeBillingContext 708 709Central context for all billing data. 710 711\`\`\`typescript
712// src/contexts/StripeBillingContext.tsx 713interface StripeBillingData { 714 paymentMethods: PaymentMethod[]; 715 invoices: Invoice[]; 716 payrollInvoices: Invoice[]; // Subset with order details 717 currentBalance: number; 718 customerId?: string; 719} 720 721interface StripeBillingContextType { 722 data: StripeBillingData; 723 loading: boolean; 724 error: string | null; 725 refetch: () => void; 726} 727 728export const StripeBillingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { 729 const { selectedOrgId } = useOrganization(); 730 731 const fetchWithRetry = async (retries = 5, delay = 3000) => { 732 for (let attempt = 1; attempt <= retries; attempt++) { 733 try { 734 const { data, error } = await supabase.functions.invoke('get-stripe-billing', { 735 body: { organization_id: selectedOrgId } 736 }); 737 if (error) throw new Error(error.message); 738 739 // Filter payroll invoices (those with order details) 740 const payrollInvoices = data.invoices.filter(inv => 741 inv.orderDetails || inv.orderId || 742 inv.lineItems?.some(item => item.order_id) 743 ); 744 745 return { ...data, payrollInvoices }; 746 } catch (err) { 747 if (attempt === retries) throw err; 748 await new Promise(r => setTimeout(r, delay)); 749 } 750 } 751 }; 752 753 // Auto-fetch on org change 754 useEffect(() => { 755 if (selectedOrgId && userProfile) { 756 fetchBillingData(); 757 } 758 }, [selectedOrgId, userProfile]); 759 760 return ( 761 <StripeBillingContext.Provider value={{ data, loading, error, refetch }}> 762 {children} 763 </StripeBillingContext.Provider> 764 ); 765}; 766 767export const useStripeBilling = () => { 768 const context = useContext(StripeBillingContext); 769 if (!context) throw new Error('useStripeBilling must be used within StripeBillingProvider'); 770 return context; 771}; 772\`\`\` 773 774### 4.3 useCreditApplicationStatus Hook 775 776\`\`\`typescript
777// src/hooks/useCreditApplicationStatus.ts 778export const useCreditApplicationStatus = () => { 779 const { selectedOrgId } = useOrganization(); 780 const [statusData, setStatusData] = useState<CreditApplicationStatusData | null>(null); 781 782 // Storage key per organization 783 const getStorageKey = () => \`credit-application-status-\${selectedOrgId}\`; 784 785 useEffect(() => { 786 const loadStatus = () => { 787 const stored = localStorage.getItem(getStorageKey()); 788 if (stored) { 789 const data = JSON.parse(stored); 790 setStatusData(data); 791 792 // Demo: Auto-approve after 5 minutes 793 if (data.status === 'pending_approval' && data.submittedAt) { 794 const timeDiff = Date.now() - new Date(data.submittedAt).getTime(); 795 if (timeDiff > 300000) { 796 approveApplication(20000); 797 } 798 } 799 } else { 800 setStatusData({ status: 'no_application', organizationId: selectedOrgId }); 801 } 802 }; 803 804 loadStatus(); 805 const interval = setInterval(loadStatus, 30000); // Check every 30s 806 return () => clearInterval(interval); 807 }, [selectedOrgId]); 808 809 const submitApplication = (applicationId: string) => { 810 updateStatus({ status: 'pending_approval', applicationId, submittedAt: new Date().toISOString() }); 811 }; 812 813 const approveApplication = (approvedAmount: number) => { 814 updateStatus({ status: 'approved', reviewedAt: new Date().toISOString(), approvedAmount }); 815 }; 816 817 const rejectApplication = (rejectionReason: string) => { 818 updateStatus({ status: 'rejected', reviewedAt: new Date().toISOString(), rejectionReason }); 819 }; 820 821 return { statusData, loading, submitApplication, approveApplication, rejectApplication, resetApplication }; 822}; 823\`\`\` 824 825### 4.4 useCreditAccount Hook 826 827\`\`\`typescript
828// src/hooks/useCreditAccount.ts 829export const useCreditAccount = () => { 830 const { selectedOrgId } = useOrganization(); 831 const { statusData } = useCreditApplicationStatus(); 832 const [creditAccount, setCreditAccount] = useState<CreditAccount | null>(null); 833 const [transactions, setTransactions] = useState<CreditTransaction[]>([]); 834 835 useEffect(() => { 836 if (!selectedOrgId || !statusData) return; 837 838 const fetchCreditAccount = async () => { 839 if (statusData.status === 'approved') { 840 // TODO: Replace with actual API call 841 setCreditAccount(mockCreditAccount); 842 setTransactions(mockTransactions); 843 } else { 844 setCreditAccount(null); 845 setTransactions([]); 846 } 847 }; 848 849 fetchCreditAccount(); 850 }, [selectedOrgId, statusData]); 851 852 return { 853 creditAccount, 854 transactions, 855 loading, 856 error, 857 refetch, 858 applicationStatus: statusData?.status || 'no_application', 859 statusData 860 }; 861}; 862\`\`\` 863 864--- 865 866## 5. Stripe Integration 867 868### 5.1 Customer Lifecycle 869 870\`\`\` 871âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 872â STRIPE CUSTOMER LIFECYCLE â 873ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ⤠874â â 875â 1. Organization Created (via create-organization) â 876â â â 877â â¼ â 878â 2. create-stripe-customer triggered â 879â - Creates Stripe Customer â 880â - Waits 25 seconds â 881â - Creates org_billing record â 882â â â 883â â¼ â 884â 3. User adds payment method â 885â - create-setup-intent called â 886â - Returns client_secret for Stripe.js â 887â â â 888â â¼ â 889â 4. Stripe.js confirms SetupIntent â 890â - Payment method attached to customer â 891â â â 892â â¼ â 893â 5. Shifts approved â Invoices created â 894â - create-consolidated-invoices (cron) â 895â - Groups by location, calculates fees â 896â â â 897â â¼ â 898â 6. User pays invoices â 899â - process-payroll-payments â 900â - Stripe charges payment method â 901â â 902âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 903\`\`\` 904 905### 5.2 Payment Method Types 906 907| Type | Status Values | Verification | 908|------|--------------|--------------| 909| card | \`active\` | Immediate | 910| us_bank_account | \`new\`, \`verified\`, \`pending_verification\` | Plaid or micro-deposits | 911 912### 5.3 Invoice Statuses 913 914| Status | Description | 915|--------|-------------| 916| \`draft\` | Being prepared, not finalized | 917| \`open\` | Finalized, awaiting payment | 918| \`paid\` | Payment successful | 919| \`void\` | Cancelled | 920| \`uncollectible\` | Marked as bad debt | 921 922--- 923 924## 6. Business Logic 925 926### 6.1 Network Fee Calculation 927 928\`\`\`typescript 929// Order type fee structure 930const NETWORK_FEES = { 931 shared: 0.10, // 10% markup 932 managed: 0.30, // 30% markup 933 private: 0.00 // No fee (not invoiced through platform) 934}; 935 936const calculateNetworkFee = (order: Order, basePayrollAmount: number): number => { 937 const feeRate = NETWORK_FEES[order.type] || 0; 938 return basePayrollAmount * feeRate; 939}; 940 941// Invoice line item creation 942const createInvoiceLineItems = (order: Order) => { 943 const regularPay = order.regular_hours * order.payrate; 944 const overtimePay = order.overtime_hours * order.payrate * 1.5; 945 const basePayroll = regularPay + overtimePay; 946 const networkFee = calculateNetworkFee(order, basePayroll); 947 948 return [ 949 { description: \`Regular Hours (\${order.regular_hours}h)\`, amount: regularPay }, 950 { description: \`Overtime Hours (\${order.overtime_hours}h)\`, amount: overtimePay }, 951 { description: \`Network Fee (\${NETWORK_FEES[order.type] * 100}%)\`, amount: networkFee } 952 ]; 953}; 954\`\`\` 955 956### 6.2 Invoice Consolidation Rules 957 958\`\`\`typescript 959// Invoices are created when: 960// 1. Order status = 'filled' 961// 2. order_time_summary.approved = true 962// 3. invoice_id IS NULL 963// 4. type != 'private' 964// 5. shift_start < effectiveCutoff (completed week + 48 hours old) 965 966// Grouped by: 967// - organization_id 968// - location_id 969 970// Week boundaries: Sunday 00:00 to Saturday 23:59 971\`\`\` 972 973### 6.3 Application Status State Machine 974 975\`\`\` 976âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 977â APPLICATION STATUS STATE MACHINE â 978ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ⤠979â â 980â ââââââââââââââââââââ â 981â â no_application âââââ Start Application âââââ â 982â ââââââââââââââââââââ â â 983â â² â â 984â â â¼ â 985â Reset âââââââââââââââââââââââ â 986â â â pending_approval â â 987â âââââââââ´âââââââ â (Under Review) â â 988â â â âââââââââââââââââââââââ â 989â â â â â â 990â â â Approve Reject â 991â â â â â â 992â â â¼ â¼ â¼ â 993â â ââââââââââââââââââââ âââââââââââââââââââââââââââ â 994â âââââ rejected â â approved â â 995â â (Can Reapply) â â (Account Active) â â 996â ââââââââââââââââââââ âââââââââââââââââââââââââââ â 997â â 998âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 999\`\`\` 1000 1001--- 1002 1003## 7. User Flows 1004 1005### 7.1 Credit Application Flow 1006 1007\`\`\` 1008âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1009â CREDIT APPLICATION FLOW â 1010ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ⤠1011â â 1012â User Frontend Backend â 1013â â â â â 1014â âââNavigate to /creditââââ>â â â 1015â â â â â 1016â â<ââShow CreditWelcomeâââââ â â 1017â â â â â 1018â âââClick "Get Started"ââââ>â â â 1019â â â â â 1020â â âââââââââââââââââââââââââââââââââââââââââââ â â 1021â â â Step 1: Location Authorization â â â 1022â â â - Select org or location scope â â â 1023â â â - Authorize representative â â â 1024â â âââââââââââââââââââââââââââââââââââââââââââ â â 1025â â â Step 2: Business Information â â â 1026â â â - Legal name, EIN, address â â â 1027â â â - Industry, revenue â â â 1028â â âââââââââââââââââââââââââââââââââââââââââââ â â 1029â â â Step 3: Financial Information â â â 1030â â â - Payroll spend, banking â â â 1031â â âââââââââââââââââââââââââââââââââââââââââââ â â 1032â â â Step 4: Beneficial Ownership (KYC) â â â 1033â â â - 25%+ owners, controlling person â â â 1034â â âââââââââââââââââââââââââââââââââââââââââââ â â 1035â â â Step 5: Signer Information â â â 1036â â â - Authorized signer details â â â 1037â â âââââââââââââââââââââââââââââââââââââââââââ â â 1038â â â Step 6: Payment Method â â â 1039â â â - Add bank account or card ââââ>â â 1040â â â â â â 1041â â â â<ââââ â 1042â â âââââââââââââââââââââââââââââââââââââââââââ â â 1043â â â Step 7: Review & Submit â â â 1044â â â - E-signature, disclosures â â â 1045â â âââââââââââââââââââââââââââââââââââââââââââ â â 1046â â â â â 1047â âââSubmit Applicationâââââ>â â â 1048â â âââSave to localStorageâââââââ â 1049â â âââSet status: pendingââââââââ â 1050â â<ââShow SuccessPageâââââââ â â 1051â â â â â 1052âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1053\`\`\` 1054 1055### 7.2 Pay Invoices Flow 1056 1057\`\`\` 1058âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1059â PAY INVOICES FLOW â 1060ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ⤠1061â â 1062â User Frontend Backend â 1063â â â â â 1064â âââSelect invoicesââââââââ>â â â 1065â âââClick "Pay"ââââââââââââ>â â â 1066â â â â â 1067â â<ââPayInvoicesDialogââââââ â â 1068â â - Invoice summary â â â 1069â â - Payment methods â â â 1070â â â â â 1071â âââSelect payment methodââ>â â â 1072â âââConfirm paymentââââââââ>â â â 1073â â â â â 1074â â âââprocess-payroll-paymentsâ>â â 1075â â â { organization_id, â â 1076â â â invoice_ids, â â 1077â â â payment_method_id } â â 1078â â â â â 1079â â â For each invoice: â â 1080â â â â â â 1081â â â ââ> GET invoice â â 1082â â â ââ> POST pay â â 1083â â â ââ> Record result â â 1084â â â â â 1085â â â<ââ{ results, summary }âââââ â 1086â â â â â 1087â â<ââShow resultsâââââââââââ â â 1088â â - Success count â â â 1089â â - Failure details â â â 1090â â â â â 1091â âââClose dialogâââââââââââ>â â â 1092â â ââârefetch billing dataâââââ>â â 1093â â â â â 1094â
1094ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1095\`\`\` 1096 1097### 7.3 Add Payment Method Flow 1098 1099\`\`\` 1100âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1101â ADD PAYMENT METHOD FLOW â 1102ââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ⤠1103â â 1104â User Frontend Edge Function Stripe â 1105â â â â â â 1106â âââClick "Add"âââââââ>â â â â 1107â â â â â â 1108â â âââcreate-setup-intent->â â â 1109â â â â â â 1110â â â âââCreate SIââ>â â 1111â â â â<ââSI dataâââââ â 1112â â â â â â 1113â â â<ââclient_secretââââââ â â 1114â â â â â â 1115â â<ââShow Stripe Formââ â â â 1116â â â â â â 1117â âââEnter card/bankââ>â â â â 1118â â â â â â 1119â â âââStripe.js confirmââââââââââââââ>â â 1120â â â<ââConfirmationâââââââââââââââââââ â 1121â â â â â â 1122â â<ââSuccessâââââââââââ â â â 1123â â â â â â 1124â â ââârefetch billingââââ>â â â 1125â â â â â â 1126âââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââââ 1127\`\`\` 1128 1129--- 1130 1131## 8. Security & Permissions 1132 1133### 8.1 Permission Model 1134 1135\`\`\`typescript 1136// Credit/Billing operations require admin role 1137const checkBillingPermission = async (userId: string, orgId: string): Promise<boolean> => { 1138 const { data } = await supabase 1139 .from('org_associations') 1140 .select('role') 1141 .eq('organization_id', orgId) 1142 .eq('user_id', userId) 1143 .eq('status', 'active') 1144 .eq('role', 'admin') 1145 .single(); 1146 1147 return !!data; 1148}; 1149\`\`\` 1150 1151### 8.2 Edge Function Security 1152 1153| Function | Auth Type | Access | 1154|----------|-----------|--------| 1155| create-stripe-customer | Static Key | System triggers only | 1156| get-stripe-billing | JWT | Authenticated users | 1157| create-setup-intent | JWT | Org admins only | 1158| process-payroll-payments | JWT | Org admins only | 1159| create-consolidated-invoices | System Key | Cron jobs only | 1160| get-invoice-amounts | JWT | Authenticated users | 1161 1162### 8.3 Data Security 1163 1164\`\`\`sql 1165-- RLS on org_billing 1166CREATE POLICY "Admins can view org billing" 1167ON org_billing FOR SELECT 1168USING ( 1169 EXISTS ( 1170 SELECT 1 FROM org_associations 1171 WHERE org_associations.organization_id = org_billing.organization_id 1172 AND org_associations.user_id = auth.uid() 1173 AND org_associations.role = 'admin' 1174 AND org_associations.status = 'active' 1175 ) 1176); 1177 1178-- No direct INSERT/UPDATE/DELETE from client 1179-- All modifications via edge functions with service role 1180\`\`\` 1181 1182--- 1183 1184## 9. Testing Strategy 1185 1186### 9.1 Unit Tests 1187 1188\`\`\`typescript 1189// Network fee calculation tests
1190describe('calculateNetworkFee', () => { 1191 it('should calculate 10% for shared orders', () => { 1192 const fee = calculateNetworkFee({ type: 'shared' }, 1000); 1193 expect(fee).toBe(100); 1194 }); 1195 1196 it('should calculate 30% for managed orders', () => { 1197 const fee = calculateNetworkFee({ type: 'managed' }, 1000); 1198 expect(fee).toBe(300); 1199 }); 1200 1201 it('should return 0 for private orders', () => { 1202 const fee = calculateNetworkFee({ type: 'private' }, 1000); 1203 expect(fee).toBe(0); 1204 }); 1205}); 1206\`\`\` 1207 1208### 9.2 Integration Tests 1209 1210\`\`\`typescript 1211describe('StripeBillingContext', () => { 1212 it('should fetch billing data on mount', async () => { 1213 const { result, waitFor } = renderHook(() => useStripeBilling(), { 1214 wrapper: createTestWrapper({ orgId: 'test-org' }) 1215 }); 1216 1217 await waitFor(() => !result.current.loading); 1218 1219 expect(result.current.data.paymentMethods).toBeDefined(); 1220 expect(result.current.data.invoices).toBeDefined(); 1221 }); 1222 1223 it('should filter payroll invoices', async () => { 1224 const { result, waitFor } = renderHook(() => useStripeBilling(), { 1225 wrapper: createTestWrapper() 1226 }); 1227 1228 await waitFor(() => !result.current.loading); 1229 1230 result.current.data.payrollInvoices.forEach(inv => { 1231 expect(inv.orderDetails || inv.orderId || inv.lineItems.some(li => li.order_id)).toBe(true); 1232 }); 1233 }); 1234}); 1235\`\`\` 1236 1237### 9.3 E2E Tests 1238 1239\`\`\`typescript 1240describe('Credit Application', () => { 1241 test('should complete 7-step application', async ({ page }) => { 1242 await page.goto('/credit'); 1243 1244 // Welcome page 1245 await page.click('text=Get Started'); 1246 1247 // Step 1: Location Authorization 1248 await page.fill('[name="authorizedByName"]', 'John Doe'); 1249 await page.click('text=Continue'); 1250 1251 // ... continue through steps 1252 1253 // Step 7: Review & Submit 1254 await page.check('[name="termsAccepted"]'); 1255 await page.click('text=Submit Application'); 1256 1257 // Verify success 1258 await expect(page.locator('text=Application Submitted')).toBeVisible(); 1259 }); 1260 1261 test('should pay invoice', async ({ page }) => { 1262 await page.goto('/finance'); 1263 1264 // Select invoice 1265 await page.click('[data-testid="invoice-checkbox"]'); 1266 await page.click('text=Pay Selected'); 1267 1268 // Select payment method 1269 await page.click('[data-testid="payment-method-option"]'); 1270 await page.click('text=Confirm Payment'); 1271 1272 // Verify success 1273 await expect(page.locator('text=Payment Successful')).toBeVisible(); 1274 }); 1275}); 1276\`\`\` 1277 1278--- 1279 1280## 10. Appendices 1281 1282### A. Glossary 1283 1284| Term | Definition | 1285|------|------------| 1286| SetupIntent | Stripe object for saving payment methods without immediate charge | 1287| Network Fee | Platform markup on staffing orders (10-30%) | 1288| Consolidated Invoice | Single invoice grouping multiple shifts per location | 1289| KYC | Know Your Customer - identity verification requirements | 1290| ACH | Automated Clearing House - US bank transfer system | 1291 1292### B. Related Documents 1293 1294- [03-payroll-processing.md](./03-payroll-processing.md) - Payroll and invoicing 1295- [04-finance-management.md](./04-finance-management.md) - Finance overview 1296- [08-team-management.md](./08-team-management.md) - Organization roles 1297 1298### C. Environment Variables 1299 1300| Variable | Description | Required | 1301|----------|-------------|----------| 1302| stripeKey | Stripe secret key | Yes | 1303| SYSTEM_AUTH_KEY | System-level auth for cron jobs | Yes | 1304| SUPABASE_URL | Supabase project URL | Yes | 1305| SUPABASE_ANON_KEY | Supabase anonymous key | Yes | 1306| SUPABASE_SERVICE_ROLE_KEY | Supabase service role key | Yes | 1307 1308### D. Stripe API Endpoints Used 1309 1310| Endpoint | Method | Purpose | 1311|----------|--------|---------| 1312| /v1/customers | POST | Create customer | 1313| /v1/customers/{id} | GET | Get customer details | 1314| /v1/payment_methods | GET | List payment methods | 1315| /v1/setup_intents | POST | Create setup intent | 1316| /v1/setup_intents | GET | List setup intents | 1317| /v1/invoices | GET | List invoices | 1318| /v1/invoices | POST | Create invoice | 1319| /v1/invoices/{id}/pay | POST | Pay invoice | 1320| /v1/invoices/{id}/finalize | POST | Finalize draft invoice | 1321| /v1/invoiceitems | POST | Create invoice line item | 1322 1323### E. Component Props Reference 1324 1325\`\`\`typescript 1326// PayInvoicesDialog 1327interface PayInvoicesDialogProps { 1328 open: boolean; 1329 onOpenChange: (open: boolean) => void; 1330 invoices: Invoice[]; 1331 onSuccess: () => void; 1332} 1333 1334// PaymentMethodSummary (no props - uses context) 1335 1336// TransactionsList 1337interface TransactionsListProps { 1338 transactions: CreditTransaction[]; 1339 loading?: boolean; 1340} 1341 1342// TransactionsTable 1343interface TransactionsTableProps { 1344 transactions: CreditTransaction[]; 1345 loading?: boolean; 1346} 1347\`\`\` 1348 1349--- 1350 1351*Last Updated: December 28, 2024* 1352*Version: 2.0* 1353*Status: Approved* 1354`;export{n as default};
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.