Skip to content

feat: [TESIS-148] aggregate the real sales and dispatches for the reports screen - #101

Merged
LauAubert merged 3 commits into
masterfrom
TESIS-999007-reports-overview
Oct 5, 2026
Merged

LauAubert merged 3 commits into
masterfrom
TESIS-999007-reports-overview

Conversation

@LauAubert

@LauAubert LauAubert commented Oct 2, 2026 •

Copy link
Copy Markdown
Member

Ticket de Jira

https://proyectofinalfrlp.atlassian.net/browse/TESIS-148

ID provisorio: se reemplaza por la clave real al cargar la card en Jira. Card: cards/007.md.


Descripción

La pantalla de Reportes (S14) muestra un dataset de muestra fijo, con el distintivo «Datos de muestra», porque no existe un endpoint de agregados (TESIS-64). Los comentarios de esa card tienen razón en que varios bloques no tienen dominio detrás, pero una parte sí se puede calcular con el modelo de hoy. Para la demo, números reales de la empresa —aunque sean menos bloques— valen más que un dataset inventado. Este PR agrega GET /api/v1/reports/overview?period=7d|30d|90d.

Qué responde:

{
  "period": "7d", "from": "2026-09-26T00:00:00-03:00", "to": "2026-10-02T01:30:00-03:00",
  "granularity": "day",
  "kpis": {
    "orders":           { "value": 12,      "trend": 20.0 },
    "revenue":          { "value": 84500.0, "trend": null },
    "dispatched_units": { "value": 31,      "trend": -8.5 },
    "on_time_delivery_rate": null,
    "active_anomalies": null
  },
  "curve": [{ "date": "2026-09-26", "orders": 2, "revenue": 12000.0 }, "…"],
  "carriers": [{ "company_integration_id": 3, "name": "Andreani", "dispatched": 5, "delivered": 2 }]
}

Decisiones que conviene mirar:

La ventana va en hora de Argentina. Los últimos N días calendario, desde las 00:00 del primero hasta ahora, en America/Argentina/Buenos_Aires y no en la zona del servidor (UTC): con el corte en UTC, una venta de las 22 h caería en el día siguiente. El MVP opera sólo en Argentina (E4b, exclusión 9). El período anterior es la ventana del mismo largo inmediatamente antes, y contra él se calcula trend.

trend: null si el anterior es cero. Un «+100 %» o un «0 %» contra nada se leería como un dato. El front ya modela así la falta de comparación (TrendedValue.trend: number | null).

Unidades despachadas, por la fecha del despacho. shipments no guarda cuándo se despachó: lo dice el evento ready_to_ship que escribe ConfirmDispatch en la bitácora. Se suman las unidades de las órdenes de esos envíos, sin importar cuándo se creó la orden.

Lo que no se puede calcular va como null explícito. Cumplimiento de plazo (no hay fecha comprometida en shipments) y anomalías (no hay entidad). En vez del «% en plazo» por operador del diseño, carriers dice cuántos envíos despachó cada integración de courier y cuántos ya se entregaron: es lo que el modelo puede afirmar.

La curva incluye los buckets vacíos, un punto por día (7d/30d) o por semana empezando el lunes (90d). Sin los días en cero, la línea uniría dos ventas como si hubiera habido ventas en el medio.

Sin facturación de canceladas. Las órdenes cancelled no cuentan ni en orders ni en revenue.

  • Agrega Reports::Window (período, rangos, buckets) y Reports::BuildOverview (agregados) en el dominio nuevo app/poros/reports/.
  • Agrega ReportsController#overview con ReportPolicy#overview? (política sin registro: el aislamiento lo da CompanyScoped en cada modelo consultado).
  • Registra GET /api/v1/reports/overview. Un período desconocido o en forma de lista responde 400.
  • Documenta el dominio reports en docs/guidelines/feature-structure.md (sólo lee de orders y shipments).

Evidencia visual

N/A


Cómo probar

Precondición: bin/rails db:seed, login con un usuario de Norte.

  1. GET /api/v1/reports/overview → period: "7d", 7 puntos en curve, los KPIs de la semana.
  2. GET /api/v1/reports/overview?period=90d → granularity: "week", puntos en lunes.
  3. Crear una orden manual y volver a pedir → orders y revenue suben; la orden aparece en el punto de hoy.
  4. Despachar su envío → dispatched_units suma sus unidades y el courier aparece en carriers.
  5. ?period=1y → 400 {"error": "period must be one of 7d, 30d, 90d"}.
  6. Con un usuario de Sur, los números no incluyen nada de Norte.

Verificación: bundle exec rspec (1591 ejemplos, 0 fallas, cobertura 99.89% línea / 99.81% rama), rubocop y brakeman limpios.


Impacto y consideraciones

¿Introduce breaking changes?
No. Endpoint nuevo.

¿Requiere nuevas variables de entorno?
No

¿Afecta la arquitectura o genera un nuevo patrón?
Suma un dominio de sólo lectura, reports, documentado en feature-structure.md. No requiere ADR: no cambia ninguna convención (la respuesta es un recurso pelado, como fija ADR-015).

Escala: la curva agrupa en Ruby sobre pluck(:created_at, :total_amount) de la ventana, para cortar el día en hora local sin depender del AT TIME ZONE de Postgres. Con el volumen del MVP es instantáneo; si una empresa llegara a decenas de miles de órdenes por trimestre, conviene pasarlo a date_trunc en SQL.

Front: la card 008 (proyecto-web) reemplaza el dataset de muestra por este endpoint.

🤖 Generated with Claude Code

LauAubert and others added 2 commits October 2, 2026 01:14
…reports screen

The reports screen (S14) shows a fixed sample dataset because there was no
aggregates endpoint. Part of it can be computed with today's model, and real
numbers of the company are worth more in the demo than an invented dataset.

GET /api/v1/reports/overview?period=7d|30d|90d answers, for the last N
calendar days in Argentina time and against the previous window of the same
length: orders and revenue without cancelled ones, units dispatched (by the
dispatch event of the shipment log, since shipments keep no dispatch date),
a daily or weekly curve with its empty buckets, and the shipments of each
carrier with how many were delivered. On-time rate and anomalies come as an
explicit null: the model has nothing to compute them from.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ADR-015 asks every new endpoint to join the contract spec: the overview is a
bare resource with fixed keys, and so are its KPIs, curve points and carriers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@TomasMartin2004

Copy link
Copy Markdown
Contributor

Revisado. Lo veo bien para implementar.

Es el que más me interesa de los que agregan funcionalidad, y no por lo que suma sino por lo que saca: hoy la pantalla de Reportes (S14) muestra un dataset inventado con el cartel «Datos de muestra». RF-26 (Panel de Control Analítico) es alcance comprometido y está en la línea base. Defender un panel con números fabricados es el riesgo más caro de toda esta tanda de PRs.

Lo que más me gustó

on_time_delivery_rate y active_anomalies van en nil explícito en vez de un número. Es la decisión correcta y es la difícil: no hay fecha comprometida en shipments ni entidad de anomalía, y devolver 0 ahí diría «no hay anomalías» cuando lo cierto es «el modelo no lo registra». Mismo criterio que ya se había usado con updated_at como marca de sincronización. Da menos bloques que el diseño, pero todos los que da son verdad.

Lo que verifiqué

  • Todo pasa por modelos con CompanyScoped, así que el reporte es del tenant del request sin filtrar a mano. No hay un where(company_id:) suelto que se pueda olvidar.
  • Reports::Window corta el día en hora de Argentina y no en UTC. Está bien traído: con corte UTC, las ventas de 21 a 24 caen en el día siguiente y la curva queda corrida. Encima la justificación es la exclusión 9 de E4b, que es exactamente de dónde sale.
  • trended devuelve nil cuando el período anterior está en cero, en vez de un «+100 %». Otra vez el mismo criterio, y es el que importa para la demo: con la base recién sembrada, el período anterior va a estar vacío seguido.
  • dispatched_shipments sale del primer evento ready_to_ship de la bitácora porque shipments no guarda cuándo se despachó. Es correcto contra el modelo de hoy, y conviene que quede dicho en la card: si algún día se agrega dispatched_at, este cálculo cambia de lugar.
  • carriers resuelve con dos group().count y un includes(:service): no hay N+1 por operador.
  • Las ventas canceladas quedan fuera de facturación (where.not(status: 'cancelled')). Bien.

Un nit de volumen

curve hace pluck(:created_at, :total_amount) y agrupa en Ruby: con period=90d y una empresa con movimiento, eso trae todas las órdenes de la ventana a memoria en cada request. Para el MVP no molesta, pero es la primera cosa que se va a notar si alguien prueba con volumen. Si lo querés cerrar ahora, un group_by_day en SQL lo resuelve; si no, dejalo anotado en la card.

Antes de mergear

  1. El PR está CONFLICTING contra master: lo dejó así el merge de TESIS-138 (Shopify). Hay que rebasar.
  2. scan_ruby en rojo es la corrida vieja del 02/10; vuelve a correr sola con el rebase.
  3. Va antes de proyecto-web#64, que es el que conecta la pantalla.

@LauAubert LauAubert changed the title feat: [TESIS-999007] aggregate the real sales and dispatches for the reports screen feat: [TESIS-148] aggregate the real sales and dispatches for the reports screen Oct 3, 2026
@LauAubert LauAubert closed this Oct 3, 2026
@LauAubert
LauAubert deleted the TESIS-999007-reports-overview branch October 3, 2026 23:18
@LauAubert
LauAubert restored the TESIS-999007-reports-overview branch October 3, 2026 23:22
@LauAubert LauAubert reopened this Oct 3, 2026
@LauAubert
LauAubert marked this pull request as ready for review October 3, 2026 23:25
@LauAubert
LauAubert requested a review from a team as a code owner October 3, 2026 23:25
@LauAubert
LauAubert requested a review from Sanntinat October 3, 2026 23:25

@TomasMartin2004 TomasMartin2004 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revisión del diff completo. El código es idéntico al que leí cuando los PRs estaban en draft —ningún commit nuevo—, así que lo que sigue es el veredicto formal.

✅ Aprobado

Es el que más me interesa de los que agregan funcionalidad, y no por lo que suma sino por lo que saca: hoy S14 muestra un dataset inventado con el cartel «Datos de muestra», y RF-26 es alcance comprometido. Defender un panel con números fabricados es el riesgo más caro de toda la tanda.

Lo que más me gustó

on_time_delivery_rate y active_anomalies van en nil explícito en vez de un número. Es la decisión correcta y la difícil: no hay fecha comprometida en shipments ni entidad de anomalía, y devolver 0 ahí diría «no hay anomalías» cuando lo cierto es «el modelo no lo registra». Da menos bloques que el diseño y todos los que da son verdad.

Lo que verifiqué

  • Todo pasa por modelos con CompanyScoped: no hay un where(company_id:) suelto que se pueda olvidar.
  • Reports::Window corta el día en hora de Argentina y no en UTC, con la justificación sacada de la exclusión 9 de E4b. Es el error clásico y está contemplado.
  • trended devuelve nil cuando el período anterior está en cero, en vez de un «+100 %». Con la base recién sembrada ese caso es el habitual.
  • dispatched_shipments sale del primer evento ready_to_ship porque shipments no guarda cuándo se despachó. Correcto contra el modelo de hoy; si algún día se agrega dispatched_at, el cálculo cambia de lugar.
  • carriers resuelve con dos group().count y un includes(:service): sin N+1.

🟡 Un nit de volumen

curve hace pluck(:created_at, :total_amount) y agrupa en Ruby: con period=90d y una empresa con movimiento, trae todas las órdenes de la ventana a memoria en cada request. Para el MVP no molesta, pero es lo primero que se va a notar con volumen. Si lo querés cerrar ahora, un group_by_day en SQL lo resuelve.

⚠️ Antes de mergear

CONFLICTING por el merge de Shopify, y va antes de proyecto-web#64.

…s-overview

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@LauAubert
LauAubert merged commit cd403b4 into master Oct 5, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants