Mail Rule Actions
Le Mail Rule Actions sono moduli backend caricati a runtime dal mail-ingestion-service.
Servono a definire le azioni disponibili nel builder delle regole email. Ogni action dichiara:
- come deve essere mostrata nel frontend;
- quali parametri richiede;
- quali prerequisiti ha;
- quali output produce;
- come deve essere eseguita dal runner.
L'obiettivo è rendere estendibile l'automazione: per aggiungere una nuova azione si crea un file nella directory ACTIONS, senza hardcodare nuove opzioni nel frontend.
Directory
I moduli action vivono in:
backend/mail-ingestion-service/modules/rules/actions/
Ogni action deve essere in un file:
<nome>.action.js
Esempi:
linkEmailEntity.action.js
parseUtilityBill.action.js
parseCondominiumInstallments.action.js
stopProcessing.action.js
Il registry carica automaticamente tutti i file che terminano con .action.js.
Registry
Il loader è:
backend/mail-ingestion-service/modules/rules/actions/actionRegistry.js
All'avvio del microservizio:
- legge la directory
actions; - carica ogni file
.action.js; - valida i campi obbligatori;
- registra la action per
type; - espone le definizioni al frontend.
Endpoint esposto:
GET /mail-ingestion-service/rule-actions/
La risposta contiene solo metadata frontend-safe. Le funzioni execute e isAlreadyApplied non vengono esposte.
Contratto Del Modulo
Ogni file action deve esportare un oggetto CommonJS:
"use strict";
module.exports = {
type: "MY_ACTION",
version: "1.0.0",
label: "Nome visibile",
description: "Descrizione breve per il builder.",
category: "PARSING",
legacyTypes: [],
paramsSchema: {
type: "object",
required: ["example_id"],
properties: {
example_id: { type: "integer", minimum: 1 },
},
},
uiSchema: {
fields: [
{
name: "example_id",
label: "Record",
component: "entity-picker",
entityType: "SERVICE_CONTRACT",
},
],
},
requires: ["EMAIL"],
produces: ["DOCUMENT"],
allowedAfter: ["LINK_EMAIL_ENTITY"],
allowedBefore: ["STOP_PROCESSING"],
async isAlreadyApplied(ctx, params, action) {
return false;
},
async execute(ctx, params, action) {
return {
applied: true,
patch: {},
result: {},
};
},
};
Campi Obbligatori
| Campo | Tipo | Uso |
|---|---|---|
type | string | Identificativo stabile della action. Deve essere UPPER_SNAKE_CASE. |
version | string | Versione del contratto action. Viene salvata in mail_rule_actions.action_version. |
label | string | Nome mostrato nel builder frontend. |
description | string | Testo breve mostrato sotto la action. |
category | string | Gruppo UI: EMAIL, DOCUMENT, PARSING, INDEXING, NOTIFICATION, CONTROL. |
paramsSchema | object | Schema logico dei parametri. Serve a documentare e validare la action. |
uiSchema | object | Istruzioni per generare i controlli frontend. |
Il registry rifiuta moduli senza questi campi.
type
type è il valore salvato in:
mail_rule_actions.action_type
Regole:
- usare solo maiuscole, numeri e underscore;
- non riusare type già esistenti;
- non cambiare type dopo il rilascio, altrimenti le regole salvate non lo troveranno più.
Esempi validi:
LINK_EMAIL_ENTITY
PARSE_UTILITY_BILL
PARSE_CONDOMINIUM_INSTALLMENTS
STOP_PROCESSING
paramsSchema
paramsSchema descrive i parametri salvati in:
mail_rule_actions.params_json
Esempio:
paramsSchema: {
type: "object",
required: ["person_name"],
properties: {
service_contract_id: { type: "integer", minimum: 1 },
person_name: { type: "string", minLength: 1 },
expense_category_id: { type: "integer", minimum: 1 },
},
}
Il frontend non deve dedurre significati da value_text, value_id o target_type per le nuove action. Deve usare params_json.
I campi legacy restano solo per compatibilità con le vecchie regole.
uiSchema
uiSchema è la parte più importante per il frontend.
Definisce come costruire la form parametri senza hardcoding.
Struttura:
uiSchema: {
fields: [
{
name: "service_contract_id",
label: "Fornitura / fornitore",
component: "entity-picker",
entityType: "SERVICE_CONTRACT",
optional: true,
help: "Se vuoto usa la fornitura collegata da una azione precedente.",
},
],
}
Componenti Supportati
component | Uso |
|---|---|
select | Combo con opzioni statiche definite in options. |
entity-picker | Combo dinamica su entità applicative. |
category-picker | Selettore categoria spesa. |
text | Campo testo breve. |
textarea | Campo testo lungo. |
switch | Booleano. |
Il frontend può renderizzare automaticamente una action solo se usa componenti supportati.
select
{
name: "document_type",
label: "Tipo documento",
component: "select",
options: [
{ value: "UTILITY_BILL", label: "Bolletta" },
{ value: "CONDOMINIUM_INSTALLMENTS", label: "Rate condominio" },
],
}
entity-picker
{
name: "entity_id",
label: "Record",
component: "entity-picker",
dependsOn: "entity_type",
}
Oppure con entità fissa:
{
name: "service_contract_id",
label: "Fornitura",
component: "entity-picker",
entityType: "SERVICE_CONTRACT",
}
Per filtrare solo i condomini:
{
name: "service_contract_id",
label: "Fornitura condominio",
component: "entity-picker",
entityType: "SERVICE_CONTRACT",
filters: { serviceType: "CONDOMINIUM" },
}
Nota: nel modello attuale Condominio è una fornitura/fornitore, quindi usa SERVICE_CONTRACT.
requires E produces
Questi campi descrivono la compatibilità logica tra action.
requires: ["EMAIL", "SERVICE_CONTRACT"],
produces: ["DOCUMENT", "EXPECTED_EXPENSES", "DOCUMENT_INDEX"],
Sono usati dal frontend per:
- mostrare warning;
- filtrare azioni incompatibili;
- suggerire azioni precedenti;
- rendere la sequenza più leggibile.
Sono usati dal backend come documentazione eseguibile del contratto. La validazione runtime deve comunque stare in execute.
Esempi:
| Action | Requires | Produces |
|---|---|---|
LINK_EMAIL_ENTITY | EMAIL | EMAIL_ENTITY_LINK |
PARSE_UTILITY_BILL | EMAIL, SERVICE_CONTRACT | DOCUMENT, UTILITY_BILL, DOCUMENT_INDEX |
PARSE_CONDOMINIUM_INSTALLMENTS | EMAIL, SERVICE_CONTRACT | DOCUMENT, EXPECTED_EXPENSES, DOCUMENT_INDEX |
STOP_PROCESSING | EMAIL | PIPELINE_STOP |
allowedAfter E allowedBefore
Sono suggerimenti di composizione per il builder.
allowedAfter: ["LINK_EMAIL_ENTITY", "SAVE_DOCUMENT"],
allowedBefore: ["INDEX_EMAIL", "STOP_PROCESSING"],
Non devono essere l'unico controllo di sicurezza.
Il backend deve sempre validare:
- permessi utente;
- ownership dell'entità;
- parametri obbligatori;
- file presenti;
- output duplicati.
legacyTypes
Serve per mappare vecchie action su nuove action modulari.
Esempio:
legacyTypes: [
"LINK_PROPERTY",
"LINK_SERVICE_CONTRACT",
"LINK_CONTRACT",
"LINK_SUPPLIER_PARTY",
"LINK_TENANT_PARTY",
],
Il registry può usare questa lista per riconoscere regole salvate prima della migrazione.
Nuove regole non dovrebbero più usare action legacy.
Context Runtime
Durante l'esecuzione il runner passa alla action un context:
ctx = {
userId,
partyId,
account,
message,
effectiveMessage,
rule,
runId,
patch,
repository,
logger,
utilityBillAttachmentProcessor,
};
Campi principali:
| Campo | Descrizione |
|---|---|
userId | Utente autenticato. |
partyId | Party collegato all'utente. Usato per ownership. |
account | Mail account in elaborazione. |
message | Email originale. |
effectiveMessage | Email più patch prodotta dalle action precedenti. |
patch | Modifiche accumulate da applicare a mail_messages. |
repository | Accesso datahub usato dal microservizio. |
logger | Logger del microservizio. |
utilityBillAttachmentProcessor | Processor esistente riusabile per download allegati/link, upload e indexing documento. |
isAlreadyApplied
Serve per rendere le action idempotenti.
Una action deve evitare duplicati quando una regola viene rilanciata sulla stessa email.
Può ritornare:
false: action non ancora applicata;true: action già applicata;- un oggetto: action già applicata con dettagli da salvare in
actions_result_json.
Esempio:
async isAlreadyApplied(ctx) {
const existing = await ctx.repository.findMailMessageDocument({
messageId: ctx.message.id,
detectedDocumentType: "UTILITY_BILL",
linkedEntityType: "utility_bills",
});
if (!existing?.id) return false;
return {
mail_message_document_id: existing.id,
document_id: existing.document_id,
utility_bill_id: existing.linked_entity_id,
};
}
execute
execute contiene la logica applicativa.
Firma:
async execute(ctx, params, action) {
return {
applied: true,
patch: {},
result: {},
stop: false,
skipped: false,
skip_reason: null,
};
}
Output:
| Campo | Uso |
|---|---|
applied | true se l'azione ha prodotto effetto. |
patch | Campi da aggiornare su mail_messages. |
result | Dettaglio audit salvato in mail_rule_matches.actions_result_json. |
stop | Se true, ferma la pipeline/regole successive. |
skipped | Se true, azione saltata senza errore. |
skip_reason | Motivo dello skip. |
Esempio semplice:
async execute(_ctx, params) {
return {
applied: true,
patch: {
property_id: Number(params.entity_id),
processing_status: "PROCESSED",
},
result: {
linked_fields: ["property_id"],
},
};
}
Errori
Se una action non può proseguire deve lanciare un errore con messaggio leggibile.
const err = new Error("service_contract_id mancante: collega prima l'email alla fornitura.");
err.status = 400;
throw err;
Il runner:
- marca la mail come
ERROR; - scrive
processing_error; - scrive il match con
error_message; - aggiorna
mail_rule_runs.errors_count.
Usare errori solo per problemi bloccanti. Se il caso è atteso e non bloccante, usare skipped.
Sicurezza E Multiutente
Ogni action deve rispettare il multiutente.
Regole:
- non fidarsi degli ID in
params_json; - verificare sempre accesso via
partyId; - usare repository o endpoint sicuri quando disponibili;
- non accedere a documenti, property, contratti o forniture senza ownership;
- non salvare dati cross-user.
Esempio: per una fornitura, verificare che la property del service contract sia collegata al partyId dell'utente.
Salvataggio Risultati
Ogni action produce un record audit in:
mail_rule_matches.actions_result_json
Il risultato dovrebbe contenere ID utili per debug e retry:
{
"action_type": "PARSE_CONDOMINIUM_INSTALLMENTS",
"applied": true,
"result": {
"document_id": 42,
"mail_message_document_id": 7,
"expected_expenses": [
{ "id": 101, "created": true },
{ "id": 102, "created": true }
]
}
}
Non salvare nel result contenuti lunghi come corpo email completo, testo OCR completo o PDF base64.
Endpoint Frontend
Il frontend legge:
GET /mail-ingestion-service/rule-actions/
E si aspetta:
{
"ok": true,
"actions": [
{
"type": "PARSE_UTILITY_BILL",
"version": "1.0.0",
"label": "Parserizza bolletta",
"description": "Estrae i dati della bolletta...",
"category": "PARSING",
"paramsSchema": {},
"uiSchema": { "fields": [] },
"requires": ["EMAIL", "SERVICE_CONTRACT"],
"produces": ["DOCUMENT", "UTILITY_BILL"],
"allowedAfter": ["LINK_EMAIL_ENTITY"],
"allowedBefore": ["STOP_PROCESSING"],
"implemented": true
}
]
}
implemented: false significa che il frontend può mostrare l'action come pianificata ma deve impedirne la selezione per nuove regole.
Checklist Nuova Action
- Creare
backend/mail-ingestion-service/modules/rules/actions/<name>.action.js. - Definire
type,version,label,description,category. - Definire
paramsSchema. - Definire
uiSchema.fieldsusando solo componenti supportati. - Definire
requires,produces,allowedAfter,allowedBefore. - Implementare
isAlreadyAppliedse la action crea record o documenti. - Implementare
execute. - Validare ownership tramite
ctx.partyId. - Restituire
patchsolo per campi dimail_messages. - Restituire
resultsintetico e utile per audit. - Eseguire:
node --check backend/mail-ingestion-service/modules/rules/actions/<name>.action.js
- Verificare che il registry la carichi:
node -e "const { ActionRegistry } = require('./backend/mail-ingestion-service/modules/rules/actions/actionRegistry'); const r = new ActionRegistry({ actionsDir: './backend/mail-ingestion-service/modules/rules/actions' }); console.log(r.listDefinitions().map(a => a.type));"
- Rebuild del microservizio:
docker compose --env-file docker/.env.dev -p local -f docker/docker-compose.dev.yml up -d --build --no-deps mail-ingestion-service
- Verificare l'endpoint:
curl http://localhost:3011/rule-actions
Esempio Completo
"use strict";
module.exports = {
type: "STOP_PROCESSING",
version: "1.0.0",
label: "Ferma elaborazione",
description: "Interrompe l'esecuzione delle azioni successive e delle regole successive.",
category: "CONTROL",
legacyTypes: ["STOP"],
paramsSchema: {
type: "object",
properties: {},
},
uiSchema: {
fields: [],
},
requires: ["EMAIL"],
produces: ["PIPELINE_STOP"],
async execute() {
return {
applied: true,
stop: true,
result: {
stopped: true,
},
};
},
};
Convenzioni
- Una action deve fare una cosa chiara.
- Le action tecniche interne non devono essere esposte se non servono all'utente.
- Il frontend deve mostrare azioni parlanti, non concetti come
target_type = PIPELINE. - Il backend deve restare la fonte di verità delle action disponibili.
params_jsonè il contratto nuovo;value_text,value_id,target_typesono legacy.- Ogni action che scrive record deve essere idempotente.
- Ogni action che accede a entità business deve verificare ownership.