datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
enum RequestStatus {
PENDING
PAID
DECLINED
EXPIRED
CANCELED
}
// User representation based on Supabase Auth
model User {
id String @id @default(uuid()) // Maps to auth.users.id
email String @unique
name String
phone String? // E.164 formatted
// Relationships
sentRequests PaymentRequest[] @relation("SentRequests")
receivedRequests PaymentRequest[] @relation("ReceivedRequests")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model PaymentRequest {
id String @id @default(uuid())
// Relationships
requesterId String
requester User @relation("SentRequests", fields: [requesterId], references: [id])
recipientId String? // Populated if recipientContact matches a user email
recipient User? @relation("ReceivedRequests", fields: [recipientId], references: [id])
// Core attributes
recipientContact String // Email or E.164 phone string
amountCents Int // Positive integer
currency String @default("USD")
note String? @db.VarChar(280)
status RequestStatus @default(PENDING)
shareToken String @unique @default(uuid())
// Timestamps
createdAt DateTime @default(now())
expiresAt DateTime // Required: createdAt + 7 days
paidAt DateTime?
declinedAt DateTime?
canceledAt DateTime?
updatedAt DateTime @updatedAt
@@index([recipientContact])
@@index([requesterId])
@@index([recipientId])
@@index([shareToken])
}-
Amount Format & Storage:
- Input validation enforces
> $0.00and<= 2decimal places. - Values are multiplied by 100 and stored as
amountCentsinteger. - UI converts back to
$XX.YYfor display.
- Input validation enforces
-
Recipient Contact Validation:
- Must be a valid email or strictly E.164 phone number.
- Saved exactly as entered or normalized to lowercase if email.
- Contact matching queries (to link
recipientId) are case-insensitive on email.
-
Status Transitions (Strict Mode):
- Modifications requiring state change must lock the record if possible or use transaction.
- Expired rule is passive (
expiresAt < now()), but state actions must check it to explicitly blockPAID/DECLINED/CANCELEDon expired claims.
-
Security Bounds:
requesterIdimmutable after creation.amountCentsimmutable after creation.currencyfixed toUSD.