| name | expense-sharing |
| description | Expense sharing calculations, split types (EQUAL/PERCENTAGE/FIXED), participant state management, and settlement tracking. Use when working with shared expenses, split calculations, payment status transitions, or settlement balances. |
Expense Sharing Domain Knowledge
Schema Overview
SharedExpense Model
model SharedExpense {
id String @id @default(cuid())
transactionId String @unique
ownerId String
splitType SplitType @default(EQUAL)
totalAmount Decimal @db.Decimal(12, 2)
currency Currency @default(USD)
description String?
transaction Transaction @relation(...)
owner User @relation("SharedExpensesOwned", ...)
participants ExpenseParticipant[]
}
ExpenseParticipant Model
model ExpenseParticipant {
id String @id @default(cuid())
sharedExpenseId String
userId String
shareAmount Decimal @db.Decimal(12, 2)
sharePercentage Decimal? @db.Decimal(5, 2)
status PaymentStatus @default(PENDING)
paidAt DateTime?
reminderSentAt DateTime?
@@unique([sharedExpenseId, userId])
}
Split Types
enum SplitType {
EQUAL
PERCENTAGE
FIXED
}
calculateShares Function
Location: src/app/actions/expense-sharing.ts
export function calculateShares(
splitType: SplitType,
totalAmount: number,
participants: Array<{ email: string; shareAmount?: number; sharePercentage?: number }>,
validEmails: string[],
): Map<string, { amount: number; percentage: number | null }>
EQUAL Split Logic
const equalShare = Math.round((totalAmount / (numParticipants + 1)) * 100) / 100
PERCENTAGE Split Logic
const amount = Math.round(totalAmount * (percentage / 100) * 100) / 100
FIXED Split Logic
Payment Status Transitions
PENDING → PAID (owner marks as received)
PENDING → DECLINED (participant declines)
PAID → (terminal)
DECLINED → (terminal)
Transition Rules:
- Only owner can mark PENDING → PAID via
markSharePaidAction
- Only participant can DECLINE their own share via
declineShareAction
- Cannot mark DECLINED share as PAID
Action Pipeline
All expense sharing actions follow:
1. parseInput(schema, input)
2. requireCsrfToken(csrfToken)
3. requireActiveSubscription()
4. requireAuthUser()
5. Authorization check
6. Business logic
7. revalidatePath('/')
8. return success/error
Email Notifications
sendExpenseSharedEmail: Sent to each participant when expense is shared
sendPaymentReminderEmail: 24-hour cooldown (REMINDER_COOLDOWN_HOURS = 24)
Settlement Balance Calculation
Location: src/lib/finance.ts (getSettlementBalances)
Groups by user AND currency to avoid mixing currencies:
{
userId: string
youOwe: number
theyOwe: number
netBalance: number
}
Key Files
src/app/actions/expense-sharing.ts - All sharing actions
src/lib/finance.ts - calculateShares, getSettlementBalances
src/schemas/index.ts - shareExpenseSchema, participantSchema
tests/calculate-shares.test.ts - Unit tests for split calculations