Campaign Draft
Il Campaign Draft è un aggregate operativo separato dall'Issue. Il percorso
normale accetta una sola intenzione esplicita di scheduling, restituisce un
jobId durevole e lascia al server l'esecuzione di prepare,
content_upload, schedule, riconciliazione e resume. Gli attempt provider
restano distinti e immutabili.
Obiettivo e transizione autorevole
flowchart TD
publish["Issue published"] -.->|"nessun side effect"| ready["Eleggibile"]
ready --> request["request schedule + confirmation"]
request --> commit["D1: campaign + snapshot + job"]
commit --> accepted["202 accepted + jobId"]
commit --> workflow["Workflow: solo jobId"]
workflow --> prepare["prepare"]
prepare --> draft["draft_created"]
draft --> upload["content_upload"]
upload --> content["content_uploaded"]
content --> schedule["schedule"]
schedule --> scheduled["scheduled"]
scheduled -.->|"evidenza distinta"| delivery["delivery"]
La freccia tratteggiata indica un confine, non un automatismo.
La legacy email-campaign-draft Queue, il suo producer e il consumer restano
ritirati. La nuova email-campaign-orchestration Queue rappresenta un concetto
diverso: nasce soltanto dopo un'intenzione esplicita e trasporta esclusivamente
jobId. Pubblicazione e approvazione non creano job né messaggi. Un dispatcher
tecnico ogni cinque minuti recupera outbox o lease scaduti, ma non possiede il
send time: lo scheduling effettivo resta in Mailjet.
Input e fonti accettate
Un Issue è eleggibile soltanto se:
status: published;editorialStatus: approved;- quality gate verdi.
L'applicazione rende subject, HTML e testo dall'Issue Content canonico. Il target proviene dal catalogo server-owned e viene esposto come alias, display name e riferimento mascherato.
Attore e autorizzazione
L'identità deve possedere email:campaigns:schedule. Il browser seleziona soltanto un
target configurato, l'Issue e i metadati temporali richiesti. Credenziali,
lista reale e ID provider non sono input arbitrari del client.
Validazione e quality gate
L'API rilegge l'Issue, la configurazione e l'aggregate prima di ogni
operazione. Ogni richiesta usa lo stesso targetAlias server-owned e lo stesso
scheduledFor RFC3339 scelti alla creazione. Un Campaign Draft attivo
duplicato è rifiutato. content_upload richiede esattamente draft_created;
schedule richiede esattamente content_uploaded. Un mismatch di target,
timestamp, Issue o stato fallisce chiuso.
Responsabilità dell'AI
L'AI non partecipa alla preparazione. HTML e testo sono rendering deterministici del contenuto approvato; non vengono rigenerati dal modello.
Persistenza e audit in D1
issue_email_campaigns conserva lo stato corrente. La stessa transazione D1
crea email_campaign_orchestration_jobs,
email_campaign_content_snapshots e
email_campaign_orchestration_outbox. Lo snapshot contiene subject, HTML,
plain text, hash e revisione Issue ed è immutabile per il job. Il job conserva
fase, lease bounded, contatori di attempt/reconciliation e classificazione
sanitizzata del failure domain. Gli attempt conservano:
operationType:prepare,content_uploadoschedule;providerMode:mock,dry_runolive;- stato
pending,processing,succeeded,failedounknown; - stato durevole della richiesta provider:
not_started,startedounknown, con timestamp separato quando la richiesta parte; - parent del retry;
- evidenza provider sanitizzata.
Un attempt terminale non viene riscritto. Job, outbox e log non contengono recipient, contatti, secret, payload Mailjet grezzi o riferimenti target privati.
Evidenza di successo
L'accettazione 202 prova soltanto la persistenza durevole, non lo scheduling.
Leggere get_email_campaign_job fino a uno stato terminale. Per prepare,
richiedere attempt succeeded e provider draft autorevole. Per
content_upload, richiedere aggregate content_uploaded, attempt riuscito e
provider mode coerente. Per schedule, richiedere aggregate scheduled,
attempt riuscito, timestamp atteso ed evidenza provider; lo scheduling non
prova la delivery.
mock e dry_run provano soltanto il contratto locale. Non provano una
chiamata Mailjet live.
Fallimento e recupero
La stessa idempotencyKey riusa campaign e job. Due richieste concorrenti per
Issue e target non creano due workflow aperti. Due messaggi Queue duplicati non
eseguono due side effect perché soltanto una lease D1 non scaduta può possedere
il job. Una lease scaduta viene classificata WORKER_ABORT e ridispatchata.
Timeout o network failure senza risposta dopo providerRequestState: started
richiedono riconciliazione, mai un nuovo POST cieco. La draft viene adottata
solo per correlation ID univoco; il contenuto solo se coincide byte-per-byte
con lo snapshot; lo schedule solo se coincide con scheduledFor. Zero
candidati esauriscono una policy read-only bounded; più candidati o mismatch
portano a operator_required. 429, 5xx, timeout, network error,
WORKER_ABORT e persistence failure restano classi distinte. Per l'ambiguità
provider seguire il
runbook diagnostico.
Effetti collaterali esclusi
La preparazione:
- non pubblica;
- non carica automaticamente il contenuto;
- non schedula;
- non invia;
- non gestisce contatti o consenso;
- non distribuisce Worker.
Il caricamento contenuto non schedula e non invia. Lo scheduling non equivale a delivery e non modifica contatti, consenso o suppression.
Superfici operator
Il path felice è schedule_newsletter e poi get_email_campaign_job.
L'unica mutazione di stop è abort_email_campaign. Le route stepwise
prepare/upload/schedule/recover/replace/deliver/reconcile sono ritirate.