iltuostudiodigitale/crm-connector

Contratto e client per inviare i contatti raccolti dai form al CRM del gestionale.

Maintainers

Package info

github.com/2P2srl2025/crm-connector

pkg:composer/iltuostudiodigitale/crm-connector

Transparency log

Statistics

Installs: 9

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v2.0.0 2026-08-23 20:19 UTC

This package is auto-updated.

Last update: 2026-08-23 20:20:53 UTC


README

Contratto e client per inviare al CRM del gestionale i contatti raccolti dai form di un sito.

Il pacchetto esiste per un motivo solo: il sito non decide il formato. La submission si costruisce passando da CrmContactData; se i dati non rispettano il contratto, la richiesta non parte.

Installazione

composer require iltuostudiodigitale/crm-connector
php artisan crm-connector:install

Il comando pubblica la configurazione e deposita le istruzioni per gli agenti AI (AGENTS.md) dentro il progetto — in .ai/rules/crm-connector.md se il progetto usa quella convenzione, altrimenti nella root. Dentro vendor/ nessun agente le leggerebbe.

Credenziali

Le rilascia il gestionale in Amministrazione → CRM → Siti, con l'azione «Credenziali». Il client secret è visibile solo al momento della generazione: se lo si perde va rigenerato, e i token già emessi smettono di funzionare.

CRM_BASE_URL=https://gestionale.esempio.it
CRM_SUBMISSION_URL=https://crm.esempio.it/api/v1/form-submission
CRM_CLIENT_ID=9f8e...
CRM_CLIENT_SECRET=...

Il secret va tenuto lato server. Se il form invia da JavaScript nel browser, la POST deve comunque passare da un controller del sito: il secret non va mai messo in una pagina.

Uso

use IlTuoStudioDigitale\CrmConnector\Data\CrmContactData;
use IlTuoStudioDigitale\CrmConnector\Facades\Crm;

$contact = CrmContactData::validateAndCreate([
    'form_name' => 'Contattaci',
    'first_name' => $request->input('nome'),
    'last_name' => $request->input('cognome'),
    'email' => $request->input('email'),
    'phone' => $request->input('telefono'),
    'message' => $request->input('messaggio'),
    'privacy_accepted' => $request->boolean('privacy'),
    'source_url' => $request->fullUrl(),
]);

$result = Crm::submit($contact);   // ['contact_id' => 123, 'created_at' => '...']

In caso di errore viene sollevata CrmSubmissionFailedException, con ->status e ->errors (la mappa di validazione restituita dal gestionale).

Versioni

Il major cambia a ogni rottura del contratto: un campo che sparisce, uno che diventa obbligatorio, una regola più stretta. Mai come minor.

L'URL dell'API (/api/v1) cambia invece solo quando il gestionale deve servire due contratti contemporaneamente, perché ci sono siti in produzione fermi alla versione precedente. Finché tutti i siti installati parlano lo stesso contratto, una major del pacchetto non comporta una nuova versione dell'API.

Il contratto

Campo Obbligatorio Regole
first_name max 150
last_name max 150
privacy_accepted deve essere true
email se manca phone email valida, max 255
phone se manca email max 20
business_name no max 150
fiscal_code no 11–16 caratteri alfanumerici
message no testo libero
newsletter_accepted no default false
source_url no URL valido
referrer_url no URL valido
form_name max 150 — il nome del modulo che ha prodotto il contatto
custom no mappa di risposte libere: max 200 chiavi, ogni valore max 5000

form_name: da dove arriva il dato

È obbligatorio su ogni invio, anche per un semplice modulo di contatto, perché è la chiave con cui il gestionale raggruppa quello che riceve: senza, tutte le richieste di tutti i moduli di un sito finiscono in un unico mucchio indistinto.

Il modo consigliato è un campo nascosto nel modulo, così il nome viaggia insieme ai dati e non va ricordato lato server:

<input type="hidden" name="form_name" value="Questionario mutuo 2026">
'form_name' => $request->input('form_name', 'Contattaci'),

Due accortezze:

  • Usa lo stesso nome, scritto sempre allo stesso modo. Non esiste un elenco dei moduli nel gestionale: il nome è l'unica cosa che li distingue, quindi «Questionario mutuo» e «Questionario mutui» diventano due moduli diversi nei filtri e nei conteggi.
  • Cambia nome quando cambia il modulo in modo incompatibile (per esempio riscrivi le domande di un questionario): così lo storico resta leggibile invece di mescolare risposte a domande diverse sotto la stessa etichetta.

Campi custom e questionari

I campi fissi sopra sono il minimo che ogni form deve avere. Tutto il resto — un questionario di quaranta domande, un preventivatore, una richiesta con campi specifici del sito — va in custom, che accetta qualunque mappa chiave/valore: stringhe, numeri, booleani, null e liste di scalari.

CrmContactData::validateAndCreate([
    'first_name' => 'Mario',
    'last_name' => 'Rossi',
    'email' => 'mario@example.com',
    'privacy_accepted' => $request->boolean('privacy'),

    'form_name' => 'Questionario mutuo 2026',
    'custom' => [
        'reddito_annuo' => 32000,
        'tipo_immobile' => 'prima_casa',
        'ha_mutui_in_corso' => false,
        'province_interesse' => ['NA', 'SA'],
        'note_libere' => null,
    ],
]);

Dentro custom un null è una risposta lasciata in bianco, ed è diverso da una domanda mai posta: per questo custom viene inviato così com'è, senza ripulire i valori vuoti. I limiti (200 chiavi, 5000 caratteri per valore) esistono per non trasformare l'endpoint in un deposito.

Non sono inviabili: il sito di provenienza (deriva dalle credenziali), l'indirizzo IP e lo user agent (li rileva il gestionale), le note interne.

Siti non-Laravel

Il pacchetto è una comodità, non un obbligo tecnico: il contratto è HTTP. Due chiamate:

# 1. token (client credentials)
curl -X POST https://gestionale.esempio.it/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=$CRM_CLIENT_ID \
  -d client_secret=$CRM_CLIENT_SECRET \
  -d scope=crm:submit

# 2. submission
curl -X POST https://crm.esempio.it/api/v1/form-submission \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Accept: application/json" \
  -H "Content-Type: application/json" \
  -d '{"form_name":"Contattaci","first_name":"Mario","last_name":"Rossi","email":"mario@example.com","privacy_accepted":true}'

Risposte: 201 con {"success":true,"data":{"contact_id":…}}, 422 con errors, 401 token assente o scaduto, 403 sito non attivo o origine non consentita, 429 rate limit.