// Prisma schema for Vorteq Quest Portal // PostgreSQL 16 database generator client { provider = "prisma-client-js" } datasource db { provider = "postgresql" url = env("DATABASE_URL") } // ============================================================================= // AUTH DOMAIN - User Authentication & Authorization // ============================================================================= // Core user authentication table (Better Auth compatible) model auth_user { id String @id @default(cuid()) email String @unique email_verified Boolean @default(false) name String? image String? created_at DateTime @default(now()) updated_at DateTime @updatedAt // Password authentication password_hash String? // Account security deactivated Boolean @default(false) deactivated_at DateTime? deactivation_reason String? // Login tracking login_count Int @default(0) failed_login_count Int @default(0) last_login_at DateTime? last_login_ip String? last_failed_login_at DateTime? // Two-factor authentication two_factor_enabled Boolean @default(false) two_factor_secret String? two_factor_backup_codes String? // SSO fields sso_provider String? sso_id String? // Relationships auth_user_type_id String auth_user_type auth_user_type @relation(fields: [auth_user_type_id], references: [id]) auth_domain_id String? auth_domain auth_domain? @relation(fields: [auth_domain_id], references: [id]) sessions auth_session[] accounts auth_account[] password_history auth_password_history[] password_resets auth_password_reset[] security_answers auth_security_answer[] quest_user quest_user? @@index([email]) @@index([auth_user_type_id]) @@index([auth_domain_id]) @@map("auth_user") } // User session table (Better Auth compatible) model auth_session { id String @id @default(cuid()) user_id String token String @unique expires_at DateTime ip_address String? user_agent String? created_at DateTime @default(now()) updated_at DateTime @updatedAt user auth_user @relation(fields: [user_id], references: [id], onDelete: Cascade) @@index([user_id]) @@index([token]) @@index([expires_at]) @@map("auth_session") } // OAuth/Social provider accounts (Better Auth compatible) model auth_account { id String @id @default(cuid()) user_id String account_id String provider_id String access_token String? refresh_token String? id_token String? access_token_expires_at DateTime? refresh_token_expires_at DateTime? scope String? password String? created_at DateTime @default(now()) updated_at DateTime @updatedAt user auth_user @relation(fields: [user_id], references: [id], onDelete: Cascade) @@unique([provider_id, account_id]) @@index([user_id]) @@map("auth_account") } // OAuth client configuration model auth_oauth_client { id String @id @default(cuid()) client_id String @unique client_secret String redirect_uris String[] // Array of allowed redirect URIs grant_types String[] // Array of allowed grant types name String created_at DateTime @default(now()) updated_at DateTime @updatedAt revoked Boolean @default(false) access_tokens auth_oauth_access_token[] refresh_tokens auth_oauth_refresh_token[] @@map("auth_oauth_client") } // OAuth access tokens model auth_oauth_access_token { id String @id @default(cuid()) access_token String @unique client_id String user_id String? expires_at DateTime scopes String[] // Array of scopes created_at DateTime @default(now()) client auth_oauth_client @relation(fields: [client_id], references: [client_id], onDelete: Cascade) @@index([access_token]) @@index([expires_at]) @@map("auth_oauth_access_token") } // OAuth refresh tokens model auth_oauth_refresh_token { id String @id @default(cuid()) refresh_token String @unique access_token_id String? client_id String user_id String? expires_at DateTime scopes String[] // Array of scopes created_at DateTime @default(now()) revoked Boolean @default(false) client auth_oauth_client @relation(fields: [client_id], references: [client_id], onDelete: Cascade) @@index([refresh_token]) @@index([expires_at]) @@map("auth_oauth_refresh_token") } // User type/role definitions model auth_user_type { id String @id @default(cuid()) name String @unique description String? is_admin Boolean @default(false) is_customer Boolean @default(false) is_internal Boolean @default(false) created_at DateTime @default(now()) updated_at DateTime @updatedAt users auth_user[] permission_groups auth_user_type_permission_group[] @@map("auth_user_type") } // Domain/company grouping for multi-tenant scenarios model auth_domain { id String @id @default(cuid()) name String @unique description String? is_active Boolean @default(true) created_at DateTime @default(now()) updated_at DateTime @updatedAt users auth_user[] @@map("auth_domain") } // Password history for preventing reuse model auth_password_history { id String @id @default(cuid()) user_id String password_hash String created_at DateTime @default(now()) user auth_user @relation(fields: [user_id], references: [id], onDelete: Cascade) @@index([user_id]) @@map("auth_password_history") } // Password reset tokens model auth_password_reset { id String @id @default(cuid()) user_id String token String @unique expires_at DateTime used_at DateTime? ip_address String? min_time_bypass Boolean @default(false) created_at DateTime @default(now()) user auth_user @relation(fields: [user_id], references: [id], onDelete: Cascade) @@index([token]) @@index([user_id]) @@index([expires_at]) @@map("auth_password_reset") } // Permission groups (roles) model auth_permission_group { id String @id @default(cuid()) name String @unique description String? is_active Boolean @default(true) created_at DateTime @default(now()) updated_at DateTime @updatedAt rules auth_permission_group_rule[] user_types auth_user_type_permission_group[] @@map("auth_permission_group") } // Permission rules (individual permissions) model auth_permission_rule { id String @id @default(cuid()) name String @unique description String? auth_permission_rule_category_id String? category auth_permission_rule_category? @relation(fields: [auth_permission_rule_category_id], references: [id]) created_at DateTime @default(now()) updated_at DateTime @updatedAt groups auth_permission_group_rule[] @@index([auth_permission_rule_category_id]) @@map("auth_permission_rule") } // Permission rule categories for organization model auth_permission_rule_category { id String @id @default(cuid()) name String @unique description String? created_at DateTime @default(now()) updated_at DateTime @updatedAt rules auth_permission_rule[] @@map("auth_permission_rule_category") } // Junction table: permission groups to rules (many-to-many) model auth_permission_group_rule { id String @id @default(cuid()) auth_permission_group_id String auth_permission_rule_id String created_at DateTime @default(now()) group auth_permission_group @relation(fields: [auth_permission_group_id], references: [id], onDelete: Cascade) rule auth_permission_rule @relation(fields: [auth_permission_rule_id], references: [id], onDelete: Cascade) @@unique([auth_permission_group_id, auth_permission_rule_id]) @@index([auth_permission_group_id]) @@index([auth_permission_rule_id]) @@map("auth_permission_group_rule") } // Junction table: user types to permission groups (many-to-many) model auth_user_type_permission_group { id String @id @default(cuid()) auth_user_type_id String auth_permission_group_id String created_at DateTime @default(now()) user_type auth_user_type @relation(fields: [auth_user_type_id], references: [id], onDelete: Cascade) group auth_permission_group @relation(fields: [auth_permission_group_id], references: [id], onDelete: Cascade) @@unique([auth_user_type_id, auth_permission_group_id]) @@index([auth_user_type_id]) @@index([auth_permission_group_id]) @@map("auth_user_type_permission_group") } // Security questions for account recovery model auth_security_question { id String @id @default(cuid()) question String @unique is_active Boolean @default(true) sort_order Int @default(0) created_at DateTime @default(now()) updated_at DateTime @updatedAt answers auth_security_answer[] @@map("auth_security_question") } // User security question answers model auth_security_answer { id String @id @default(cuid()) user_id String auth_security_question_id String answer_hash String created_at DateTime @default(now()) updated_at DateTime @updatedAt user auth_user @relation(fields: [user_id], references: [id], onDelete: Cascade) question auth_security_question @relation(fields: [auth_security_question_id], references: [id]) @@unique([user_id, auth_security_question_id]) @@index([user_id]) @@map("auth_security_answer") } // ============================================================================= // QUEST DOMAIN - Portal-specific user and company data // ============================================================================= // Quest-specific user data (extends auth_user) model quest_user { id String @id @default(cuid()) auth_user_id String @unique is_sub_user Boolean @default(false) api_token String? @unique api_token_expires_at DateTime? created_at DateTime @default(now()) updated_at DateTime @updatedAt auth_user auth_user @relation(fields: [auth_user_id], references: [id], onDelete: Cascade) companies quest_user_company[] @@index([auth_user_id]) @@map("quest_user") } // Company/customer information model quest_company { id String @id @default(cuid()) epicor_cust_id String @unique display_name String can_access_invoices Boolean @default(false) invoicing_email String? order_ack_email String? receives_so_emails Boolean @default(false) is_active Boolean @default(true) created_at DateTime @default(now()) updated_at DateTime @updatedAt users quest_user_company[] @@index([epicor_cust_id]) @@map("quest_company") } // Junction table: users to companies (many-to-many with active company tracking) model quest_user_company { id String @id @default(cuid()) quest_user_id String quest_company_id String is_active_company Boolean @default(false) created_at DateTime @default(now()) user quest_user @relation(fields: [quest_user_id], references: [id], onDelete: Cascade) company quest_company @relation(fields: [quest_company_id], references: [id], onDelete: Cascade) @@unique([quest_user_id, quest_company_id]) @@index([quest_user_id]) @@index([quest_company_id]) @@map("quest_user_company") } // Account request for new user registration model quest_account_request { id String @id @default(cuid()) first_name String last_name String email String phone String? company_name String epicor_cust_id String? message String? request_processed Boolean @default(false) processed_at DateTime? processed_by String? approved Boolean @default(false) rejection_reason String? created_at DateTime @default(now()) updated_at DateTime @updatedAt @@index([email]) @@index([request_processed]) @@map("quest_account_request") } // Notification/alert system model quest_notification { id String @id @default(cuid()) title String text String is_alert Boolean @default(false) display_date DateTime display_time String? is_active Boolean @default(true) created_at DateTime @default(now()) updated_at DateTime @updatedAt alert_reads quest_user_notification_alert_read[] @@index([is_alert]) @@index([display_date]) @@index([is_active]) @@map("quest_notification") } // Tracking which users have read alert notifications model quest_user_notification_alert_read { id String @id @default(cuid()) quest_notification_id String auth_user_id String read_at DateTime @default(now()) notification quest_notification @relation(fields: [quest_notification_id], references: [id], onDelete: Cascade) @@unique([quest_notification_id, auth_user_id]) @@index([auth_user_id]) @@map("quest_user_notification_alert_read") } // Email event definitions (types of emails that can be sent) model quest_email_event { id String @id @default(cuid()) name String @unique description String? is_active Boolean @default(true) created_at DateTime @default(now()) updated_at DateTime @updatedAt user_subscriptions quest_user_email_event[] @@map("quest_email_event") } // User subscriptions to email events (many-to-many) model quest_user_email_event { id String @id @default(cuid()) auth_user_id String quest_email_event_id String subscribed Boolean @default(true) created_at DateTime @default(now()) email_event quest_email_event @relation(fields: [quest_email_event_id], references: [id], onDelete: Cascade) @@unique([auth_user_id, quest_email_event_id]) @@index([auth_user_id]) @@map("quest_user_email_event") } // Email log for tracking sent emails model quest_email_log { id String @id @default(cuid()) to_addresses String[] // Array of recipient emails from_address String subject String body String sent_at DateTime @default(now()) status String @default("sent") // sent, failed, pending error_message String? quest_email_event_id String? auth_user_id String? @@index([sent_at]) @@index([status]) @@index([auth_user_id]) @@map("quest_email_log") } // Plant/facility definitions model quest_plant { id String @id @default(cuid()) name String @unique code String @unique is_active Boolean @default(true) created_at DateTime @default(now()) updated_at DateTime @updatedAt inventory_assignments quest_inventory_plant[] @@map("quest_plant") } // Inventory type definitions (WIP, Finished Goods, etc.) model quest_inventory_type { id String @id @default(cuid()) name String @unique code String @unique description String? is_active Boolean @default(true) sort_order Int @default(0) created_at DateTime @default(now()) updated_at DateTime @updatedAt plant_assignments quest_inventory_plant[] @@map("quest_inventory_type") } // Junction: which inventory types are available at which plants model quest_inventory_plant { id String @id @default(cuid()) quest_plant_id String quest_inventory_type_id String is_active Boolean @default(true) created_at DateTime @default(now()) plant quest_plant @relation(fields: [quest_plant_id], references: [id], onDelete: Cascade) inventory_type quest_inventory_type @relation(fields: [quest_inventory_type_id], references: [id], onDelete: Cascade) @@unique([quest_plant_id, quest_inventory_type_id]) @@index([quest_plant_id]) @@index([quest_inventory_type_id]) @@map("quest_inventory_plant") } // Tracking processed order acknowledgement emails to prevent duplicates model quest_processed_order_acknowledgement_email { id String @id @default(cuid()) epicor_order_num Int epicor_cust_id String epicor_po_num String? sent_at DateTime @default(now()) quest_company_id String? @@unique([epicor_order_num, epicor_cust_id]) @@index([epicor_cust_id]) @@index([sent_at]) @@map("quest_processed_order_acknowledgement_email") } // ============================================================================= // SHIPMENT REQUEST DOMAIN // ============================================================================= // Shipment request header model ship_request { id String @id @default(cuid()) auth_user_id String quest_company_id String ship_to_address String order_number String? po_number String? release_number String? pickup_date DateTime? instructions String? email_recipients String[] // Array of email addresses is_submitted Boolean @default(false) submitted_at DateTime? is_cancelled Boolean @default(false) cancelled_at DateTime? cancelled_by String? last_cart_activity DateTime @default(now()) created_at DateTime @default(now()) updated_at DateTime @updatedAt details ship_request_detail[] @@index([auth_user_id]) @@index([quest_company_id]) @@index([is_submitted]) @@index([is_cancelled]) @@map("ship_request") } // Shipment request line items model ship_request_detail { id String @id @default(cuid()) ship_request_id String part_num String lot_num String? plant String warehouse String? quantity Float notes String? created_at DateTime @default(now()) request ship_request @relation(fields: [ship_request_id], references: [id], onDelete: Cascade) @@index([ship_request_id]) @@map("ship_request_detail") } // ============================================================================= // ALLOCATION REQUEST DOMAIN // ============================================================================= // Coil allocation request header model alloc_request { id String @id @default(cuid()) auth_user_id String quest_company_id String job_number String ship_to_address String order_number String? po_number String? release_number String? pickup_date DateTime? instructions String? email_recipients String[] // Array of email addresses is_submitted Boolean @default(false) submitted_at DateTime? is_cancelled Boolean @default(false) cancelled_at DateTime? cancelled_by String? last_cart_activity DateTime @default(now()) created_at DateTime @default(now()) updated_at DateTime @updatedAt details alloc_request_detail[] @@index([auth_user_id]) @@index([quest_company_id]) @@index([job_number]) @@index([is_submitted]) @@map("alloc_request") } // Allocation request line items model alloc_request_detail { id String @id @default(cuid()) alloc_request_id String part_num String lot_num String? coil_number String? quantity Float notes String? created_at DateTime @default(now()) request alloc_request @relation(fields: [alloc_request_id], references: [id], onDelete: Cascade) @@index([alloc_request_id]) @@map("alloc_request_detail") } // Allocation plant assignments (which plants can allocate) model alloc_plant { id String @id @default(cuid()) plant_code String @unique plant_name String is_active Boolean @default(true) created_at DateTime @default(now()) updated_at DateTime @updatedAt @@map("alloc_plant") } // ============================================================================= // INVOICE UPLOAD DOMAIN // ============================================================================= // Invoice upload batch status model inv_upload_status { id String @id @default(cuid()) name String @unique description String? sort_order Int @default(0) uploads inv_upload[] @@map("inv_upload_status") } // Invoice upload batch model inv_upload { id String @id @default(cuid()) auth_user_id String original_filename String storage_filename String file_path String file_size Int total_pages Int? inv_upload_status_id String upload_notes String? created_at DateTime @default(now()) processed_at DateTime? status inv_upload_status @relation(fields: [inv_upload_status_id], references: [id]) entries inv_upload_entry[] @@index([auth_user_id]) @@index([inv_upload_status_id]) @@index([created_at]) @@map("inv_upload") } // Individual invoice entries from upload model inv_upload_entry { id String @id @default(cuid()) inv_upload_id String invoice_number String epicor_cust_id String? quest_company_id String? job_number String? page_number Int? storage_filename String? file_path String? is_ignored Boolean @default(false) is_replaced Boolean @default(false) send_error String? created_at DateTime @default(now()) sent_at DateTime? upload inv_upload @relation(fields: [inv_upload_id], references: [id], onDelete: Cascade) @@index([inv_upload_id]) @@index([invoice_number]) @@index([epicor_cust_id]) @@index([quest_company_id]) @@map("inv_upload_entry") } // ============================================================================= // FINANCE/AP CHECK PROCESSING DOMAIN // ============================================================================= // AP check processing status definitions model finance_ap_check_processing_status { id String @id @default(cuid()) name String @unique description String? sort_order Int @default(0) batches finance_ap_check_processing_batch[] @@map("finance_ap_check_processing_status") } // AP check processing batch tracking model finance_ap_check_processing_batch { id String @id @default(cuid()) auth_user_id String group_id String original_filename String generated_csv_filename String? generated_csv_path String? finance_ap_check_processing_status_id String error_message String? record_count Int @default(0) created_at DateTime @default(now()) processed_at DateTime? status finance_ap_check_processing_status @relation(fields: [finance_ap_check_processing_status_id], references: [id]) @@index([auth_user_id]) @@index([group_id]) @@index([finance_ap_check_processing_status_id]) @@map("finance_ap_check_processing_batch") } // ============================================================================= // PAINT SCHEDULE DOMAIN // ============================================================================= // Paint line types model paint_line_type { id String @id @default(cuid()) name String @unique description String? lines paint_line[] @@map("paint_line_type") } // Paint plants model paint_plant { id String @id @default(cuid()) name String @unique code String @unique is_active Boolean @default(true) plant_lines paint_plant_line[] @@map("paint_plant") } // Paint lines model paint_line { id String @id @default(cuid()) name String @unique paint_line_type_id String is_active Boolean @default(true) line_type paint_line_type @relation(fields: [paint_line_type_id], references: [id]) plant_lines paint_plant_line[] schedules paint_schedule[] @@map("paint_line") } // Junction: which lines are at which plants model paint_plant_line { id String @id @default(cuid()) paint_plant_id String paint_line_id String is_active Boolean @default(true) plant paint_plant @relation(fields: [paint_plant_id], references: [id], onDelete: Cascade) line paint_line @relation(fields: [paint_line_id], references: [id], onDelete: Cascade) @@unique([paint_plant_id, paint_line_id]) @@map("paint_plant_line") } // Paint schedule entries model paint_schedule { id String @id @default(cuid()) paint_line_id String job_id String? part_description String? customer String? quantity Int? estimated_run_time Float? start_time DateTime? notes String? is_complete Boolean @default(false) line_speed Float? created_at DateTime @default(now()) updated_at DateTime @updatedAt line paint_line @relation(fields: [paint_line_id], references: [id]) @@index([paint_line_id]) @@index([start_time]) @@index([is_complete]) @@map("paint_schedule") } // ============================================================================= // DOCUMENTATION/KNOWLEDGE BASE DOMAIN // ============================================================================= // Documentation categories model doc_category { id String @id @default(cuid()) name String @unique description String? icon String? sort_order Int @default(0) is_active Boolean @default(true) created_at DateTime @default(now()) updated_at DateTime @updatedAt articles doc_article[] @@map("doc_category") } // Documentation articles model doc_article { id String @id @default(cuid()) title String content String doc_category_id String is_published Boolean @default(false) sort_order Int @default(0) created_at DateTime @default(now()) updated_at DateTime @updatedAt created_by String? updated_by String? category doc_category @relation(fields: [doc_category_id], references: [id]) permission_groups doc_article_permission_group[] @@index([doc_category_id]) @@index([is_published]) @@map("doc_article") } // Junction: which permission groups can see which articles model doc_article_permission_group { id String @id @default(cuid()) doc_article_id String auth_permission_group_id String created_at DateTime @default(now()) article doc_article @relation(fields: [doc_article_id], references: [id], onDelete: Cascade) @@unique([doc_article_id, auth_permission_group_id]) @@map("doc_article_permission_group") } // ============================================================================= // WAVE/EDI PROCESSING DOMAIN // ============================================================================= // Wave process definitions model wave_process { id String @id @default(cuid()) name String @unique description String? is_enabled Boolean @default(true) schedule_cron String? created_at DateTime @default(now()) updated_at DateTime @updatedAt history wave_process_history[] @@map("wave_process") } // Wave process execution history model wave_process_history { id String @id @default(cuid()) wave_process_id String started_at DateTime @default(now()) completed_at DateTime? status String @default("running") // running, success, failed result_message String? error_message String? process wave_process @relation(fields: [wave_process_id], references: [id]) @@index([wave_process_id]) @@index([started_at]) @@index([status]) @@map("wave_process_history") } // ============================================================================= // LTS (LONG-TERM SCHEDULER) TASK DOMAIN // ============================================================================= // Task types model lts_task_type { id String @id @default(cuid()) name String @unique description String? tasks lts_task[] @@map("lts_task_type") } // Scheduled tasks model lts_task { id String @id @default(cuid()) lts_task_type_id String name String description String? command String is_enabled Boolean @default(true) created_at DateTime @default(now()) updated_at DateTime @updatedAt task_type lts_task_type @relation(fields: [lts_task_type_id], references: [id]) schedules lts_task_schedule[] executions lts_task_execution[] @@map("lts_task") } // Task schedule definitions model lts_task_schedule { id String @id @default(cuid()) lts_task_id String schedule_type String // cron, interval, daily, weekly, monthly schedule_value String // cron expression or interval value timezone String @default("UTC") is_active Boolean @default(true) next_run_at DateTime? created_at DateTime @default(now()) updated_at DateTime @updatedAt task lts_task @relation(fields: [lts_task_id], references: [id], onDelete: Cascade) @@index([lts_task_id]) @@index([next_run_at]) @@map("lts_task_schedule") } // Task execution log model lts_task_execution { id String @id @default(cuid()) lts_task_id String started_at DateTime @default(now()) completed_at DateTime? status String @default("running") // running, success, failed output String? error_message String? exit_code Int? task lts_task @relation(fields: [lts_task_id], references: [id]) @@index([lts_task_id]) @@index([started_at]) @@index([status]) @@map("lts_task_execution") }