@@ -7,24 +7,56 @@ Sincroniza estudiantes (con padres y planes de pago) desde PostgreSQL hacia Mong
...
@@ -7,24 +7,56 @@ Sincroniza estudiantes (con padres y planes de pago) desde PostgreSQL hacia Mong
1. La tabla `sync_state` se crea/inicializa automáticamente al arrancar (fila `id=1` estudiantes) — no se requiere paso de migración manual (`migrations/0001_create_sync_state.sql` se conserva como referencia).
1. La tabla `sync_state` se crea/inicializa automáticamente al arrancar (fila `id=1` estudiantes) — no se requiere paso de migración manual (`migrations/0001_create_sync_state.sql` se conserva como referencia).
2. Asegúrate de que la colección `students` exista en Mongo con el validador `$jsonSchema` provisto (requeridos: `student_id` int, `enrollment_id` int). La colección `deleted_students` archiva los estudiantes eliminados en el origen — no requiere configuración de esquema.
2. Asegúrate de que la colección `students` exista en Mongo con el validador `$jsonSchema` provisto (requeridos: `student_id` int, `enrollment_id` int). La colección `deleted_students` archiva los estudiantes eliminados en el origen — no requiere configuración de esquema.
3. Define las variables de entorno: `PG_DSN`, `MONGO_URI`, `MONGO_DB=intranet` (o tu base de datos), `SYNC_INTERVAL` (por defecto `5m`), `PORT` (por defecto `8080`). Los nombres de función PG / colección Mongo por pipeline ya no son variables de entorno — se definen en `internal/config/pipelines.go`. Agrega un nuevo pipeline añadiendo una entrada ahí, no variables de entorno.
3. Define las variables de entorno: `PG_DSN`, `MONGO_URI`, `MONGO_DB=intranet` (o tu base de datos), `SYNC_INTERVAL` (por defecto `5m`), `PORT` (por defecto `8080`). Los nombres de función PG / colección Mongo por pipeline ya no son variables de entorno — se definen en `internal/config/pipelines.go`. Agrega un nuevo pipeline añadiendo una entrada ahí, no variables de entorno.
4.`go run ./cmd/server`
4. Horario por pipeline (opcional, `SYNC_<NOMBRE>_...` en mayúsculas, ej. `SYNC_STUDENTS_MODE`, `SYNC_PAYMENT_PLANS_MODE`, `SYNC_PARENTS_MODE`):
-`SYNC_<NOMBRE>_MODE`: `interval` (por defecto) o `daily`.
-`SYNC_<NOMBRE>_INTERVAL`: duración tipo `5m`/`1h` (modo `interval`; si falta, usa `SYNC_INTERVAL`).
-`SYNC_<NOMBRE>_TIME`: hora `HH:MM` (modo `daily`, obligatoria en ese modo). Ej: `SYNC_STUDENTS_MODE=daily` + `SYNC_STUDENTS_TIME=03:00` corre estudiantes una vez al día a las 3am.
5.`go run ./cmd/server`
## Endpoints
## Endpoints
-`GET /health` — verifica la conectividad con Postgres y Mongo.
Todos los endpoints llevan el prefijo `/api/v1`.
-`POST /sync/students/trigger` — ejecuta un ciclo de sincronización de estudiantes de inmediato.
-`GET /sync/students/status` — última ejecución de sincronización de estudiantes, filas sincronizadas (desglose creado/actualizado/eliminado), último error si lo hay.
-`GET /api/v1/health` — verifica la conectividad con Postgres y Mongo.
-`POST /api/v1/sync/students/trigger` / `GET /api/v1/sync/students/status` — pipeline de estudiantes: ejecutar ahora / última ejecución, filas sincronizadas (desglose creado/actualizado/sin cambios/eliminado), último error si lo hay.
-`POST /api/v1/sync/payment_plans/trigger` / `GET /api/v1/sync/payment_plans/status` — pipeline de planes de pago (misma forma).
Cada colección (`students`, `payment_plans`, `parents`, `users`) es un paquete Go propio bajo `internal/<módulo>` (`internal/students`, `internal/paymentplans`, `internal/parents`, `internal/users`):
-`entity.go` — struct del documento Mongo (implementa `SetUpdatedAt` y `SetRowHash`).
-`query.go` — `const Query`, el SQL crudo de origen.
-`reader.go` — `QueryReader`/`NewQueryReader`.
Agregar un módulo nuevo = paquete `internal/<módulo>` + entrada en `internal/config.Pipelines` + case en `cmd/server/main.go` + endpoints en `internal/api`, sin nuevas variables de entorno. Las capas transversales (`sync.Service`, `sync.Scheduler`, `sync.StateStore`, `db.CollectionUpserter`, `db.DeletedMover`) son genéricas y se comparten entre módulos.
## Regla de `_id` en Mongo
**`_id` nunca es el id de negocio.** Todas las colecciones dejan que Mongo genere su propio ObjectID; el id de negocio (`student_id`, `payment_plan_id`, `parent_id`, `user_id`) vive como campo normal del documento, el mismo declarado en `PipelineDef.IDField`. Los upserts y la reconciliación de borrados filtran por ese campo, no por `_id`. Al archivar en `deleted_*` se descarta el `_id` original para que se genere uno nuevo. Requiere índice único sobre `idField` en cada colección Mongo (no lo impone el código).
## Contratos del origen Postgres
## Contratos del origen Postgres
Estudiantes: una consulta SQL cruda (no una función) que une `matricula.ma_estudiante`/`persona.pe_persona`/`matricula.ma_matricula`/`caja.ca_plan_de_pago`, agrupada por estudiante. Cada fila se convierte en un documento Mongo, upsert por `_id = student_id`, con `updated_at` estampado por este servicio. Requiere `student_id` y `enrollment_id` numéricos.
Estudiantes: una consulta SQL cruda (no una función) que une `matricula.ma_estudiante`/`persona.pe_persona`/`matricula.ma_matricula`/`caja.ca_plan_de_pago`, agrupada por estudiante. Cada fila se convierte en un documento Mongo, upsert por `student_id`, con `updated_at` estampado por este servicio. Requiere `student_id` y `enrollment_id` numéricos.
Planes de pago: una consulta SQL propia, un documento Mongo por plan de pago, upsert por `payment_plan_id`.
Padres/apoderados: une `persona.pe_persona` con una subconsulta agregada sobre `matricula.ma_estudiante_apoderado` (una fila por padre/apoderado, con los estudiantes asociados embebidos vía `JSON_AGG`). Upsert por `parent_id`.
Usuarios: una fila por login (`user_id`, `user_login`, `user_password`, `user_creation_date`, `parent_id`, `user_status`). Upsert por `user_id`.
## Comportamiento de sincronización
## Comportamiento de sincronización
Cada ciclo corre en concurrencia (pool de workers acotado, 10 en vuelo por defecto) para que una migración completa termine más rápido que un bucle secuencial:
Cada ciclo tiene dos fases:
1.**Hash + diff (CPU, concurrente)**: un pool de workers acotado (10 en vuelo por defecto) calcula un hash SHA-256 (`row_hash`) de cada fila de Postgres y lo compara contra el `row_hash` ya guardado en Mongo para ese id (traído de antemano en una sola consulta). Las filas cuyo hash coincide se saltan por completo — sin escritura — y se cuentan como `Unchanged`. Solo las filas nuevas o cambiadas reciben `updated_at`/`row_hash` y pasan a la fase 2.
2.**Escritura en lote (I/O, un solo round trip)**: todas las filas cambiadas se mandan en un único `BulkWrite` a Mongo (upsert por `idField`).
-**Registro nuevo**: id aún no en Mongo → insertado, contado como `Created`.
-**Registro nuevo**: id aún no en Mongo → insertado, contado como `Created`.
-**Registro existente**: id ya en Mongo → campos sobrescritos, contado como `Updated`.
-**Registro existente, cambiado**: hash distinto al guardado → campos sobrescritos, contado como `Updated`.
-**Registro existente, sin cambios**: hash igual al guardado → no se escribe nada, contado como `Unchanged`.
-**Registro eliminado**: id presente en Mongo pero ya no devuelto por el origen Postgres → el documento se mueve (no solo se elimina) a la colección de archivo (`deleted_students`) con una marca `deleted_at`, contado como `Deleted`.
-**Registro eliminado**: id presente en Mongo pero ya no devuelto por el origen Postgres → el documento se mueve (no solo se elimina) a la colección de archivo (`deleted_students`) con una marca `deleted_at`, contado como `Deleted`.
El estado persistido (`sync_state`) solo avanza si el ciclo completo (upserts + reconciliación de borrados) tiene éxito; cualquier fallo se corta y se reporta vía `/sync/students/status`.
El estado persistido (`sync_state`) solo avanza si el ciclo completo (hash/upsert + reconciliación de borrados) tiene éxito; cualquier fallo se corta y se reporta vía `/api/v1/sync/<módulo>/status`.