// ============================================================================= // Prisma Schema - Customer Portal BFF // ============================================================================= // IMPORTANT: When building Docker images, the Prisma client must be regenerated // from the production directory structure. See prisma/README.md for details. // ============================================================================= generator client { provider = "prisma-client-js" // Only include engines we actually need: // - native: for local development // - linux-musl-openssl-3.0.x: for Alpine production binaryTargets = ["native", "linux-musl-openssl-3.0.x"] } datasource db { provider = "postgresql" // Prisma 7+: URL is configured via prisma.config.ts for migrations // and via --url flag for studio. Runtime uses the adapter in PrismaClient. } model User { id String @id @default(uuid()) email String @unique passwordHash String? @map("password_hash") role UserRole @default(USER) // Authentication state only - profile data comes from WHMCS mfaSecret String? @map("mfa_secret") emailVerified Boolean @default(false) @map("email_verified") failedLoginAttempts Int @default(0) @map("failed_login_attempts") lockedUntil DateTime? @map("locked_until") lastLoginAt DateTime? @map("last_login_at") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") auditLogs AuditLog[] idMapping IdMapping? @@map("users") } model IdMapping { userId String @id @map("user_id") whmcsClientId Int @unique @map("whmcs_client_id") sfAccountId String? @map("sf_account_id") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") user User @relation(fields: [userId], references: [id], onDelete: Cascade) @@map("id_mappings") } model AuditLog { id String @id @default(uuid()) userId String? @map("user_id") action AuditAction resource String? details Json? ipAddress String? @map("ip_address") userAgent String? @map("user_agent") success Boolean @default(true) error String? createdAt DateTime @default(now()) @map("created_at") user User? @relation(fields: [userId], references: [id]) @@index([userId, action]) @@index([action, createdAt]) @@index([createdAt]) @@map("audit_logs") } enum UserRole { USER ADMIN } enum AuditAction { LOGIN_SUCCESS LOGIN_FAILED LOGOUT SIGNUP PASSWORD_RESET PASSWORD_CHANGE ACCOUNT_LOCKED ACCOUNT_UNLOCKED PROFILE_UPDATE MFA_ENABLED MFA_DISABLED API_ACCESS SYSTEM_MAINTENANCE } // Per-SIM daily usage snapshot used to build full-month charts model SimUsageDaily { id Int @id @default(autoincrement()) account String date DateTime @db.Date usageMb Float createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@unique([account, date]) @@index([account, date]) @@map("sim_usage_daily") } model SimVoiceOptions { account String @id voiceMailEnabled Boolean @default(false) @map("voice_mail_enabled") callWaitingEnabled Boolean @default(false) @map("call_waiting_enabled") internationalRoamingEnabled Boolean @default(false) @map("international_roaming_enabled") networkType String @default("4G") @map("network_type") createdAt DateTime @default(now()) @map("created_at") updatedAt DateTime @updatedAt @map("updated_at") @@map("sim_voice_options") } // Call history from SFTP monthly imports (domestic calls) model SimCallHistoryDomestic { id String @id @default(uuid()) account String // Customer phone number (e.g., "08077052946") callDate DateTime @db.Date @map("call_date") // Date the call was made callTime String @map("call_time") // Start time of the call (HHMMSS) calledTo String @map("called_to") // Phone number called location String? // Location info durationSec Int @map("duration_sec") // Duration in seconds (320 = 32.0 sec) chargeYen Int @map("charge_yen") // Call charge in JPY month String // YYYY-MM format for filtering createdAt DateTime @default(now()) @map("created_at") @@unique([account, callDate, callTime, calledTo]) @@index([account, month]) @@index([account, callDate]) @@map("sim_call_history_domestic") } // Call history from SFTP monthly imports (international calls) model SimCallHistoryInternational { id String @id @default(uuid()) account String // Customer phone number callDate DateTime @db.Date @map("call_date") // Date the call was made startTime String @map("start_time") // Start time of the call stopTime String? @map("stop_time") // Stop time (if available) country String? // Country/location for international calls calledTo String @map("called_to") // Phone number called durationSec Int @map("duration_sec") // Duration in seconds chargeYen Int @map("charge_yen") // Call charge in JPY month String // YYYY-MM format for filtering createdAt DateTime @default(now()) @map("created_at") @@unique([account, callDate, startTime, calledTo]) @@index([account, month]) @@index([account, callDate]) @@map("sim_call_history_international") } // SMS history from SFTP monthly imports model SimSmsHistory { id String @id @default(uuid()) account String // Customer phone number smsDate DateTime @db.Date @map("sms_date") // Date the SMS was sent smsTime String @map("sms_time") // Time the SMS was sent sentTo String @map("sent_to") // Phone number SMS was sent to smsType SmsType @default(DOMESTIC) @map("sms_type") // SMS or 国際SMS month String // YYYY-MM format for filtering createdAt DateTime @default(now()) @map("created_at") @@unique([account, smsDate, smsTime, sentTo]) @@index([account, month]) @@index([account, smsDate]) @@map("sim_sms_history") } enum SmsType { DOMESTIC // SMS INTERNATIONAL // 国際SMS } // Track which months have been imported model SimHistoryImport { id String @id @default(uuid()) month String @unique // YYYY-MM format talkFile String? @map("talk_file") // Filename imported smsFile String? @map("sms_file") // Filename imported talkRecords Int @default(0) @map("talk_records") // Records imported smsRecords Int @default(0) @map("sms_records") // Records imported importedAt DateTime @default(now()) @map("imported_at") status String @default("completed") // completed, failed, partial @@map("sim_history_imports") }