«Borren nuestra cuenta» suena a una sola sentencia DELETE. Nunca lo es.
Cuando un socio deja la License API, eliminar a su consumidor toca cinco cosas, y cada una tiene una respuesta correcta distinta:
el consumidor se va
tokens borrar ya: son credenciales
secretos borrar ya: token de proveedor, secreto webhook
log conservar hasta que la retención lo elimine
la fila borrado lógico ya, purga a los 30 días
licencias no son nuestras: viven en el proveedor
El método destroy del Capítulo 5 hace la primera, la segunda y la cuarta en una sola transacción. Si eliminar un consumidor tiene además que iniciar trabajo en otra parte, despáchalo desde dentro de esa transacción y retenlo hasta el commit:
RevokeProviderAccess::dispatch($consumer->id)
->afterCommit();
Primero las credenciales y los secretos. Nada más importa si los tokens siguen funcionando, o si el token de proveedor de un socio sobrevive en tu base de datos a la relación con ese socio.
Una transacción, y trabajo que sale de ella solo después del commit. afterCommit() retiene el job hasta que la transacción se ha confirmado de verdad. Sin él, un worker puede tomar el job antes de que existan las filas de las que depende, o después de que un rollback las haya deshecho. (Documentación de Laravel: Queues › Jobs & Database Transactions.)
No todo es tuyo para borrarlo. Las licencias pertenecen a la cuenta que el socio tiene en el proveedor. Saber dónde termina tu responsabilidad forma parte de la respuesta.
«Borrar» no significa la misma operación en todas las tablas. Significa desaparecido para siempre en lo que existía solo a causa de esta identidad, y puede significar conservado, sin la identidad, en los registros que tienes una razón declarada para guardar: una factura que exige la ley, un recuento que alimenta un informe. La prueba es si puedes decirle la razón en voz alta a la persona que pregunta. «Lo guardamos porque podría ser útil» no es una razón. Si hay datos personales de por medio, lo deciden las normas de retención de las jurisdicciones en las que operas, no tu esquema.
Archivar los datos fríos
Borrar un registro y perder la capacidad de responder «¿cuánto nos usó esta cuenta antes de irse?» son dos resultados distintos. Archivar impide que el segundo desaparezca con el primero.
Un servicio hermano que recoge analíticas hace esto cuando se borra un sitio. No conserva las filas crudas. Conserva un resumen:
// app/Listeners/ArchiveSiteAnalytics.php
public function handle(SiteDeleted $event): void
{
Archive::create([
'site_id' => $event->siteId,
'summary' => $event->totals,
'archived_at' => now(),
]);
}
El archivo es una instantánea, no una copia. Una fila de totales por sitio borrado, donde la tabla viva tenía millones. Eso es lo que mantiene barato el archivo para siempre e impide que se convierta en una segunda copia del problema de crecimiento que estabas resolviendo.
Es un listener, no parte del borrado. Cualquier cosa que dispare SiteDeleted recibe la misma garantía, sin que el archivado se repita en cada lugar desde el que se puede borrar un sitio.
Alguien puede encontrarlo. Un archivo que nadie sabe consultar seis meses después no es un archivo. Solo retrasaste el borrado.
Demostrar qué conservas y por qué
Una regla de retención cumple su función solo si alguien puede señalarla y decir «esta es la razón por la que conservamos esto, y esta es la prueba de que se ejecuta». Una política que nadie puede verificar no se gana la confianza de nadie, ni siquiera del equipo que la escribió.
Dos cosas hacen demostrable una política. Los umbrales viven en la configuración, como retention.api_requests_days más arriba, y no en un número enterrado en un método. La política se cambia cambiando un valor. Y la evidencia se puede inspeccionar:
php artisan model:prune --pretend
Eso imprime cuántas filas borraría cada modelo purgable, sin borrar ninguna. Ejecútalo antes de acortar una ventana de retención, y sabrás lo que hará el cambio.
Resumen del capítulo 17
Dueños:
- Cada tabla y cada campo JSON tiene un dueño y una razón de existir de una línea.
- Si nadie sabe por qué se conserva una tabla, trátalo como un bug.
Retención:
- La retención vive en el modelo, con
PrunableoMassPrunable. - Un solo
model:pruneprogramado cubre todos los modelos que se apuntan.
Cambios de esquema y backfills:
- Añade, no cambies. Haz que las migraciones sean seguras de ejecutar dos veces.
- Recorre las tablas en uso con
chunkById(), nunca conchunk()basado en offset. - Los backfills son comandos con un ensayo en seco y una comprobación que salta lo que no cambia.
Borrado y archivado:
- Decide, tabla por tabla, qué elimina una petición de borrado y qué conserva, y sé capaz de decir por qué.
- Despacha el trabajo posterior con
afterCommit(). - Archiva un resumen, desde un listener, donde alguien pueda encontrarlo.