Confirma sólo cuando es real
$provisioning->update(['response' => $result['data']]);
$lead->update(['status' => LeadStatus::Customer->value]);
El estado cambia después de que la llamada externa haya tenido éxito, no cuando se intentó. La fila creada al principio era una reclamación; esto es la confirmación. Una reclamación que nunca se confirma es identificable y recolectable —un estado fijado con optimismo es indistinguible de uno real, para siempre.
La idempotencia es una decisión por herramienta
El capítulo 6 fijó tries = 1 en el job, y esta es la razón. La política de reintentos no puede vivir a nivel de job en cuanto un turno tiene efectos secundarios, porque un turno es una mezcla de herramientas con semánticas de reintento completamente distintas.
Comprobar una URL es idempotente: llámala cincuenta veces y no cambia nada. Sugerir paletas es idempotente. Crear un sitio no lo es, ni enviar un correo tampoco.
Así que el bucle no reintenta nunca, y cada herramienta se responsabiliza de su propia seguridad ante la repetición:
- Idempotente por naturaleza — herramientas de sólo lectura. Nada que hacer.
- Idempotente por una restricción — creación protegida por un índice único sobre la clave natural.
- Idempotente por un token de un solo uso — el token es el permiso, se gasta una vez.
- Genuinamente insegura de repetir — no debería ser una herramienta que el modelo pueda llamar libremente, que es el capítulo 11.
Pregúntale a cada herramienta: ¿qué pasa si esto se ejecuta dos veces? Si la respuesta no es «nada», la restricción que lo hace seguro va en la herramienta —no en una descripción, y no en el bucle.
Dile al usuario qué ha pasado
return Response::json([
'site_id' => $siteId,
'status' => $provisioning->status,
'message' => 'Site provisioning has started.',
'expected_url' => 'https://'.$url.'.example',
'site_details' => $siteDetails,
'account_url' => $accountActionUrl ?: null,
'subscription_deadline' => 'Users must subscribe before the trial window ends
to keep the site active. Use the window stated in your instructions —
never state a duration of your own.',
]);
El resultado de una acción irreversible debería ser lo más informativo posible, porque es el recibo del usuario. No «hecho», sino qué se creó, dónde estará, qué debe hacer a continuación y para cuándo.
Ese último campo merece atención: le dice al modelo que se remita a sus instrucciones en lugar de inventarse un número. Es la regla del capítulo 2 sobre datos que el modelo no puede saber, aplicada donde equivocarse sería una promesa comercial falsa.
Lo que sostiene este capítulo
- Una instrucción describe una intención; una restricción la impone. La unicidad no se consigue pidiéndola con educación.
- Persiste primero una reclamación y deja que un índice único sobre la clave natural rechace el duplicado.
- Captura la violación de restricción y tradúcela a algo sobre lo que el modelo pueda actuar, en vez de un 500 que quizá reintente.
- Un token de un solo uso necesita dueño durante toda su vida. Sácalo, guárdalo y devuélvelo en todos los caminos de fallo.
- Nunca mantengas una transacción a través de una llamada de red lenta. Un servicio externo colgado no debería agotar tu pool de conexiones.
- Revierte antes de reportar. La observabilidad puede fallar; las compensaciones deben ejecutarse primero.
- Confirma el estado sólo cuando el trabajo es real, para que las reclamaciones sin confirmar sigan siendo identificables.
- La política de reintentos va por herramienta, no por job. Pregunta qué pasa si cada una se ejecuta dos veces.
- Haz de un resultado irreversible un recibo —qué se creó, dónde, qué hacer luego y para cuándo.