627 lines
24 KiB
TypeScript
627 lines
24 KiB
TypeScript
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;
|