Aller au contenu principal

Exports - Aaperture

Module D : Exports comptables, contacts, sessions/projets, GDPR export avec génération asynchrone et stockage R2.

Contexte

Exports comptables, contacts, sessions/projets, GDPR export, rapports. Génération lourde + stockage R2.

Bounded Context (DDD)

Exporting

  • Agrégat : ExportJob
  • Domain services : ExportBuilder (par type), ExportPolicyService
  • Ports : StoragePort, PdfRendererPort (si PDF), ZipPort

Schéma DB

export_jobs

CREATE TABLE export_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL,
requested_by UUID NOT NULL,
type TEXT NOT NULL, -- ACCOUNTING|CONTACTS|SESSIONS|GDPR|CUSTOM
params JSONB NOT NULL, -- Paramètres de l'export (filtres, champs, format, etc.)
status TEXT NOT NULL DEFAULT 'QUEUED', -- QUEUED|RUNNING|DONE|FAILED
file_object_id UUID REFERENCES file_objects(id),
format TEXT NOT NULL, -- CSV|EXCEL|PDF|ZIP|JSON
error TEXT,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
finished_at TIMESTAMPTZ
);

CREATE INDEX idx_export_jobs_org ON export_jobs(org_id);
CREATE INDEX idx_export_jobs_status ON export_jobs(status);
CREATE INDEX idx_export_jobs_requested_by ON export_jobs(requested_by);
CREATE INDEX idx_export_jobs_type ON export_jobs(type);
CREATE INDEX idx_export_jobs_created ON export_jobs(created_at);

Jobs

export:build (worker-export)

Responsabilités :

  • Générer export selon type et format
  • Upload vers R2
  • Créer file_object
  • Mettre à jour export_jobs

Payload :

{
v: 1,
orgId: "uuid",
userId: "uuid",
entityId: "uuid", // export_job.id
}

Processus :

  1. Charger export_job depuis DB
  2. Selon type :
    • ACCOUNTING : Exporter factures, paiements, avoirs (CSV/Excel)
    • CONTACTS : Exporter contacts avec champs sélectionnés (CSV/Excel/JSON)
    • SESSIONS : Exporter sessions avec détails (CSV/Excel/JSON)
    • GDPR : Export complet données utilisateur (JSON/XML)
    • CUSTOM : Export personnalisé selon params
  3. Générer fichier selon format :
    • CSV : Génération CSV avec headers
    • EXCEL : Génération XLSX avec feuilles multiples si nécessaire
    • PDF : Génération PDF via PdfRendererPort
    • ZIP : Génération ZIP si plusieurs fichiers (ex: GDPR avec fichiers joints)
    • JSON : Génération JSON structuré
  4. Upload vers R2 via StoragePort
  5. Créer file_object avec métadonnées
  6. Mettre à jour export_jobs.file_object_id, status = 'DONE', finished_at = NOW()

Gestion erreurs :

  • Erreur génération → status = 'FAILED', error = ...
  • Erreur upload → retry (3 tentatives max)
  • Timeout → retry (3 tentatives max)

export:cleanup (optionnel)

Responsabilités :

  • Nettoyer exports anciens (> 30 jours)
  • Supprimer fichiers R2 orphelins

Payload :

{
v: 1,
orgId?: string, // Optionnel : toutes les orgs
retentionDays?: number, // Par défaut 30
}

Processus :

  1. Trouver export_jobs avec finished_at < NOW() - retentionDays
  2. Supprimer fichiers R2 via StoragePort
  3. Supprimer file_objects
  4. Supprimer export_jobs (ou archiver)

Types d'exports

ACCOUNTING

Params :

{
dateFrom: string; // ISO date
dateTo: string;
includeInvoices: boolean;
includePayments: boolean;
includeCreditNotes: boolean;
currency?: "EUR" | "USD";
}

Format : CSV, Excel

Colonnes :

  • Date, Type (Invoice/Payment/CreditNote), Numéro, Montant, Statut, Client, etc.

CONTACTS

Params :

{
filters?: {
tags?: string[];
hasSessions?: boolean;
createdAfter?: string;
};
fields?: string[]; // Champs à exporter (id, name, email, phone, ...)
}

Format : CSV, Excel, JSON

Colonnes : Selon fields sélectionnés

SESSIONS

Params :

{
filters?: {
type?: string[];
status?: string[];
dateFrom?: string;
dateTo?: string;
};
fields?: string[]; // Champs à exporter
includeFiles?: boolean; // Si true → génère ZIP avec fichiers
}

Format : CSV, Excel, JSON, ZIP (si fichiers)

Colonnes : Selon fields sélectionnés

GDPR

Params :

{
userId: string; // Utilisateur concerné
includeFiles?: boolean; // Inclure fichiers uploadés
}

Format : JSON, XML, ZIP (si fichiers)

Structure :

{
"user": { ... },
"sessions": [ ... ],
"contacts": [ ... ],
"quotes": [ ... ],
"invoices": [ ... ],
"files": [ ... ],
"metadata": {
"exportedAt": "2024-01-01T00:00:00Z",
"format": "JSON"
}
}

CUSTOM

Params :

{
entityType: "QUOTE" | "INVOICE" | "CONTRACT" | ...;
filters: Record<string, any>;
fields: string[];
format: "CSV" | "EXCEL" | "JSON";
}

Endpoints

POST /exports

Créer export job.

Request :

{
type: "ACCOUNTING" | "CONTACTS" | "SESSIONS" | "GDPR" | "CUSTOM";
format: "CSV" | "EXCEL" | "PDF" | "ZIP" | "JSON";
params: Record<string, any>; // Selon type
}

Response :

{
exportJobId: string;
status: "QUEUED";
jobId: string; // job export:build
}

GET /exports/:id

Statut + métadonnées export.

Response :

{
id: string;
type: string;
format: string;
status: "QUEUED" | "RUNNING" | "DONE" | "FAILED";
fileObjectId?: string;
error?: string;
createdAt: string;
finishedAt?: string;
}

GET /exports/:id/download

URL presignée pour téléchargement.

Response :

{
downloadUrl: string; // Presigned R2 URL
expiresAt: string;
filename: string;
sizeBytes: number;
}

GET /exports

Liste exports avec filtres.

Query params :

  • type?: Filtrer par type
  • status?: Filtrer par statut
  • dateFrom?, dateTo?: Plage de dates

Response :

{
exports: Array<ExportJob>;
total: number;
page: number;
pageSize: number;
}

DELETE /exports/:id

Supprimer export (et fichier R2).

Response :

{
deleted: boolean;
}

Frontend

Hooks

useCreateExport()

const { mutate: createExport } = useCreateExport();

createExport({
type: "ACCOUNTING",
format: "CSV",
params: {
dateFrom: "2024-01-01",
dateTo: "2024-12-31",
includeInvoices: true,
includePayments: true
}
}, {
onSuccess: ({ exportJobId }) => {
// Polling status
}
});

useExportJob(id) (polling)

const { data: exportJob } = useExportJob(exportJobId, {
refetchInterval: (query) => {
return query.state.data?.status === "QUEUED" || query.state.data?.status === "RUNNING"
? 2000
: false;
}
});

useDownloadExport(id)

const { mutate: download } = useDownloadExport(exportJobId);

download(undefined, {
onSuccess: ({ downloadUrl }) => {
window.open(downloadUrl, '_blank');
}
});

Composants

ExportDialog

Dialog pour créer export :

  • Sélecteur type
  • Sélecteur format
  • Formulaire params selon type
  • Bouton "Export"

ExportJobStatus

Affiche statut avec :

  • Badge statut (QUEUED/RUNNING/DONE/FAILED)
  • Spinner si en cours
  • Bouton "Download" si DONE
  • Message erreur si FAILED

ExportsList

Liste exports avec :

  • Filtres (type, statut, date)
  • Actions : download, delete
  • Pagination

Tests

Unit

  • ExportBuilder : Génération selon type/format
  • ExportPolicyService : Vérification permissions
  • Formats : CSV, Excel, PDF, JSON valides

Integration

  • export:build : Génération complète + upload R2
  • Presigned URLs : Génération et expiration correctes
  • Cleanup : Suppression fichiers anciens

E2E

  • Créer export → polling → download disponible
  • GDPR export : Structure JSON complète

Edge Cases

Idempotence

  • Vérifier si export avec mêmes params existe déjà (même jour)
  • Retourner file_object_id existant si disponible

Retries

  • Erreur génération → retry (3 tentatives max)
  • Erreur upload → retry (3 tentatives max)
  • Timeout → retry (3 tentatives max)

Performance

  • Exports volumineux (> 10k lignes) :
    • Génération par batch
    • Streaming si possible
    • Limiter taille max (ex: 100k lignes)

Stockage

  • Nettoyage automatique exports > 30 jours
  • Limiter nombre d'exports simultanés par organisation

Références