Files
LogiFlow/scripts/bench/cases/fournisseurs-rapprochement.ts
T
LogiFlowandClaude Opus 5.5 600627c632 feat(rust) : serveur réécrit en Rust, Node retiré ; Calendrier en tête du menu (#571)
- Serveur Rust (server-rs/, axum + sqlx) : les 148 adresses de l'API,
  l'interface, les migrations de démarrage et les tâches de fond ; même
  comportement que Node (banc : 2027 cas, 0 écart hors écarts déclarés
  USERS-02, RAPPRO-21, R4, R5).
- Node retiré : server/, dépendances serveur, image Docker sans Node
  (51 Mo), docker-compose à un seul service applicatif.
- R4 : écritures d'un autre site refusées (Sec-Fetch-Site / Origin).
- R5 : « aujourd'hui » et sauvegarde automatique à l'heure de Paris.
- R6 : mainteneur dans .github/CODEOWNERS.
- Menu latéral : Accueil puis Calendrier en tête.
- Plan et état : docs/PLAN-MIGRATION-RUST.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ke2qyS8rgacP3ap5iezDbM
2026-10-15 20:31:14 +02:00

411 lines
23 KiB
TypeScript

// Lot 4, rapprochement : avoirs (et leur webhook), échéancier des paiements (lecture
// complétée par NocoDB, export CSV), envoi d'une facture au webhook du magasin,
// SQL et migration d'urgence de l'administration. NocoDB et les webhooks sont
// simulés par stub.ts. Données complémentaires : seeds/lot4-rapprochement.ts.
import { STUB_URL } from "../config";
import type { ReadCase, WriteScenario } from "../types";
// Mois « AAAA-MM » relatif au mois courant (sélecteur de l'échéancier)
function month(offset: number): string {
const date = new Date();
date.setUTCDate(1);
date.setUTCMonth(date.getUTCMonth() + offset);
return date.toISOString().slice(0, 7);
}
// Durée de vie des entrées du cache : relative à l'heure du passage, différente
// entre les deux serveurs
const CACHE_IGNORE = ["expires_at"];
const ALL_METHODS = ["virement", "prelevement", "cheque", "Non défini"];
// Fichier envoyé par BLReconciliation.tsx avec les champs du formulaire
const INVOICE_FILE = [{ field: "file", filename: "facture BL-1001.pdf", contentType: "application/pdf", size: 2048 }];
export const reads: ReadCase[] = [
// ---------- Avoirs (Avoirs.tsx, mobile/AvoirsPage.tsx : storeId du magasin choisi) ----------
{ module: "avoirs", path: "/api/avoirs" },
{ module: "avoirs", path: "/api/avoirs?storeId=1" },
{ module: "avoirs", path: "/api/avoirs?storeId=2" },
{ module: "avoirs", path: "/api/avoirs?storeId=3" },
{ module: "avoirs", path: "/api/avoirs?storeId=" },
{ module: "avoirs", path: "/api/avoirs?storeId=abc" },
{ module: "avoirs", path: "/api/avoirs?storeId=99999999999" },
{ module: "avoirs", path: "/api/avoirs/1" },
{ module: "avoirs", path: "/api/avoirs/3" },
{ module: "avoirs", path: "/api/avoirs/701" },
{ module: "avoirs", path: "/api/avoirs/999" },
{ module: "avoirs", path: "/api/avoirs/abc" },
// ---------- Échéancier (PaymentSchedulePage.tsx : groupId du magasin choisi) ----------
// La lecture complète en base les échéances et montants manquants (NocoDB) :
// les comptes suivants voient les livraisons déjà complétées
{ module: "payment-schedule", path: "/api/payment-schedule?groupId=1" },
{ module: "payment-schedule", path: "/api/payment-schedule?groupId=2" },
{ module: "payment-schedule", path: "/api/payment-schedule?groupId=3" },
{ module: "payment-schedule", path: "/api/payment-schedule?groupId=999" },
{ module: "payment-schedule", path: "/api/payment-schedule?groupId=abc", accounts: ["admin", "directeur", "manager"] },
{ module: "payment-schedule", path: "/api/payment-schedule", accounts: ["admin", "directeur"] },
{ module: "payment-schedule", path: "/api/payment-schedule?groupId=", accounts: ["admin", "directeur"] },
{ module: "payment-schedule", path: "/api/payment-schedule?groupId=0", accounts: ["admin", "directeur"] },
{ module: "payment-schedule", path: "/api/payment-schedule?groupId=1.5", accounts: ["admin", "directeur"] },
{ module: "payment-schedule", path: "/api/payment-schedule?groupId=-1", accounts: ["admin", "directeur"] },
{ module: "payment-schedule", path: "/api/payment-schedule?groupId=%201%20", accounts: ["admin", "directeur"] },
{ module: "payment-schedule", path: "/api/payment-schedule?groupId=99999999999", accounts: ["admin", "directeur"] },
];
export const writes: WriteScenario[] = [
// ---------- Avoirs ----------
{
module: "avoirs",
name: "créer un avoir (webhook du magasin à la création)",
steps: [
// Formulaire du bureau
{ method: "POST", path: "/api/avoirs", body: { supplierId: 1, groupId: 1, invoiceReference: "AV-4001", amount: 12.5, comment: "Casse", commercialProcessed: false, status: "En attente de demande" } },
{ method: "GET", path: "/api/avoirs/{{0.id}}" },
// Formulaire mobile : montant forcé à 0, sans référence ni commentaire
{ method: "POST", path: "/api/avoirs", body: { supplierId: 2, groupId: 1, amount: 0, status: "Demandé", createdBy: "pirate" } },
// Metz : sans webhook
{ method: "POST", path: "/api/avoirs", body: { supplierId: 3, groupId: 2, invoiceReference: "", amount: "-7.25", comment: "", commercialProcessed: true, status: "Reçu" } },
// Épinal : aucun compte du banc n'y est affecté (sauf l'administrateur)
{ method: "POST", path: "/api/avoirs", body: { supplierId: 4, groupId: 3, amount: null } },
{ method: "GET", path: "/api/avoirs" },
],
tables: ["avoirs"],
},
{
// Fiche du magasin modifiable par l'admin et le manager
module: "avoirs",
name: "webhook du magasin en erreur, injoignable ou invalide",
accounts: ["admin", "manager"],
steps: [
{ method: "PUT", path: "/api/groups/1", body: { webhookUrl: `${STUB_URL}/inconnu` } },
{ method: "POST", path: "/api/avoirs", body: { supplierId: 1, groupId: 1, invoiceReference: "AV-4101", amount: 1 } },
{ method: "PUT", path: "/api/groups/1", body: { webhookUrl: "http://127.0.0.1:1/webhook" } },
{ method: "POST", path: "/api/avoirs", body: { supplierId: 1, groupId: 1, invoiceReference: "AV-4102", amount: 2 } },
{ method: "PUT", path: "/api/avoirs/1", body: { status: "Reçu" } },
{ method: "PUT", path: "/api/groups/1", body: { webhookUrl: "pas une adresse" } },
{ method: "POST", path: "/api/avoirs", body: { supplierId: 1, groupId: 1, invoiceReference: "AV-4103", amount: 3 } },
],
tables: ["avoirs"],
},
{
module: "avoirs",
name: "avoirs invalides à la création",
steps: [
{ method: "POST", path: "/api/avoirs", body: {} },
{ method: "POST", path: "/api/avoirs", body: { supplierId: "1", groupId: 1.5 } },
{ method: "POST", path: "/api/avoirs", body: { supplierId: 1, groupId: 1, amount: "abc", status: "Perdu", commercialProcessed: "oui" } },
{ method: "POST", path: "/api/avoirs", body: { supplierId: 1, groupId: 1, invoiceReference: null, comment: null, commercialProcessed: null } },
{ method: "POST", path: "/api/avoirs", body: ["x"] },
// Erreurs de la base : fournisseur ou magasin inconnu, référence trop longue, montant trop grand
{ method: "POST", path: "/api/avoirs", body: { supplierId: 999, groupId: 1 } },
{ method: "POST", path: "/api/avoirs", body: { supplierId: 1, groupId: 999 } },
{ method: "POST", path: "/api/avoirs", body: { supplierId: 1, groupId: 1, invoiceReference: "R".repeat(300) } },
{ method: "POST", path: "/api/avoirs", body: { supplierId: 1, groupId: 1, amount: 1e9 } },
// Montant vide : Number("") = 0
{ method: "POST", path: "/api/avoirs", body: { supplierId: 1, groupId: 2, amount: "" } },
],
tables: ["avoirs"],
},
{
module: "avoirs",
name: "modifier un avoir (passage à « Reçu » et webhook)",
steps: [
// Formulaire complet : passage à « Reçu », webhook du magasin
{ method: "PUT", path: "/api/avoirs/1", body: { supplierId: 1, groupId: 1, invoiceReference: "AV-3001", amount: 50, comment: "Reçu par courrier", commercialProcessed: true, status: "Reçu" } },
// Déjà « Reçu » : pas de webhook ; montant, référence et commentaire effacés (AVOIRS-API-06)
{ method: "PUT", path: "/api/avoirs/1", body: { status: "Reçu" } },
// Metz (sans webhook)
{ method: "PUT", path: "/api/avoirs/3", body: { status: "Demandé", amount: "15.5", invoiceReference: "AV-3003", comment: "" } },
{ method: "PUT", path: "/api/avoirs/700", body: { status: "Reçu", invoiceReference: "FAC-2006", amount: 10.5 } },
// Changement de magasin et de créateur acceptés (AVOIRS-API-07)
{ method: "PUT", path: "/api/avoirs/2", body: { groupId: 2, createdBy: "pirate", invoiceReference: "AV-3002" } },
// Épinal
{ method: "PUT", path: "/api/avoirs/701", body: { status: "Reçu" } },
{ method: "GET", path: "/api/avoirs/1" },
],
tables: ["avoirs"],
},
{
module: "avoirs",
name: "avoirs invalides ou introuvables à la modification",
steps: [
{ method: "PUT", path: "/api/avoirs/1", body: { status: "Perdu" } },
{ method: "PUT", path: "/api/avoirs/1", body: { amount: "abc", supplierId: "2" } },
{ method: "PUT", path: "/api/avoirs/1", body: { invoiceReference: null } },
{ method: "PUT", path: "/api/avoirs/1", body: { supplierId: 999 } },
{ method: "PUT", path: "/api/avoirs/999", body: { status: "Reçu" } },
{ method: "PUT", path: "/api/avoirs/abc", body: { status: "Reçu" } },
{ method: "PUT", path: "/api/avoirs/1", body: [] },
],
tables: ["avoirs"],
},
{
module: "avoirs",
name: "supprimer des avoirs",
steps: [
{ method: "DELETE", path: "/api/avoirs/2" },
{ method: "DELETE", path: "/api/avoirs/700" },
{ method: "DELETE", path: "/api/avoirs/701" },
{ method: "DELETE", path: "/api/avoirs/999" },
{ method: "DELETE", path: "/api/avoirs/abc" },
{ method: "GET", path: "/api/avoirs" },
],
tables: ["avoirs"],
},
{
module: "avoirs",
name: "vérifier la facture d'un avoir dans NocoDB",
steps: [
{ method: "POST", path: "/api/avoirs/1/verify-invoice", body: { invoiceReference: "AV-3001", forceRefresh: false } },
// Depuis le cache, puis en forçant
{ method: "POST", path: "/api/avoirs/1/verify-invoice", body: { invoiceReference: "AV-3001" } },
{ method: "POST", path: "/api/avoirs/1/verify-invoice", body: { invoiceReference: " av-3001 ", forceRefresh: "oui" } },
{ method: "POST", path: "/api/avoirs/2/verify-invoice", body: { invoiceReference: "AV-INCONNU" } },
{ method: "POST", path: "/api/avoirs/700/verify-invoice", body: { invoiceReference: "FAC-2006" } },
// Épinal : sans configuration NocoDB
{ method: "POST", path: "/api/avoirs/701/verify-invoice", body: { invoiceReference: "FAC-2004" } },
{ method: "POST", path: "/api/avoirs/1/verify-invoice", body: {} },
{ method: "POST", path: "/api/avoirs/1/verify-invoice", body: { invoiceReference: " " } },
{ method: "POST", path: "/api/avoirs/1/verify-invoice", body: { invoiceReference: 2001 } },
{ method: "POST", path: "/api/avoirs/999/verify-invoice", body: { invoiceReference: "AV-3001" } },
{ method: "POST", path: "/api/avoirs/abc/verify-invoice", body: { invoiceReference: "AV-3001" } },
],
tables: ["invoice_verification_cache"],
ignore: CACHE_IGNORE,
},
{
module: "avoirs",
name: "valider un avoir et l'état de son webhook",
steps: [
// Entrée du cache à marquer « rapprochée » à la validation
{ method: "POST", path: "/api/avoirs/1/verify-invoice", body: { invoiceReference: "AV-3001" } },
{ method: "PUT", path: "/api/avoirs/1/nocodb-verification", body: { verified: true } },
{ method: "PUT", path: "/api/avoirs/2/nocodb-verification", body: { verified: false } },
// Référence absente ou blanche : rien à marquer ; autre magasin accepté (AVOIRS-API-08)
{ method: "PUT", path: "/api/avoirs/3/nocodb-verification", body: { verified: true } },
{ method: "PUT", path: "/api/avoirs/702/nocodb-verification", body: { verified: "yes" } },
{ method: "PUT", path: "/api/avoirs/700/nocodb-verification", body: {} },
{ method: "PUT", path: "/api/avoirs/701/nocodb-verification", body: { verified: "oui" } },
{ method: "PUT", path: "/api/avoirs/999/nocodb-verification", body: { verified: true } },
{ method: "PUT", path: "/api/avoirs/abc/nocodb-verification", body: { verified: true } },
{ method: "PUT", path: "/api/avoirs/1/webhook-status", body: { webhookSent: false } },
{ method: "PUT", path: "/api/avoirs/2/webhook-status", body: { webhookSent: 1 } },
{ method: "PUT", path: "/api/avoirs/700/webhook-status", body: {} },
{ method: "PUT", path: "/api/avoirs/701/webhook-status", body: { webhookSent: null } },
{ method: "PUT", path: "/api/avoirs/3/webhook-status", body: { webhookSent: "oui" } },
{ method: "PUT", path: "/api/avoirs/999/webhook-status", body: { webhookSent: true } },
{ method: "PUT", path: "/api/avoirs/abc/webhook-status", body: { webhookSent: true } },
],
tables: ["avoirs", "invoice_verification_cache"],
ignore: CACHE_IGNORE,
},
// ---------- Échéancier ----------
{
module: "payment-schedule",
name: "échéancier : échéances et montants complétés par NocoDB",
steps: [
{ method: "GET", path: "/api/payment-schedule?groupId=1" },
// Seconde lecture : le repli « TTC » recommence (montant TTC jamais écrit)
{ method: "GET", path: "/api/payment-schedule?groupId=1" },
{ method: "GET", path: "/api/payment-schedule?groupId=2" },
{ method: "GET", path: "/api/payment-schedule?groupId=3" },
],
tables: ["deliveries", "invoice_verification_cache"],
ignore: CACHE_IGNORE,
},
{
module: "payment-schedule",
name: "export CSV de l'échéancier",
steps: [
// Formulaire d'export : mois courant, tous les modes de paiement, HT et TTC
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: "1", month: month(0), paymentMethods: ALL_METHODS, includeHT: true, includeTTC: true } },
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: 1, month: month(0), paymentMethods: ["virement", "cheque"], includeHT: false, includeTTC: true } },
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: 1, month: month(1), paymentMethods: ALL_METHODS, includeHT: "non", includeTTC: 0 } },
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: 2, month: month(0), paymentMethods: ALL_METHODS, includeHT: true } },
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: 3, month: month(0), paymentMethods: ALL_METHODS, includeTTC: true } },
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: 1, month: "2020-13", paymentMethods: ALL_METHODS, includeHT: true } },
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: 999, month: month(0), paymentMethods: ALL_METHODS } },
],
tables: [],
},
{
module: "payment-schedule",
name: "export CSV : paramètres invalides",
accounts: ["admin", "directeur", "manager"],
steps: [
{ method: "POST", path: "/api/payment-schedule/export", body: {} },
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: "abc", month: month(0), paymentMethods: ALL_METHODS } },
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: 1.5, month: month(0), paymentMethods: ALL_METHODS } },
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: 1, month: "2026-1", paymentMethods: ALL_METHODS } },
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: 1, month: 202610, paymentMethods: ALL_METHODS } },
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: 1, month: month(0), paymentMethods: [] } },
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: 1, month: month(0), paymentMethods: "virement" } },
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: 1, month: month(0), paymentMethods: ["virement", 5, null] } },
{ method: "POST", path: "/api/payment-schedule/export", body: { groupId: 1, month: month(0) } },
],
tables: [],
},
// ---------- Envoi d'une facture au webhook (BLReconciliation.tsx) ----------
{
module: "send-invoice",
name: "envoyer une facture au webhook du magasin",
steps: [
{
method: "POST",
path: "/api/reconciliation/send-invoice",
bodyType: "multipart",
body: { webhookUrl: `${STUB_URL}/webhook/avoirs`, supplier: "Lactalis", blNumber: "BL-1001", type: "Facture" },
files: INVOICE_FILE,
},
// Webhook BAP configuré ; champs facultatifs absents
{
method: "POST",
path: "/api/reconciliation/send-invoice",
bodyType: "multipart",
body: { webhookUrl: `${STUB_URL}/webhook/bap` },
files: [{ field: "file", filename: "scan échéance.png", contentType: "image/PNG", size: 10 }],
},
// Fichier ou adresse manquants
{ method: "POST", path: "/api/reconciliation/send-invoice", bodyType: "multipart", body: { webhookUrl: `${STUB_URL}/webhook/avoirs`, supplier: "Lactalis" } },
{ method: "POST", path: "/api/reconciliation/send-invoice", bodyType: "multipart", body: { supplier: "Lactalis" }, files: INVOICE_FILE },
{ method: "POST", path: "/api/reconciliation/send-invoice", bodyType: "multipart", body: { webhookUrl: "" }, files: INVOICE_FILE },
// Champ « file » en texte
{ method: "POST", path: "/api/reconciliation/send-invoice", bodyType: "multipart", body: { file: "pas un fichier", webhookUrl: `${STUB_URL}/webhook/avoirs` } },
{ method: "POST", path: "/api/reconciliation/send-invoice", bodyType: "multipart", body: { file: "", webhookUrl: `${STUB_URL}/webhook/avoirs` } },
// Pas de multipart
{ method: "POST", path: "/api/reconciliation/send-invoice", body: { webhookUrl: `${STUB_URL}/webhook/avoirs` } },
{ method: "POST", path: "/api/reconciliation/send-invoice" },
],
tables: [],
},
{
module: "send-invoice",
name: "webhook de facture en erreur ou injoignable",
accounts: ["admin"],
steps: [
// Adresses enregistrées sur la fiche du magasin d'Épinal
{ method: "PUT", path: "/api/groups/3", body: { webhookUrl: `${STUB_URL}/inconnu` } },
{
method: "POST",
path: "/api/reconciliation/send-invoice",
bodyType: "multipart",
body: { webhookUrl: `${STUB_URL}/inconnu`, supplier: "Jardin & Co", blNumber: "", type: "Avoir" },
files: INVOICE_FILE,
},
{ method: "PUT", path: "/api/groups/3", body: { webhookUrl: "http://127.0.0.1:1/webhook" } },
{
method: "POST",
path: "/api/reconciliation/send-invoice",
bodyType: "multipart",
body: { webhookUrl: "http://127.0.0.1:1/webhook", supplier: "Jardin & Co" },
files: INVOICE_FILE,
},
],
tables: [],
},
{
// RAPPRO-21 : Node envoie le fichier à toute adresse fournie par le navigateur
module: "send-invoice",
name: "envoyer une facture à une adresse non configurée",
// Les autres comptes sont refusés avant (403), comme dans le premier scénario
accounts: ["admin", "directeur"],
steps: [
{
method: "POST",
path: "/api/reconciliation/send-invoice",
bodyType: "multipart",
body: { webhookUrl: `${STUB_URL}/webhook/non-configure`, supplier: "Lactalis", blNumber: "BL-1001" },
files: INVOICE_FILE,
},
// Webhook BAP désactivé (par l'admin) : son adresse n'est plus acceptée
{ method: "POST", path: "/api/webhook-bap-config", body: { webhookUrl: `${STUB_URL}/webhook/bap`, isActive: false } },
{
method: "POST",
path: "/api/reconciliation/send-invoice",
bodyType: "multipart",
body: { webhookUrl: `${STUB_URL}/webhook/bap`, supplier: "Lactalis" },
files: INVOICE_FILE,
},
],
tables: [],
deviation: {
fields: ["status", "body", "appels"],
reason:
"RAPPRO-21 : l'adresse du webhook doit être celle d'un magasin ou du webhook BAP actif ; toute autre adresse est refusée (400) au lieu d'être appelée par le serveur",
},
},
// ---------- Administration : SQL et migration d'urgence (SQLExecutor.tsx) ----------
{
module: "admin-sql",
name: "exécuter du SQL : lectures, types de colonnes et erreurs",
steps: [
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "SELECT id, name, color, nocodb_config_id FROM groups ORDER BY id" } },
{
method: "POST",
path: "/api/admin/execute-sql",
body: {
sql: "SELECT count(*) AS total, sum(amount) AS somme, min(created_at) AS premier, min(created_at)::date AS jour, bool_and(true) AS vrai, " +
"1.5::float8 AS flottant, 2::float8 AS entier, 7::int2 AS petit, 12345678901::int8 AS grand, NULL AS rien, '{\"a\": [1, \"b\"]}'::jsonb AS objet, " +
"ARRAY[1, 2] AS tableau, ARRAY['a', NULL] AS textes, 'x'::bytea AS octets, '1 day 02:00'::interval AS duree, " +
"'2026-01-01 10:00+02'::timestamptz AS horodate, 'f'::bool AS faux, 'é\"\\\\'::text AS texte FROM avoirs",
},
},
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "SELECT 'a' AS x, 'b' AS x, 1" } },
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "SELECT n, n * 1.5 AS demi FROM generate_series(1, 12) AS n" } },
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "SELECT 1 WHERE false" } },
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "SELECT 1 AS a; SELECT 2 AS b" } },
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "SELECT 1 AS a; SELEC 2" } },
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "ANALYZE groups" } },
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: " " } },
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "SELEC 1" } },
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "SELECT 1/0" } },
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "" } },
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: 5 } },
{ method: "POST", path: "/api/admin/execute-sql", body: {} },
],
tables: [],
},
{
module: "admin-sql",
name: "exécuter du SQL : modification d'une ligne",
steps: [
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "UPDATE groups SET color = '#123456' WHERE id = 3" } },
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "UPDATE groups SET color = color WHERE id = 999" } },
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "UPDATE avoirs SET comment = 'modifié' WHERE id = 1 RETURNING id, comment, amount" } },
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "SELECT id, color FROM groups WHERE id = 3" } },
],
tables: ["groups", "avoirs"],
},
{
module: "admin-sql",
name: "migration d'urgence : secrets en clair chiffrés",
steps: [
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "UPDATE groups SET smtp_password = 'secret-en-clair' WHERE id = 2" } },
{ method: "POST", path: "/api/admin/emergency-migration" },
{ method: "POST", path: "/api/admin/execute-sql", body: { sql: "SELECT id, smtp_password LIKE 'enc:v1:%' AS chiffre FROM groups ORDER BY id" } },
],
tables: [],
},
{
// N'existe que si EMERGENCY_SECRET est défini (sinon 404) : le banc rejoue le
// secret de son propre environnement
module: "admin-sql",
name: "réinitialisation d'urgence du compte admin",
accounts: ["anonyme", "manager"],
steps: [
{ method: "POST", path: "/api/emergency-admin-reset", body: { secret: "mauvais-secret" } },
{ method: "POST", path: "/api/emergency-admin-reset", body: {} },
{ method: "POST", path: "/api/emergency-admin-reset", body: { secret: process.env.EMERGENCY_SECRET || "secret-absent" } },
{ method: "POST", path: "/api/login", body: { username: "admin", password: "admin" } },
],
tables: ["users"],
// Empreinte au sel aléatoire
ignore: ["password"],
},
];