Initial local backup snapshot

This commit is contained in:
Selecta Keke 2026-06-24 00:02:55 -03:00
commit acd9e14ba5
367 changed files with 118038 additions and 0 deletions

627
drizzle/schema.ts Normal file
View file

@ -0,0 +1,627 @@
import { int, mysqlEnum, mysqlTable, text, timestamp, varchar, boolean } from "drizzle-orm/mysql-core";
import { associationGeoPrecisions, associationGeoSources } from "@shared/associationGeo";
import { associationThematicValues } from "@shared/associationThematics";
/**
* Core user table backing auth flow.
*/
export const users = mysqlTable("users", {
id: int("id").autoincrement().primaryKey(),
openId: varchar("openId", { length: 64 }).notNull().unique(),
name: text("name"),
email: varchar("email", { length: 320 }),
passwordHash: varchar("passwordHash", { length: 255 }),
loginMethod: varchar("loginMethod", { length: 64 }),
role: mysqlEnum("role", ["user", "accueil", "service_terrain", "logistique_controle", "admin", "directrice", "super_admin"]).default("user").notNull(),
canManageLogistics: boolean("canManageLogistics").default(false).notNull(),
delegatedSalleSignerUserId: int("delegatedSalleSignerUserId"),
isActive: boolean("isActive").default(true).notNull(),
failedLoginAttempts: int("failedLoginAttempts").default(0).notNull(),
lockedUntil: timestamp("lockedUntil"),
mfaEnabled: boolean("mfaEnabled").default(false).notNull(),
mfaMethod: varchar("mfaMethod", { length: 32 }),
mfaChallengeToken: varchar("mfaChallengeToken", { length: 96 }),
mfaCodeHash: varchar("mfaCodeHash", { length: 255 }),
mfaCodeExpiresAt: timestamp("mfaCodeExpiresAt"),
mfaCodeAttempts: int("mfaCodeAttempts").default(0).notNull(),
mfaTotpSecretEncrypted: varchar("mfaTotpSecretEncrypted", { length: 512 }),
mfaTotpPendingSecretEncrypted: varchar("mfaTotpPendingSecretEncrypted", { length: 512 }),
deletionRequestedAt: timestamp("deletionRequestedAt"),
purgeScheduledAt: timestamp("purgeScheduledAt"),
purgedAt: timestamp("purgedAt"),
legalHold: boolean("legalHold").default(false).notNull(),
legalHoldReason: text("legalHoldReason"),
privacyConsentVersion: varchar("privacyConsentVersion", { length: 32 }),
privacyConsentAcceptedAt: timestamp("privacyConsentAcceptedAt"),
privacyConsentContext: varchar("privacyConsentContext", { length: 64 }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
lastSignedIn: timestamp("lastSignedIn").defaultNow().notNull(),
});
export type User = typeof users.$inferSelect;
export type InsertUser = typeof users.$inferInsert;
/**
* Association profile table - stores complete association information
*/
export const associations = mysqlTable("associations", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull().unique(),
sourceDirectoryEntryId: int("sourceDirectoryEntryId"),
// Basic information
nomAssociation: varchar("nomAssociation", { length: 255 }).notNull(),
siret: varchar("siret", { length: 14 }),
rna: varchar("rna", { length: 10 }), // Numéro RNA (W + 9 chiffres)
thematique: text("thematique"),
// Address
adresse: text("adresse"),
codePostal: varchar("codePostal", { length: 10 }),
ville: varchar("ville", { length: 100 }),
// Contact
telephone: varchar("telephone", { length: 20 }),
emailContact: varchar("emailContact", { length: 320 }),
siteWeb: varchar("siteWeb", { length: 255 }),
facebookUrl: varchar("facebookUrl", { length: 255 }),
instagramUrl: varchar("instagramUrl", { length: 255 }),
// Legal information
dateCreation: timestamp("dateCreation"),
objetAssociation: text("objetAssociation"),
statutJuridique: mysqlEnum("statutJuridique", [
"association_loi_1901",
"association_reconnue_utilite_publique",
"fondation",
"autre"
]).default("association_loi_1901"),
// Representative
nomRepresentant: varchar("nomRepresentant", { length: 255 }),
fonctionRepresentant: varchar("fonctionRepresentant", { length: 100 }),
gouvernance: text("gouvernance"),
// Profile completion and status
profileComplete: boolean("profileComplete").default(false),
isActive: boolean("isActive").default(true).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Association = typeof associations.$inferSelect;
export type InsertAssociation = typeof associations.$inferInsert;
/**
* Imported association directory - source of truth from the Savanes registry.
*/
export const associationDirectoryEntries = mysqlTable("associationDirectoryEntries", {
id: int("id").autoincrement().primaryKey(),
nomAssociation: varchar("nomAssociation", { length: 255 }).notNull(),
emailOfficiel: varchar("emailOfficiel", { length: 320 }),
emailOfficielNormalise: varchar("emailOfficielNormalise", { length: 320 }).unique(),
siret: varchar("siret", { length: 14 }),
rna: varchar("rna", { length: 10 }),
thematique: text("thematique"),
adresse: text("adresse"),
codePostal: varchar("codePostal", { length: 10 }),
ville: varchar("ville", { length: 100 }),
telephone: varchar("telephone", { length: 20 }),
siteWeb: varchar("siteWeb", { length: 255 }),
facebookUrl: varchar("facebookUrl", { length: 255 }),
instagramUrl: varchar("instagramUrl", { length: 255 }),
dateCreation: timestamp("dateCreation"),
objetAssociation: text("objetAssociation"),
statutJuridique: mysqlEnum("statutJuridique", [
"association_loi_1901",
"association_reconnue_utilite_publique",
"fondation",
"autre"
]).default("association_loi_1901"),
nomRepresentant: varchar("nomRepresentant", { length: 255 }),
fonctionRepresentant: varchar("fonctionRepresentant", { length: 100 }),
gouvernance: text("gouvernance"),
latitude: varchar("latitude", { length: 32 }),
longitude: varchar("longitude", { length: 32 }),
geoSource: mysqlEnum("geoSource", associationGeoSources),
geoPrecision: mysqlEnum("geoPrecision", associationGeoPrecisions).default("commune_center").notNull(),
geoLastSyncedAt: timestamp("geoLastSyncedAt"),
externalSourceStatus: varchar("externalSourceStatus", { length: 100 }),
externalSourceLabel: varchar("externalSourceLabel", { length: 255 }),
associationStatus: varchar("associationStatus", { length: 40 }),
registryLastUpdatedAt: timestamp("registryLastUpdatedAt"),
referenceLastCheckedAt: timestamp("referenceLastCheckedAt"),
referenceStatus: varchar("referenceStatus", { length: 100 }),
referenceSourceLabel: varchar("referenceSourceLabel", { length: 255 }),
sourceFileName: varchar("sourceFileName", { length: 255 }),
sourceRowNumber: int("sourceRowNumber"),
sourceFingerprint: varchar("sourceFingerprint", { length: 64 }),
isActive: boolean("isActive").default(true).notNull(),
importedAt: timestamp("importedAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type AssociationDirectoryEntry = typeof associationDirectoryEntries.$inferSelect;
export type InsertAssociationDirectoryEntry = typeof associationDirectoryEntries.$inferInsert;
export const associationDirectoryReviewSourceTypes = ["portal_signup", "helloasso"] as const;
export const associationDirectoryReviewStatuses = ["pending", "linked", "created", "ignored"] as const;
export const associationDirectoryReviews = mysqlTable("associationDirectoryReviews", {
id: int("id").autoincrement().primaryKey(),
sourceType: mysqlEnum("sourceType", associationDirectoryReviewSourceTypes).notNull(),
sourceLabel: varchar("sourceLabel", { length: 255 }),
userId: int("userId"),
resolvedDirectoryEntryId: int("resolvedDirectoryEntryId"),
proposedNomAssociation: varchar("proposedNomAssociation", { length: 255 }).notNull(),
proposedEmail: varchar("proposedEmail", { length: 320 }),
proposedEmailNormalise: varchar("proposedEmailNormalise", { length: 320 }),
proposedSiret: varchar("proposedSiret", { length: 14 }),
proposedRna: varchar("proposedRna", { length: 10 }),
proposedVille: varchar("proposedVille", { length: 100 }),
matchStatus: varchar("matchStatus", { length: 40 }),
payload: text("payload"),
status: mysqlEnum("status", associationDirectoryReviewStatuses).default("pending").notNull(),
resolutionNote: text("resolutionNote"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type AssociationDirectoryReview = typeof associationDirectoryReviews.$inferSelect;
export type InsertAssociationDirectoryReview = typeof associationDirectoryReviews.$inferInsert;
export const associationDirectoryUpdateProposalSourceTypes = ["official_registry", "helloasso"] as const;
export const associationDirectoryUpdateProposalStatuses = ["pending", "applied", "dismissed"] as const;
export const associationDirectoryUpdateProposals = mysqlTable("associationDirectoryUpdateProposals", {
id: int("id").autoincrement().primaryKey(),
directoryEntryId: int("directoryEntryId").notNull(),
sourceType: mysqlEnum("sourceType", associationDirectoryUpdateProposalSourceTypes).notNull(),
sourceLabel: varchar("sourceLabel", { length: 255 }),
summary: varchar("summary", { length: 255 }),
payload: text("payload").notNull(),
status: mysqlEnum("status", associationDirectoryUpdateProposalStatuses).default("pending").notNull(),
appliedAt: timestamp("appliedAt"),
dismissedAt: timestamp("dismissedAt"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type AssociationDirectoryUpdateProposal = typeof associationDirectoryUpdateProposals.$inferSelect;
export type InsertAssociationDirectoryUpdateProposal = typeof associationDirectoryUpdateProposals.$inferInsert;
/**
* Document types for associations
*/
export const documentTypes = [
"statuts",
"recepisse_declaration",
"rib",
"rapport_activite",
"rapport_financier",
"pv_assemblee",
"liste_dirigeants",
"attestation_assurance",
"autre"
] as const;
/**
* Documents table - stores document metadata (files in S3)
*/
export const documents = mysqlTable("documents", {
id: int("id").autoincrement().primaryKey(),
associationId: int("associationId").notNull(),
nom: varchar("nom", { length: 255 }).notNull(),
type: mysqlEnum("type", documentTypes).notNull(),
description: text("description"),
// S3 storage
fileKey: varchar("fileKey", { length: 512 }).notNull(),
fileUrl: varchar("fileUrl", { length: 1024 }).notNull(),
mimeType: varchar("mimeType", { length: 100 }),
fileSize: int("fileSize"), // in bytes
uploadedAt: timestamp("uploadedAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Document = typeof documents.$inferSelect;
export type InsertDocument = typeof documents.$inferInsert;
/**
* Request types available
*/
export const requestTypes = [
"subvention_fonctionnement",
"subvention_projet",
"agrement_jeunesse_education",
"agrement_sport",
"autorisation_occupation",
"demande_salle",
"demande_materiel_evenementiel",
"autre"
] as const;
/**
* Request status
*/
export const requestStatuses = [
"brouillon",
"soumise",
"en_cours_traitement",
"information_complementaire",
"validee",
"refusee",
"annulee"
] as const;
/**
* Request priority levels
*/
export const requestPriorities = [
"basse",
"normale",
"haute",
"urgente"
] as const;
/**
* Requests table - stores all requests from associations
*/
export const requests = mysqlTable("requests", {
id: int("id").autoincrement().primaryKey(),
associationId: int("associationId").notNull(),
// Request info
type: mysqlEnum("type", requestTypes).notNull(),
titre: varchar("titre", { length: 255 }).notNull(),
description: text("description"),
// Form data stored as JSON
formData: text("formData"), // JSON string
// Status tracking
status: mysqlEnum("status", requestStatuses).default("brouillon").notNull(),
priority: mysqlEnum("priority", requestPriorities).default("normale").notNull(),
// Financial (for subventions)
montantDemande: int("montantDemande"), // in cents
montantAccorde: int("montantAccorde"), // in cents
// Processing
dateSubmission: timestamp("dateSubmission"),
dateTraitement: timestamp("dateTraitement"),
dateLimiteTraitement: timestamp("dateLimiteTraitement"),
assigneA: int("assigneA"), // admin user id assigned to process
traitePar: int("traitePar"), // admin user id who processed
commentaireAdmin: text("commentaireAdmin"),
// Attached documents (JSON array of document IDs)
documentsJoints: text("documentsJoints"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Request = typeof requests.$inferSelect;
export type InsertRequest = typeof requests.$inferInsert;
/**
* Request history - tracks all actions on a request
*/
export const requestHistory = mysqlTable("requestHistory", {
id: int("id").autoincrement().primaryKey(),
requestId: int("requestId").notNull(),
action: mysqlEnum("action", [
"creation",
"soumission",
"assignation",
"changement_statut",
"ajout_commentaire",
"modification",
"validation",
"refus",
"changement_salle",
"annulation"
]).notNull(),
ancienStatut: mysqlEnum("ancienStatut", requestStatuses),
nouveauStatut: mysqlEnum("nouveauStatut", requestStatuses),
commentaire: text("commentaire"),
userId: int("userId").notNull(), // who performed the action
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type RequestHistory = typeof requestHistory.$inferSelect;
export type InsertRequestHistory = typeof requestHistory.$inferInsert;
/**
* Response templates - predefined responses for admins
*/
export const responseTemplates = mysqlTable("responseTemplates", {
id: int("id").autoincrement().primaryKey(),
nom: varchar("nom", { length: 255 }).notNull(),
type: mysqlEnum("type", ["validation", "refus", "information_complementaire", "autre"]).notNull(),
sujet: varchar("sujet", { length: 255 }),
contenu: text("contenu").notNull(),
actif: boolean("actif").default(true),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type ResponseTemplate = typeof responseTemplates.$inferSelect;
export type InsertResponseTemplate = typeof responseTemplates.$inferInsert;
/**
* Request templates - predefined form templates
*/
export const requestTemplates = mysqlTable("requestTemplates", {
id: int("id").autoincrement().primaryKey(),
type: mysqlEnum("type", requestTypes).notNull(),
nom: varchar("nom", { length: 255 }).notNull(),
description: text("description"),
// Form schema as JSON
formSchema: text("formSchema").notNull(),
// Required documents
documentsRequis: text("documentsRequis"), // JSON array
// Service destinataire
serviceDestinataire: varchar("serviceDestinataire", { length: 255 }),
emailDestinataire: varchar("emailDestinataire", { length: 320 }),
// Processing settings
delaiTraitementJours: int("delaiTraitementJours").default(30),
actif: boolean("actif").default(true),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type RequestTemplate = typeof requestTemplates.$inferSelect;
export type InsertRequestTemplate = typeof requestTemplates.$inferInsert;
/**
* Audit log - tracks all admin actions
*/
export const auditLog = mysqlTable("auditLog", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId").notNull(),
action: varchar("action", { length: 100 }).notNull(),
entityType: varchar("entityType", { length: 50 }).notNull(), // association, request, user, etc.
entityId: int("entityId"),
details: text("details"), // JSON with action details
ipAddress: varchar("ipAddress", { length: 45 }),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type AuditLog = typeof auditLog.$inferSelect;
export type InsertAuditLog = typeof auditLog.$inferInsert;
/**
* Admin notifications
*/
export const adminNotifications = mysqlTable("adminNotifications", {
id: int("id").autoincrement().primaryKey(),
userId: int("userId"), // null = all admins
type: mysqlEnum("type", [
"nouvelle_demande",
"demande_en_retard",
"dossier_incomplet",
"nouvelle_association",
"systeme",
"changement_salle",
"annulation_demande",
"modification_demande"
]).notNull(),
titre: varchar("titre", { length: 255 }).notNull(),
message: text("message").notNull(),
lien: varchar("lien", { length: 512 }),
lu: boolean("lu").default(false),
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type AdminNotification = typeof adminNotifications.$inferSelect;
export type InsertAdminNotification = typeof adminNotifications.$inferInsert;
/**
* Portal settings - configurable settings
*/
export const portalSettings = mysqlTable("portalSettings", {
id: int("id").autoincrement().primaryKey(),
cle: varchar("cle", { length: 100 }).notNull().unique(),
valeur: text("valeur").notNull(),
description: text("description"),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type PortalSetting = typeof portalSettings.$inferSelect;
export type InsertPortalSetting = typeof portalSettings.$inferInsert;
/**
* Operational recap services - reusable internal distribution lists for processed requests.
*/
export const operationalRecapServices = mysqlTable("operationalRecapServices", {
id: int("id").autoincrement().primaryKey(),
label: varchar("label", { length: 255 }).notNull(),
usage: mysqlEnum("usage", ["terrain", "controle"]).default("terrain").notNull(),
description: text("description"),
recipientEmails: text("recipientEmails").notNull(), // JSON array of emails
actif: boolean("actif").default(true).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type OperationalRecapService = typeof operationalRecapServices.$inferSelect;
export type InsertOperationalRecapService = typeof operationalRecapServices.$inferInsert;
/**
* Association invitations - secure one-time invitations for directory onboarding.
*/
export const associationInvitations = mysqlTable("associationInvitations", {
id: int("id").autoincrement().primaryKey(),
directoryEntryId: int("directoryEntryId").notNull(),
emailOfficiel: varchar("emailOfficiel", { length: 320 }).notNull(),
emailOfficielNormalise: varchar("emailOfficielNormalise", { length: 320 }).notNull(),
token: varchar("token", { length: 128 }).notNull().unique(),
deliveryMode: mysqlEnum("deliveryMode", ["email", "manual_link"]).notNull(),
emailSent: boolean("emailSent").default(false).notNull(),
sentByUserId: int("sentByUserId").notNull(),
sentAt: timestamp("sentAt").defaultNow().notNull(),
expiresAt: timestamp("expiresAt").notNull(),
usedAt: timestamp("usedAt"),
revokedAt: timestamp("revokedAt"),
acceptedByUserId: int("acceptedByUserId"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type AssociationInvitation = typeof associationInvitations.$inferSelect;
export type InsertAssociationInvitation = typeof associationInvitations.$inferInsert;
/**
* Email action tokens - secure one-time tokens for email-based actions (validate/refuse)
*/
export const emailActionTokens = mysqlTable("emailActionTokens", {
id: int("id").autoincrement().primaryKey(),
token: varchar("token", { length: 64 }).notNull().unique(),
requestId: int("requestId").notNull(),
action: mysqlEnum("action", ["validee", "refusee", "acceptation_devis_salle", "refus_devis_salle"]).notNull(),
used: boolean("used").default(false).notNull(),
expiresAt: timestamp("expiresAt").notNull(),
usedAt: timestamp("usedAt"),
usedBy: int("usedBy"), // admin user id
createdAt: timestamp("createdAt").defaultNow().notNull(),
});
export type EmailActionToken = typeof emailActionTokens.$inferSelect;
export type InsertEmailActionToken = typeof emailActionTokens.$inferInsert;
export const materialReturnStatuses = [
"planifie",
"en_attente",
"en_cours",
"cloture",
] as const;
export const materialReturnComplianceValues = [
"conforme",
"non_conforme",
] as const;
export const materialReturnLitigationStatuses = [
"none",
"pending",
"resolved",
] as const;
export const materialReturnArbitrationDecisions = [
"partial_retention",
"full_retention",
"dismissed",
] as const;
export const materialReturnFollowups = mysqlTable("materialReturnFollowups", {
id: int("id").autoincrement().primaryKey(),
requestId: int("requestId").notNull().unique(),
associationId: int("associationId").notNull(),
serviceLabel: varchar("serviceLabel", { length: 255 }),
recipientEmails: text("recipientEmails"), // JSON array
supervisionServiceLabel: varchar("supervisionServiceLabel", { length: 255 }),
supervisionRecipientEmails: text("supervisionRecipientEmails"), // JSON array
restitutionDate: timestamp("restitutionDate").notNull(),
plannedSendAt: timestamp("plannedSendAt").notNull(),
sentAt: timestamp("sentAt"),
status: mysqlEnum("status", materialReturnStatuses).default("planifie").notNull(),
uploadToken: varchar("uploadToken", { length: 96 }).notNull().unique(),
uploadTokenExpiresAt: timestamp("uploadTokenExpiresAt").notNull(),
signedFileKey: varchar("signedFileKey", { length: 512 }),
signedFileUrl: varchar("signedFileUrl", { length: 1024 }),
signedFileName: varchar("signedFileName", { length: 255 }),
signedMimeType: varchar("signedMimeType", { length: 100 }),
uploadedByName: varchar("uploadedByName", { length: 255 }),
uploadedByEmail: varchar("uploadedByEmail", { length: 320 }),
agentUserId: int("agentUserId"),
agentRole: varchar("agentRole", { length: 100 }),
borrowerName: varchar("borrowerName", { length: 255 }),
borrowerRole: varchar("borrowerRole", { length: 120 }),
compliance: mysqlEnum("compliance", materialReturnComplianceValues),
discrepancyCategories: text("discrepancyCategories"),
discrepancyDetails: text("discrepancyDetails"),
issueFlag: boolean("issueFlag").default(false).notNull(),
litigationStatus: mysqlEnum("litigationStatus", materialReturnLitigationStatuses).default("none").notNull(),
blockedItems: text("blockedItems"),
estimatedDamageAmount: int("estimatedDamageAmount"),
estimatedDamageSource: varchar("estimatedDamageSource", { length: 32 }),
arbitrationDecision: mysqlEnum("arbitrationDecision", materialReturnArbitrationDecisions),
arbitrationAmount: int("arbitrationAmount"),
arbitrationNotes: text("arbitrationNotes"),
arbitratedByUserId: int("arbitratedByUserId"),
arbitratedAt: timestamp("arbitratedAt"),
litigationLetterKey: varchar("litigationLetterKey", { length: 512 }),
litigationLetterUrl: varchar("litigationLetterUrl", { length: 1024 }),
litigationLetterName: varchar("litigationLetterName", { length: 255 }),
agentSignatureKey: varchar("agentSignatureKey", { length: 512 }),
agentSignatureUrl: varchar("agentSignatureUrl", { length: 1024 }),
borrowerSignatureKey: varchar("borrowerSignatureKey", { length: 512 }),
borrowerSignatureUrl: varchar("borrowerSignatureUrl", { length: 1024 }),
finalPdfKey: varchar("finalPdfKey", { length: 512 }),
finalPdfUrl: varchar("finalPdfUrl", { length: 1024 }),
finalPdfName: varchar("finalPdfName", { length: 255 }),
geoLatitude: varchar("geoLatitude", { length: 64 }),
geoLongitude: varchar("geoLongitude", { length: 64 }),
geoStatus: varchar("geoStatus", { length: 32 }),
geoFailureReason: text("geoFailureReason"),
uploadedAt: timestamp("uploadedAt"),
validatedAt: timestamp("validatedAt"),
closedAt: timestamp("closedAt"),
lastReminderSentAt: timestamp("lastReminderSentAt"),
reactivatedByUserId: int("reactivatedByUserId"),
reactivatedAt: timestamp("reactivatedAt"),
reactivationReason: text("reactivationReason"),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type MaterialReturnFollowup = typeof materialReturnFollowups.$inferSelect;
export type InsertMaterialReturnFollowup = typeof materialReturnFollowups.$inferInsert;