Implements docs/RECEIPT_CAPTURE_SPEC.md §1, the legacy "Editor" replacement, on top of the single-movement capture from plan step 6. No new abilities: batching and resolving are both capturing. - outstanding (legacy NOPAGO): capture flag, ?outstanding= filter, and POST /billing/:id/resolve-outstanding (gated ledger:create, not ledger:void — resolving completes a capture rather than reversing one). Outstanding rows are excluded from every balance aggregate, matching the legacy SALDOS ULTIMO 0 query's HAVING NOPAGO = 0, but still count in the movement browser's filtered totals. - POST /billing/batch: many customers' receipts against one check, in one $transaction. Deliberately not a persisted batch entity — checkNumber is already a column and grouping by it answers every legacy by-check query. - GET /billing/by-check + a cheque-count report, replacing REPORTE CHEQUE COUNT / REPORTE POR CHEQUE / EDITA CHEQUE ALF|COUNT|NUM. Print, PDF, CSV and XLSX come free from the existing /reportes/:slug machinery. - Web: /estado-cuenta/lote (the Editor screen, with live reconciliation against the physical check amount), an "Estado de pago" filter, a "sin fondos" row tag and a Resolver dialog, plus a top-level "Captura" nav entry. Integration seam for the OCR auto-capture module (spec §2), which is required to post through createBatch rather than writing Transaction rows itself: items[i] maps to lines[i] so postedTransactionId can be zipped back on; opts.refs[i] stamps captureRef with a duplicate-post guard that a voided row deliberately does not block; opts.source is service-level only, so an HTTP client cannot label hand-keyed rows as machine-captured. captureSource/captureRef are nullable so the 40,136 migrated rows stay NULL rather than being mislabelled. Fixes two pre-existing bugs found while building this: - statement() filtered legacySourceTable with `notIn`, which compiles to SQL NOT IN — and `NULL NOT IN (...)` is NULL, so every app-captured movement was invisible on the customer statement (438 rows in the movement browser vs 392 on the statement) while showing everywhere else. This would have made the whole capture feature look broken. - The balances count query omitted the void filter its own page query applied, so the total disagreed with the rows. Nav highlighting now resolves by longest match; the previous first-startsWith logic lit up both the parent and any nested entry. Verified end-to-end against the dev DB, API and browser; all test rows removed afterwards. Also corrects RESUME.md, which documented the dev ports as :3001/:3000 — they are :4501/:4500, from the env files. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
637 lines
21 KiB
Plaintext
637 lines
21 KiB
Plaintext
// Unified customer / insurance / utilities data model.
|
|
// See C:\Users\ricar\.claude\plans\logical-yawning-tome.md for the migration
|
|
// plan this schema implements (source: UTILITIES.accdb, SEGUROS 16_be.mdb,
|
|
// SCOTHIA.mdb). Every model that originates from a legacy Access table
|
|
// carries legacySource* provenance columns so migrated rows are traceable
|
|
// back to their Access original and the ETL can be re-run idempotently.
|
|
|
|
generator client {
|
|
provider = "prisma-client-js"
|
|
output = "../generated/client"
|
|
}
|
|
|
|
datasource db {
|
|
provider = "mysql"
|
|
url = env("DATABASE_URL")
|
|
}
|
|
|
|
enum Currency {
|
|
USD
|
|
MXN
|
|
}
|
|
|
|
enum TransactionDomain {
|
|
UTILITY
|
|
INSURANCE
|
|
TRUST
|
|
}
|
|
|
|
/// How a ledger row entered the system. Every capture path funnels through
|
|
/// BillingService (single write path, single audit trail); this records which
|
|
/// one, so an auto-captured receipt is auditable without joining the statement
|
|
/// tables. `OCR` is reserved for the statement auto-capture pipeline
|
|
/// (docs/RECEIPT_CAPTURE_SPEC.md §2), which posts through the same batch path
|
|
/// as hand-keyed check batches.
|
|
enum TransactionCaptureSource {
|
|
MANUAL
|
|
BATCH
|
|
OCR
|
|
}
|
|
|
|
enum ServiceKind {
|
|
WATER
|
|
ELECTRIC
|
|
GAS
|
|
CABLE
|
|
PROPERTY_TAX
|
|
FEDERAL_ZONE
|
|
ALARM
|
|
OTHER
|
|
}
|
|
|
|
/// Ordered access tier (rank): ADMIN > MANAGER > STAFF > VIEWER. VIEWER is the
|
|
/// read-only role; STAFF and above can write. Enforced by the API's ability
|
|
/// matrix (apps/api/src/auth/abilities.ts), not by the enum itself.
|
|
enum UserRole {
|
|
ADMIN
|
|
MANAGER
|
|
STAFF
|
|
VIEWER
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Identity — the actual point of the project: one customer record shared by
|
|
// both business lines.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
model Customer {
|
|
id String @id @default(uuid())
|
|
name String
|
|
// Which legacy table `name` actually came from. Null = DATGRAL.NOMBRE, the
|
|
// normal case. Anything else means DATGRAL's name was blank and the name was
|
|
// recovered from a secondary table (see migration/transform_customers.py),
|
|
// so staff can tell a reconstructed name from an original one.
|
|
nameSource String?
|
|
// True when `name` is the "(SIN NOMBRE)" placeholder. Denormalized so lists
|
|
// can sort nameless records last — ordering by `name` alone puts them first,
|
|
// since "(" sorts before every letter.
|
|
nameMissing Boolean @default(false)
|
|
addressLine1 String?
|
|
addressLine2 String?
|
|
city String?
|
|
state String?
|
|
zipCode String?
|
|
country String?
|
|
phone String?
|
|
mobile String?
|
|
fax String?
|
|
email String?
|
|
notes String? @db.Text
|
|
identificationType String?
|
|
identificationNumber String?
|
|
identificationExpiration DateTime?
|
|
customerSince DateTime?
|
|
status Boolean @default(true)
|
|
minimumBalance Decimal? @db.Decimal(12, 2)
|
|
feeAmount Decimal? @db.Decimal(12, 2)
|
|
preferredCurrency Currency @default(USD)
|
|
// Soft-delete marker. Distinct from `status` (a legacy business flag): a
|
|
// non-null archivedAt hides the row from default lists while preserving it
|
|
// and its legacy provenance. Never hard-delete migrated data.
|
|
archivedAt DateTime?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
legacyRefs CustomerLegacyRef[]
|
|
properties Property[]
|
|
policies Policy[]
|
|
vehicles Vehicle[]
|
|
transactions Transaction[]
|
|
|
|
@@map("customers")
|
|
}
|
|
|
|
/// Generalizes the old app's customer_mapping table: one row per legacy
|
|
/// record folded into this customer, from either source system.
|
|
model CustomerLegacyRef {
|
|
id String @id @default(uuid())
|
|
customerId String
|
|
customer Customer @relation(fields: [customerId], references: [id])
|
|
sourceSystem String // "utilities" | "insurance"
|
|
sourceTable String // e.g. "DATGRAL", "COBRO3"
|
|
legacyId String // stringified legacy id (source columns are often DOUBLE)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@unique([sourceSystem, sourceTable, legacyId])
|
|
@@map("customer_legacy_refs")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Insurance domain
|
|
// ---------------------------------------------------------------------------
|
|
|
|
model InsuranceProvider {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
policies Policy[]
|
|
|
|
@@map("insurance_providers")
|
|
}
|
|
|
|
model PolicyType {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
shortDescription String?
|
|
policies Policy[]
|
|
|
|
@@map("policy_types")
|
|
}
|
|
|
|
/// Consolidates INCENDIO/MULT/M EMPR/all auto-table variants/LICENCIAS into
|
|
/// one table with a policyType discriminator, instead of one Access table
|
|
/// per line of business.
|
|
model Policy {
|
|
id String @id @default(uuid())
|
|
policyNumber String
|
|
customerId String
|
|
customer Customer @relation(fields: [customerId], references: [id])
|
|
policyTypeId String?
|
|
policyType PolicyType? @relation(fields: [policyTypeId], references: [id])
|
|
insuranceProviderId String?
|
|
insuranceProvider InsuranceProvider? @relation(fields: [insuranceProviderId], references: [id])
|
|
agentName String?
|
|
policyDate DateTime?
|
|
policyFrom DateTime?
|
|
policyTo DateTime?
|
|
coveragePeriodDays Int? @default(365)
|
|
netPremium Decimal? @db.Decimal(12, 2)
|
|
policyFee Decimal? @db.Decimal(12, 2)
|
|
brokerFee Decimal? @db.Decimal(12, 2)
|
|
commission Decimal? @db.Decimal(12, 2)
|
|
total Decimal? @db.Decimal(12, 2)
|
|
currency Currency @default(MXN)
|
|
observations String? @db.Text
|
|
notes String? @db.Text
|
|
coveragesJson Json?
|
|
endorsement Boolean @default(false)
|
|
liquidated Boolean @default(false)
|
|
liquidationNumber String?
|
|
liquidationDate DateTime?
|
|
// Soft-delete marker (see Customer.archivedAt). Never hard-delete migrated
|
|
// policy data; archiving hides it from default lists.
|
|
archivedAt DateTime?
|
|
legacySourceDb String?
|
|
legacySourceTable String?
|
|
legacyId String?
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
|
|
installments PolicyPaymentInstallment[]
|
|
vehicles Vehicle[]
|
|
insuredDrivers InsuredDriver[]
|
|
beneficiaries PolicyBeneficiary[]
|
|
claims Claim[]
|
|
documents PolicyDocument[]
|
|
properties Property[]
|
|
renewalNotices RenewalNotice[]
|
|
|
|
@@unique([legacySourceDb, legacySourceTable, legacyId])
|
|
@@index([policyNumber])
|
|
@@map("policies")
|
|
}
|
|
|
|
enum RenewalNoticeChannel {
|
|
MAIL
|
|
EMAIL
|
|
}
|
|
|
|
/// Replaces the legacy `CONTROL <ramo> RENEW[2/3] X MES` reports — a
|
|
/// per-batch printed checklist of who'd been sent which reminder. One row
|
|
/// per notice generation actually sent for a policy, so "who got a 1st/2nd/
|
|
/// 3rd notice and when" is a query instead of a paper trail. See
|
|
/// docs/RENEWAL_NOTICES.md for the legacy report chain this replaces.
|
|
model RenewalNotice {
|
|
id String @id @default(uuid())
|
|
policyId String
|
|
policy Policy @relation(fields: [policyId], references: [id])
|
|
// 1 = first notice (bare RENEW), 2 = RENEW2, 3 = RENEW3 in the legacy naming.
|
|
generation Int
|
|
channel RenewalNoticeChannel @default(MAIL)
|
|
sentAt DateTime?
|
|
sentById String?
|
|
notes String? @db.Text
|
|
createdAt DateTime @default(now())
|
|
|
|
// One row per generation per policy — matches the legacy's 1st/2nd/3rd
|
|
// notice cadence; re-running the same generation for a policy updates it
|
|
// rather than duplicating a log entry.
|
|
@@unique([policyId, generation])
|
|
@@map("renewal_notices")
|
|
}
|
|
|
|
/// Unpivots the 4 hardcoded payment-installment columns found on every
|
|
/// legacy policy table (1ER PAGO/FECHA PAGO/NO CHEQUE, ...2, ...3, ...4).
|
|
model PolicyPaymentInstallment {
|
|
id String @id @default(uuid())
|
|
policyId String
|
|
policy Policy @relation(fields: [policyId], references: [id])
|
|
sequence Int
|
|
amount Decimal? @db.Decimal(12, 2)
|
|
currency Currency @default(MXN)
|
|
dueDate DateTime?
|
|
paidDate DateTime?
|
|
checkNumber String?
|
|
isCash Boolean @default(false)
|
|
|
|
@@map("policy_payment_installments")
|
|
}
|
|
|
|
/// Unpivots MCA2's 3 hardcoded vehicle slots (and the single-vehicle auto
|
|
/// tables) into one row per vehicle.
|
|
model Vehicle {
|
|
id String @id @default(uuid())
|
|
customerId String?
|
|
customer Customer? @relation(fields: [customerId], references: [id])
|
|
policyId String?
|
|
policy Policy? @relation(fields: [policyId], references: [id])
|
|
make String?
|
|
model String?
|
|
modelYear String?
|
|
bodyType String?
|
|
engineNumber String?
|
|
licensePlate String?
|
|
vinNumber String?
|
|
stateCode String?
|
|
notes String? @db.Text
|
|
// One legacy policy row can carry up to 3 vehicles, so they share a
|
|
// legacyId (the source row number) — provenance is NOT unique per vehicle.
|
|
// Sync rebuilds legacy vehicles by scoped delete + reinsert instead of upsert.
|
|
legacySourceTable String?
|
|
legacyId String?
|
|
|
|
@@map("vehicles")
|
|
}
|
|
|
|
/// Unpivots the repeated named-insured/license columns in MCA2/LICENCIAS.
|
|
model InsuredDriver {
|
|
id String @id @default(uuid())
|
|
policyId String
|
|
policy Policy @relation(fields: [policyId], references: [id])
|
|
fullName String?
|
|
birthDate DateTime?
|
|
sex String?
|
|
occupation String?
|
|
licenseNumber String?
|
|
licenseState String?
|
|
|
|
@@map("insured_drivers")
|
|
}
|
|
|
|
/// From BENEF — already a clean child table in the source data.
|
|
model PolicyBeneficiary {
|
|
id String @id @default(uuid())
|
|
policyId String
|
|
policy Policy @relation(fields: [policyId], references: [id])
|
|
name String?
|
|
address String?
|
|
phone String?
|
|
email String?
|
|
|
|
@@map("policy_beneficiaries")
|
|
}
|
|
|
|
/// From DATOS (siniestros).
|
|
model Claim {
|
|
id String @id @default(uuid())
|
|
policyId String
|
|
policy Policy @relation(fields: [policyId], references: [id])
|
|
claimType String?
|
|
incidentDate DateTime?
|
|
reportedDate DateTime?
|
|
description String? @db.Text
|
|
adjusterId String?
|
|
adjuster Adjuster? @relation(fields: [adjusterId], references: [id])
|
|
claimedAmount Decimal? @db.Decimal(12, 2)
|
|
settledAmount Decimal? @db.Decimal(12, 2)
|
|
settlementDate DateTime?
|
|
checkNumber String?
|
|
resolved Boolean @default(false)
|
|
resolutionNotes String? @db.Text
|
|
|
|
@@map("claims")
|
|
}
|
|
|
|
/// From AJUSTADORES / AJUSTADORESATLAS.
|
|
model Adjuster {
|
|
id String @id @default(uuid())
|
|
company String?
|
|
city String?
|
|
name String?
|
|
phone String?
|
|
beeper String?
|
|
claims Claim[]
|
|
|
|
@@map("adjusters")
|
|
}
|
|
|
|
/// Extracted LONGBINARY blobs from the policy tables — file lives in object
|
|
/// storage, only the pointer + type lives here.
|
|
model PolicyDocument {
|
|
id String @id @default(uuid())
|
|
policyId String
|
|
policy Policy @relation(fields: [policyId], references: [id])
|
|
documentType String
|
|
storageKey String
|
|
originalColumn String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@map("policy_documents")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Utilities domain
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// From DATMEX — shared with the insurance domain so a property can carry
|
|
/// both a home-insurance policy and utility service enrollments.
|
|
model Property {
|
|
id String @id @default(uuid())
|
|
customerId String
|
|
customer Customer @relation(fields: [customerId], references: [id])
|
|
policyId String?
|
|
policy Policy? @relation(fields: [policyId], references: [id])
|
|
addressLine1 String?
|
|
addressLine2 String?
|
|
phone1 String?
|
|
phone2 String?
|
|
phone3 String?
|
|
zone String?
|
|
// Soft-delete marker (see Customer.archivedAt).
|
|
archivedAt DateTime?
|
|
legacySourceTable String?
|
|
legacyId String?
|
|
createdAt DateTime @default(now())
|
|
|
|
services PropertyService[]
|
|
documents ServiceDocument[]
|
|
trustAccount TrustAccount?
|
|
|
|
@@unique([legacySourceTable, legacyId])
|
|
@@map("properties")
|
|
}
|
|
|
|
/// Unpivots DATMEX's inline service columns and PROFILE's enrollment flags
|
|
/// into one row per enrolled service per property.
|
|
model PropertyService {
|
|
id String @id @default(uuid())
|
|
propertyId String
|
|
property Property @relation(fields: [propertyId], references: [id])
|
|
kind ServiceKind
|
|
accountNumber String?
|
|
meterNumber String?
|
|
route String?
|
|
dueDay String?
|
|
active Boolean @default(true)
|
|
notes String? @db.Text
|
|
|
|
@@map("property_services")
|
|
}
|
|
|
|
model ServiceDocument {
|
|
id String @id @default(uuid())
|
|
propertyId String
|
|
property Property @relation(fields: [propertyId], references: [id])
|
|
documentType String
|
|
storageKey String
|
|
createdAt DateTime @default(now())
|
|
|
|
@@map("service_documents")
|
|
}
|
|
|
|
/// From TRUSTVENCE.
|
|
model TrustAccount {
|
|
id String @id @default(uuid())
|
|
propertyId String @unique
|
|
property Property @relation(fields: [propertyId], references: [id])
|
|
bankName String?
|
|
trustNumber String?
|
|
bankFee Decimal? @db.Decimal(12, 2)
|
|
dueDate1 DateTime?
|
|
dueDate2 DateTime?
|
|
|
|
@@map("trust_accounts")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared financial ledger — one office, one set of books.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// ES/EN transaction-type lookup, carried over from the old schema.
|
|
model TypeTransaction {
|
|
id String @id @default(uuid())
|
|
nameEn String
|
|
nameEs String?
|
|
isService Boolean @default(false)
|
|
transactions Transaction[]
|
|
|
|
@@map("type_transactions")
|
|
}
|
|
|
|
/// Unifies utilities' EFECTIVO/EFECTIVO FM3/EFECTIVO_BACKUP/FEE ANUAL/
|
|
/// datos2/fee15/billing/CHEQUE FM3/IVA 2015 and insurance's EFECTIVO.
|
|
model Transaction {
|
|
id String @id @default(uuid())
|
|
customerId String
|
|
customer Customer @relation(fields: [customerId], references: [id])
|
|
domain TransactionDomain
|
|
typeId String?
|
|
type TypeTransaction? @relation(fields: [typeId], references: [id])
|
|
transactionDate DateTime
|
|
period String?
|
|
reference String?
|
|
amount Decimal @db.Decimal(12, 2)
|
|
currency Currency @default(MXN)
|
|
exchangeRate Decimal? @db.Decimal(10, 4)
|
|
checkNumber String?
|
|
message String? @db.Text
|
|
outstanding Boolean @default(false)
|
|
/// How this row was captured. NULL = migrated from Access (the legacy*
|
|
/// columns below say which table). Set explicitly on everything the app
|
|
/// books, so an OCR-posted receipt is distinguishable from a hand-keyed one
|
|
/// without joining the statement tables.
|
|
captureSource TransactionCaptureSource?
|
|
/// Back-pointer to the artifact that produced this row — a
|
|
/// `StatementDocument.id` for OCR captures (see RECEIPT_CAPTURE_SPEC §2).
|
|
/// Unique among live rows via the app's duplicate guard, not a DB constraint,
|
|
/// because a voided row must not block a corrected re-post of the same
|
|
/// document.
|
|
captureRef String?
|
|
// Append + void: booked rows are never edited or hard-deleted. A non-null
|
|
// voidedAt reverses the movement — it MUST be excluded from every balance
|
|
// and total (SUM/count) so a voided amount stops affecting the books.
|
|
voidedAt DateTime?
|
|
voidedById String?
|
|
legacySourceDb String?
|
|
legacySourceTable String?
|
|
legacyId String?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@index([customerId, transactionDate])
|
|
// By-check reconciliation (billing.byCheck / the cheque-count report) looks
|
|
// rows up by check number alone — the legacy EDITA CHEQUE COUNT lookup.
|
|
@@index([checkNumber])
|
|
// Drives the duplicate-post guard in BillingService.createBatch.
|
|
@@index([captureRef])
|
|
@@unique([legacySourceDb, legacySourceTable, legacyId])
|
|
@@map("transactions")
|
|
}
|
|
|
|
/// From TIPO HIST.
|
|
model ExchangeRate {
|
|
id String @id @default(uuid())
|
|
rate Decimal @db.Decimal(10, 4)
|
|
effectiveDate DateTime
|
|
effectiveHour DateTime?
|
|
|
|
@@map("exchange_rates")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Company bank register (SCOTHIA.mdb) — the office's own operating account,
|
|
// deliberately separate from customer-facing Transaction records.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// From TABLA RAMODOS ("ramo" = line of business).
|
|
model BusinessLineCategory {
|
|
id String @id @default(uuid())
|
|
name String @unique
|
|
bankTransactions BankTransaction[]
|
|
|
|
@@map("business_line_categories")
|
|
}
|
|
|
|
/// Unifies SCOTHIA's DATOS E (egresos) / DATOS I (ingresos) into one
|
|
/// signed-amount table: income positive, expense negative.
|
|
model BankTransaction {
|
|
id String @id @default(uuid())
|
|
transactionDate DateTime
|
|
transactionType String?
|
|
reference String?
|
|
concept String?
|
|
amount Decimal @db.Decimal(12, 2)
|
|
categoryId String?
|
|
category BusinessLineCategory? @relation(fields: [categoryId], references: [id])
|
|
cleared Boolean @default(false)
|
|
transferred Boolean @default(false)
|
|
notes String? @db.Text
|
|
amountInWords String?
|
|
// Append + void (see Transaction.voidedAt): excluded from income/expense/net.
|
|
voidedAt DateTime?
|
|
voidedById String?
|
|
legacySourceTable String?
|
|
legacyId String?
|
|
|
|
@@unique([legacySourceTable, legacyId])
|
|
@@map("bank_transactions")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Admin / shared
|
|
// ---------------------------------------------------------------------------
|
|
|
|
model User {
|
|
id String @id @default(uuid())
|
|
name String
|
|
email String @unique
|
|
passwordHash String
|
|
role UserRole @default(STAFF)
|
|
active Boolean @default(true)
|
|
createdAt DateTime @default(now())
|
|
updatedAt DateTime @updatedAt
|
|
activityLogs ActivityLog[]
|
|
|
|
@@map("users")
|
|
}
|
|
|
|
model ActivityLog {
|
|
id String @id @default(uuid())
|
|
userId String?
|
|
user User? @relation(fields: [userId], references: [id])
|
|
event String
|
|
level String
|
|
message Json?
|
|
createdAt DateTime @default(now())
|
|
|
|
@@map("activity_logs")
|
|
}
|
|
|
|
model EmailTemplate {
|
|
id String @id @default(uuid())
|
|
name String
|
|
subject String
|
|
templateSource String @db.Text
|
|
|
|
@@map("email_templates")
|
|
}
|
|
|
|
model EmailCampaign {
|
|
id String @id @default(uuid())
|
|
campaignName String
|
|
subject String?
|
|
body String? @db.Text
|
|
status String @default("in_progress")
|
|
emailSentCount Int @default(0)
|
|
createdAt DateTime @default(now())
|
|
|
|
@@map("email_campaigns")
|
|
}
|
|
|
|
model EmailLog {
|
|
id String @id @default(uuid())
|
|
customerId String?
|
|
emailAddress String?
|
|
emailType String?
|
|
requestBody String? @db.Text
|
|
responseBody String? @db.Text
|
|
sentAt DateTime @default(now())
|
|
|
|
@@map("email_log")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Admin database operations (Operaciones): backup / restore / re-import / sync.
|
|
// Each long-running op is one OpsJob row so the web UI can poll status + tail
|
|
// the captured log. Rows are the audit trail for who ran a destructive op.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
enum OpsJobKind {
|
|
BACKUP
|
|
RESTORE
|
|
REIMPORT
|
|
SYNC
|
|
}
|
|
|
|
enum OpsJobStatus {
|
|
RUNNING
|
|
SUCCESS
|
|
FAILED
|
|
}
|
|
|
|
model OpsJob {
|
|
id String @id @default(uuid())
|
|
kind OpsJobKind
|
|
status OpsJobStatus @default(RUNNING)
|
|
// Combined stdout+stderr of the spawned process, appended as it runs.
|
|
log String @db.LongText
|
|
// Op-specific inputs (e.g. the backup filename a RESTORE targets). No FK on
|
|
// createdById — the actor id is stored flat, like activity_logs' userId use.
|
|
params Json?
|
|
createdById String?
|
|
startedAt DateTime @default(now())
|
|
finishedAt DateTime?
|
|
|
|
@@index([status])
|
|
@@index([startedAt])
|
|
@@map("ops_jobs")
|
|
}
|