Skip to main content

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:

  1. legge la directory actions;
  2. carica ogni file .action.js;
  3. valida i campi obbligatori;
  4. registra la action per type;
  5. 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

CampoTipoUso
typestringIdentificativo stabile della action. Deve essere UPPER_SNAKE_CASE.
versionstringVersione del contratto action. Viene salvata in mail_rule_actions.action_version.
labelstringNome mostrato nel builder frontend.
descriptionstringTesto breve mostrato sotto la action.
categorystringGruppo UI: EMAIL, DOCUMENT, PARSING, INDEXING, NOTIFICATION, CONTROL.
paramsSchemaobjectSchema logico dei parametri. Serve a documentare e validare la action.
uiSchemaobjectIstruzioni 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

componentUso
selectCombo con opzioni statiche definite in options.
entity-pickerCombo dinamica su entità applicative.
category-pickerSelettore categoria spesa.
textCampo testo breve.
textareaCampo testo lungo.
switchBooleano.

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:

ActionRequiresProduces
LINK_EMAIL_ENTITYEMAILEMAIL_ENTITY_LINK
PARSE_UTILITY_BILLEMAIL, SERVICE_CONTRACTDOCUMENT, UTILITY_BILL, DOCUMENT_INDEX
PARSE_CONDOMINIUM_INSTALLMENTSEMAIL, SERVICE_CONTRACTDOCUMENT, EXPECTED_EXPENSES, DOCUMENT_INDEX
STOP_PROCESSINGEMAILPIPELINE_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:

CampoDescrizione
userIdUtente autenticato.
partyIdParty collegato all'utente. Usato per ownership.
accountMail account in elaborazione.
messageEmail originale.
effectiveMessageEmail più patch prodotta dalle action precedenti.
patchModifiche accumulate da applicare a mail_messages.
repositoryAccesso datahub usato dal microservizio.
loggerLogger del microservizio.
utilityBillAttachmentProcessorProcessor 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:

CampoUso
appliedtrue se l'azione ha prodotto effetto.
patchCampi da aggiornare su mail_messages.
resultDettaglio audit salvato in mail_rule_matches.actions_result_json.
stopSe true, ferma la pipeline/regole successive.
skippedSe true, azione saltata senza errore.
skip_reasonMotivo 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

  1. Creare backend/mail-ingestion-service/modules/rules/actions/<name>.action.js.
  2. Definire type, version, label, description, category.
  3. Definire paramsSchema.
  4. Definire uiSchema.fields usando solo componenti supportati.
  5. Definire requires, produces, allowedAfter, allowedBefore.
  6. Implementare isAlreadyApplied se la action crea record o documenti.
  7. Implementare execute.
  8. Validare ownership tramite ctx.partyId.
  9. Restituire patch solo per campi di mail_messages.
  10. Restituire result sintetico e utile per audit.
  11. Eseguire:
node --check backend/mail-ingestion-service/modules/rules/actions/<name>.action.js
  1. 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));"
  1. 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
  1. 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_type sono legacy.
  • Ogni action che scrive record deve essere idempotente.
  • Ogni action che accede a entità business deve verificare ownership.