Expense sharing calculations, split types (EQUAL/PERCENTAGE/FIXED), participant state management, and settlement tracking...
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[]
}
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])
}
enum SplitType {
EQUAL // Divide total equally among all participants + owner
PERCENTAGE // Each participant gets a specified percentage
FIXED // Each participant gets a fixed amount
}
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 }>
// Divide among (numParticipants + 1) to include owner
const equalShare = Math.round((totalAmount / (numParticipants + 1)) * 100) / 100
// Validation: total percentage must not exceed 100%
const amount = Math.round(totalAmount * (percentage / 100) * 100) / 100
// Validation: total fixed amounts cannot exceed totalAmount
// Each participant gets their specified shareAmount
PENDING β PAID (owner marks as received)
PENDING β DECLINED (participant declines)
PAID β (terminal)
DECLINED β (terminal)
Transition Rules:
markSharePaidActiondeclineShareActionAll expense sharing actions follow:
1. parseInput(schema, input) // Zod validation
2. requireCsrfToken(csrfToken) // CSRF protection
3. requireActiveSubscription() // Subscription gate
4. requireAuthUser() // Authentication
5. Authorization check // Ownership/participant verification
6. Business logic
7. revalidatePath('/') // Cache invalidation
8. return success/error
sendExpenseSharedEmail: Sent to each participant when expense is sharedsendPaymentReminderEmail: 24-hour cooldown (REMINDER_COOLDOWN_HOURS = 24)Location: src/lib/finance.ts (getSettlementBalances)
Groups by user AND currency to avoid mixing currencies:
// Returns for each user:
{
userId: string
youOwe: number // What you owe them (PENDING expenses they shared with you)
theyOwe: number // What they owe you (PENDING expenses you shared with them)
netBalance: number // theyOwe - youOwe
}
src/app/actions/expense-sharing.ts - All sharing actionssrc/lib/finance.ts - calculateShares, getSettlementBalancessrc/schemas/index.ts - shareExpenseSchema, participantSchematests/calculate-shares.test.ts - Unit tests for split calculations