Histoire ING-002 : Scheduler + MinIO inbox client + tenant iteration skeleton¶
Module : ingestion
Tranche : Sous-tranche de niveau 1 ING-001-a (architecture §11)
Side : [BACKEND]
Version cible : V0.0.0
Priorité : 2
Dépend de : ING-001 (infra de test + migrations vides), TENANT-001 (TenantConfigCache bean + default-agency provisioning), AUDIT-001 (AuditEventBuilder + outbox) et AUDIT catalog extension OQ-X10 admettant ING_UPLOAD_RECEIVED
Peut être développé en même temps que : ING-003 (le code pur de l'analyseur/valideur n'a pas de chevauchement avec cette tranche)
Ordre de fusion : Avant ING-004
Complexité estimée : S
Histoires d'utilisateurs du DRP : Dérivé de ING-US-01 (voie de ramassage en amont ; substitut cron dans v0.0.0)
Cadre de travail : N/A - backend uniquement
Objectif¶
Câbler le cron nocturne @Scheduled qui itère les locataires actifs, liste /<tenantId>/inbox/ dans le seau MinIO de la plateforme, et émet l'audit ING_UPLOAD_RECEIVED lorsqu'un fichier est récupéré. Pas d'analyse, pas de validation, pas encore de validation - juste le squelette d'orchestration et le wrapper MinIO utilisé par toutes les tranches suivantes. Après cette histoire, l'opérateur qui dépose un fichier dans MinIO provoque une ligne d'audit et un mouvement processed/ au prochain tick.
Portée du backend¶
Entités¶
Aucune nouvelle. Cette tranche n'écrit pas encore de lignes ingestion_import - cela arrive avec ING-004. Elle n'émet que des événements d'audit avec la clé MinIO comme métadonnées équivalentes à subject_id.
Migrations¶
Aucune. (Le corps de V010 est encore vide ; rempli avec ING-004)
Couche de service¶
IngestionScheduler(@Component) : single@Scheduled(cron = "${INGESTION_CRON_EXPRESSION:0 0 2 * *}", zone = "${INGESTION_CRON_ZONE:Africa/Abidjan}")methodtick(). Également exposée via le point d'endpoint POST/internal/ingestion/tickuniquement sur le profillocalpour la commande de vérification de la fumée dans l'architecture §14.3.IngestionService.tick(): itèreTenantConfigCache.listActiveTenants(), ouvreTenantContext.scope(tenant.id, tenant.countryCode), appelleprocessInbox(tenant)par locataire séquentiellement (architecture §4.1).IngestionInboxClient(wrapper MinIO) :list(tenantId) → Stream<InboxObject>,get(tenantId, key) → InputStream,moveToProcessed(...),moveToRejected(...),moveToDuplicate(...),putErrorsSidecar(...). Clés construites uniquement à partir dutenantIdlié à l'itération (architecture §2.5, §7.6 menace A4).IngestionAuditEvents.uploadReceived(...): construitING_UPLOAD_RECEIVEDAuditEventBuilder payload avec{ filename, file_size_bytes, file_hash_sha256, minio_inbox_key }- pas de contenu de ligne.- Dans cette tranche, le flux
processInbox= liste → pour chaque objet : read size + stream-hash SHA-256 → emitING_UPLOAD_RECEIVED→ callmoveToProcessed(placeholder behavior ; ING-004 remplace par full parse/commit/reject branching).
Les endpoints de l'API¶
| Method | Path | Request | Response | Auth |
|---|---|---|---|---|
| POST | /internal/ingestion/tick (local profile ONLY) |
— | 204 No Content | None (gated by profile filter) |
Règles de validation¶
- La vérification des octets magiques (CSV vs XLSX) est reportée aux analyseurs ING-003.
- La taille des fichiers est limitée (
INGESTION_MAX_FILE_BYTES=5_242_880) à l'étape du hash-streaming : si les octets cumulés dépassent la limite, l'itération est interrompue pour ce fichier,ING_UPLOAD_RECEIVEDest émise avecfile_size_bytesfixé à la limite + un drapeautruncated:true, le fichier est déplacé vers/<tid>/rejected/avec la raisonFICHIER_TROP_VOLUMINEUXdans le sidecar. (Note : dans ING-004 cela devient une lignePARSE_FAILEDcorrecte ; dans ING-002 il n'y a pas encore de ligne DB)
Considérations multi-tenant¶
Cron ne transporte pas de JWT. Par itération, TenantContext.scope(tenant.id, tenant.countryCode) est la seule source de vérité pour le locataire. actor_type=SYSTEM, actor_id="ingestion-cron" sur toutes les émissions d'audit (architecture §4.1, §7.4). La dérivation de la clé MinIO DOIT utiliser uniquement la variable d'itération - jamais une valeur provenant du contenu du fichier (architecture §7.6 A4). Les deux profils de base de données fonctionnent parce que les seules écritures dans la base de données sont des lignes d'audit-outbox dans la base de données de la plate-forme (architecture §4.2, D12).
Audit et journalisation¶
ING_UPLOAD_RECEIVEDémis une fois par objet ramassé.actor_type=SYSTEM,actor_id=ingestion-cron,subject_type=INGESTION_IMPORT,subject_id=<UUIDv7 minted for the import>(le même UUID est réutilisé comme l'éventuelingestion_import.idune fois que ING-004 arrive ; persistez dans un thread-local pour l'itération). Détails :{ nom_de_fichier, taille_du_fichier_bytes, file_hash_sha256, minio_inbox_key }. Règles PII : le nom de fichier est contrôlé par l'opérateur ; ne PAS hacher ; le contenu du fichier n'est jamais répercuté.- Journal INFO structuré par limite d'itération (
tenantId,objectsFound,durationMs).
Portée du frontend¶
N/A - backend uniquement.
États de l'interface utilisateur (TOUS OBLIGATOIRES)¶
| State | Behavior | French Copy |
|---|---|---|
| Loading | N/A — backend only | — |
| Empty | N/A — backend only | — |
| Error | N/A — backend only | — |
| Offline / low network | N/A — backend only | — |
| Success | N/A — backend only | — |
Comportement réactif¶
N/A.
Interactions¶
N/A.
Critères d'acceptation¶
AC-001: Cron iterates active tenants and emits ING_UPLOAD_RECEIVED per picked file
Given two active tenants A and B in `papillon_platform.tenant`
And a 1-KB text file dropped in `<tenantA>/inbox/foo.csv`
When `IngestionService.tick()` runs
Then exactly one `ING_UPLOAD_RECEIVED` audit row exists for tenant A with subject_type=INGESTION_IMPORT
And zero audit rows for tenant B
And the audit details contain `filename=foo.csv`, the streamed SHA-256, and the inbox key prefixed by tenant A's UUID
And the file is moved to `<tenantA>/processed/<yyyy-MM-dd>__foo.csv`.
AC-002: Local profile tick endpoint exists; non-local profiles do not expose it
Given the application running with `spring.profiles.active=local`
When `POST /internal/ingestion/tick` is called
Then it returns 204 and IngestionService.tick() is invoked
And the same request on `spring.profiles.active=staging` returns 404 (endpoint not registered).
AC-003: File over 5 MB is rejected at the hash-streaming step without ever being fully buffered
Given a 6-MB binary file in `<tenantA>/inbox/big.csv`
When IngestionService.tick() runs
Then the file is moved to `<tenantA>/rejected/<ts>__big.csv` with sidecar JSON `{"reason":"FICHIER_TROP_VOLUMINEUX"}`
And heap usage during the run stays under 256 MB (asserted by the test profile JFR snapshot).
Règles de conformité¶
- Architecture §7.4 :
actor_type=SYSTEM,actor_id="ingestion-cron"est obligatoire sur chaque émission d'audit de cette tranche. - Règle critique #1 (isolation des locataires,
docs/standards/critical-rules.md) : La construction de la clé MinIO doit dérivertenantIdde la variable d'itération uniquement. - Architecture §7.6 A4 : la revue de code doit rejeter tout préfixe dérivé du contenu d'un fichier.
Normes et conventions¶
docs/standards/critical-rules.md(règles #1 isolation du locataire, #3 catalogue d'audit).docs/standards/multi-tenant-model.md§ contexte cron.docs/standards/java-spring-guidelines.md§@Scheduled+@TransactionalEventListener.docs/standards/api-guidelines.md§ endpoints internes (liés au profil).
Exigences de test¶
Tests unitaires¶
IngestionInboxClientTest: la construction des clés utilise uniquementtenantIdlié par itération ; refuse les clés ne commençant pas par<tenantId>/.IngestionAuditEventsTest: la forme du payloadING_UPLOAD_RECEIVEDcorrespond à la spécification.
Tests d'intégration¶
IngestionSchedulerIT(TestContainers) : fixation de deux locataires, drop in A only → AC-001 assertions ; verifyaudit_logrow count by tenant.IngestionLocalTickEndpointIT: visibilité des endpoints liée au profil selon AC-002.IngestionLargeFileIT: AC-003 avec un fichier aléatoire généré de 6 Mo.
Ce que l'AQ validera¶
- Exécuter la commande de vérification de la fumée de l'architecture §14.3 étapes 1-4 (drop, tick, verify audit row, verify processed/ key). L'étape 5 (DB assertions on
ingestion_import) est N/A jusqu'à ING-004.
Hors du champ d'application¶
- Analyse CSV/XLSX et validation des lignes (ING-003).
- écriture des tables
ingestion_import/ingestion_import_row, déduplication SHA-256 contre les importations existantes, transitions de statutPARSE_FAILED/DUPLICATE/COMMITTED(ING-004). - Récupération au démarrage pour les lignes
PICKED_UPbloquées (ING-005). - Option de fonctionnalité
feature.artci.phone-auth.grantedsur le profil prod (ING-005). - Interface utilisateur pour l'upload ou l'historique (V0.0.3+).
- résolution du
code_agence(V0.0.2+).
Définition de Fait¶
- [Tous les endpoints du backend sont implémentés et renvoient des réponses correctes
- [Toutes les pages du frontend s'affichent correctement à 360 / 768 / 1024 px (N/A - backend uniquement)
- [Les 5 états de l'interface utilisateur sont implémentés (N/A - backend uniquement)
- [La micro-copie française correspond exactement aux spécifications du design (N/A - backend uniquement)
- [Tous les critères d'acceptation sont passés manuellement
- [Tests unitaires et d'intégration écrits et réussis
- [ ] Isolation des locataires vérifiée (
tenant_idsur chaque clé MinIO + ligne d'audit ; les deux profils de BD V1 testés - SHARED actif, BYO smoke) - [ ] [Histoires de clients] payload + Slow-3G + resumability (N/A - backend uniquement)
- [Les entrées du journal d'audit sont présentes pour chaque événement requis (
ING_UPLOAD_RECEIVEDpar ramassage ; vérifié dans le service informatique) - [Aucune défaillance silencieuse sur les erreurs de réseau (les erreurs transitoires MinIO sont enregistrées au niveau de WARN et l'itération continue ; le fichier reste dans la boîte de réception pour le prochain tick - architecture §5.1 étape 1)