- `POST /api/v1/sync/balotarios/trigger` / `GET /api/v1/sync/balotarios/status` — pipeline de balotarios (misma forma). La primera corrida tarda ~2 min, así que usar `curl -m 290`.
- `POST /api/v1/sync/users/upsert` — upsert puntual de un solo usuario, sin esperar al ciclo programado. Body `{"id": <user_id>}`; relee esa fila de `intranet.users` en Postgres y la refleja en la colección `users` de Mongo. Responde `{"status": true, "module": "users", "id": N, "outcome": "created|updated|unchanged"}`, `404` si el id no existe en el origen. Lo consume Trismegisto al crear usuario o restablecer contraseña desde `padresIntranet.jsp`. Solo se registra para los módulos con lector por id en `pipeline.rowReaderFor` (hoy `users`); agregar un `case` ahí más un `FetchByID` en el reader habilita el endpoint para otro módulo.
- `POST /api/v1/sync/users/upsert` — upsert puntual de un solo usuario, sin esperar al ciclo programado. Body `{"id": <user_id>}`; relee esa fila de `intranet.users` en Postgres y la refleja en la colección `users` de Mongo. Responde `{"status": true, "module": "users", "id": N, "outcome": "created|updated|unchanged"}`, `404` si el id no existe en el origen. Lo consume Trismegisto al crear usuario o restablecer contraseña desde `padresIntranet.jsp`. Solo se registra para los módulos con lector por id en `pipeline.rowReaderFor` (hoy `users`); agregar un `case` ahí más un `FetchByID` en el reader habilita el endpoint para otro módulo.
Users: `internal/infrastructure/database/postgres.UserQuery`, one row per `matricula.ma_usuario`-equivalent login (`user_id`, `user_login`, `user_password`, `user_creation_date`, `parent_id`, `user_status`). Read by `postgres.UserReader` into `entities.User` — collection `users`, archive `deleted_users`, `IDField` = `user_id`, `StateID` = 4.
Users: `internal/infrastructure/database/postgres.UserQuery`, one row per `matricula.ma_usuario`-equivalent login (`user_id`, `user_login`, `user_password`, `user_creation_date`, `parent_id`, `user_status`). Read by `postgres.UserReader` into `entities.User` — collection `users`, archive `deleted_users`, `IDField` = `user_id`, `StateID` = 4.
Balotarios: `internal/infrastructure/database/postgres.BalotarioQuery`, un documento por matrícula activa del período 14 / ciclo 28 (~35 700), con los 8 períodos pedagógicos (M1, B1, M2, B2, M3, B3, M4, B4) siempre presentes y sus cursos anidados (`cursos: []` si no hay nada configurado). Los cursos salen de `academico.ac_balotario_aula` por el aula actual **más** los balotarios que el alumno ya respondió (por si cambió de aula); **no** se filtra por `grado_id`, porque las aulas multigrado y "PRE" reciben a propósito los balotarios de otro grado. Leído por `postgres.BalotarioReader` hacia `entities.Balotario`: colección `c_balotario`, archivo `deleted_c_balotario`, `IDField` = `matricula_id`, `StateID` = 6. Lo consume `GET /enrollments/:id/balotarios` de intranet-so-be. Detalle y hallazgos de datos en `intranet-padres-react/docs/backend-queries.md`.
## Mongo `_id` rule
## Mongo `_id` rule
**`_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`, etc.) vive como campo normal del documento — el mismo declarado en `PipelineDef.IDField` (`internal/infrastructure/config/pipelines.go`). `repository.DocumentRepository` y `repository.DeletedRepository` reciben ese `idField` y filtran/listan/archivan por él, no por `_id`. Al archivar en la colección `deleted_*`, se descarta el `_id` original para que se genere uno nuevo. Cualquier pipeline nuevo debe seguir esta misma regla — no volver a usar `_id = <id de negocio>`. Requiere índice único sobre `idField` en cada colección Mongo (no lo impone el código).
**`_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`, etc.) vive como campo normal del documento — el mismo declarado en `PipelineDef.IDField` (`internal/infrastructure/config/pipelines.go`). `repository.DocumentRepository` y `repository.DeletedRepository` reciben ese `idField` y filtran/listan/archivan por él, no por `_id`. Al archivar en la colección `deleted_*`, se descarta el `_id` original para que se genere uno nuevo. Cualquier pipeline nuevo debe seguir esta misma regla — no volver a usar `_id = <id de negocio>`. Requiere índice único sobre `idField` en cada colección Mongo (no lo impone el código).
...
@@ -94,6 +97,7 @@ El `row_hash` se calcula sobre el JSON del documento, así que **cualquier array
...
@@ -94,6 +97,7 @@ El `row_hash` se calcula sobre el JSON del documento, así que **cualquier array
Implementado hoy en:
Implementado hoy en:
- `parent_query.go` — `JSONB_AGG(DISTINCT jsonb_build_object('student_id', ...) ORDER BY ...)`, más `sort.Slice` por `StudentID` en `parent_reader.go`.
- `parent_query.go` — `JSONB_AGG(DISTINCT jsonb_build_object('student_id', ...) ORDER BY ...)`, más `sort.Slice` por `StudentID` en `parent_reader.go`.
- `payment_plan_query.go` — `ORDER BY payment_plan_order, payment_plan_id` en el `SELECT` externo (orden funcional por número de orden, con el id como desempate determinista); los años de `payment_plans` se ordenan descendente en `buildPaymentPlanYears`.
- `payment_plan_query.go` — `ORDER BY payment_plan_order, payment_plan_id` en el `SELECT` externo (orden funcional por número de orden, con el id como desempate determinista); los años de `payment_plans` se ordenan descendente en `buildPaymentPlanYears`.
- `balotario_query.go` — períodos con `ORDER BY p.periodo_orden` y cursos con `ORDER BY curso_id` dentro de cada `JSONB_AGG`, más `sortBalotarioPeriodos` en `balotario_reader.go` (períodos por `periodo_orden`, cursos por `curso_id`). El orden alfabético de cursos lo resuelve el frontend.
Cualquier módulo nuevo con arrays anidados debe cumplir esta regla antes de entrar a `config.Pipelines`.
Cualquier módulo nuevo con arrays anidados debe cumplir esta regla antes de entrar a `config.Pipelines`.
...
@@ -104,7 +108,7 @@ Misma arquitectura por capas que `intranet-so-be`: `domain` (entidades + puertos
...
@@ -104,7 +108,7 @@ Misma arquitectura por capas que `intranet-so-be`: `domain` (entidades + puertos