Reports & Analytics Backend Documentation
Backend architecture, data model, ETL pipeline, watermark mechanics, rollups, queues, and operational behavior for Reports & Analytics.
Reports & Analytics - Backend Documentation
1. Documentation Evidence
| Area | Files Inspected | Verified Details |
|---|---|---|
| Module wiring | apps/api/src/modules/analytics/*.module.ts | Aggregate + 4 admin leaves + shared + worker; app.module registration |
| Controllers | admin/{dashboard,reports,exports,sync}/*.controller.ts | 19 routes, permissions, rate limits |
| Services | shared/*.service.ts, shared/sync/*.ts, shared/reports/*.ts | ETL, watermark, rollups, exports, operator surface |
| DTOs | admin/dto/*.ts | Period/channel query, filters-as-JSON, export request |
| Schema | packages/mongodb/src/schemas/analytics/*.schema.ts | 18 collections, access patterns, retention |
| Jobs | packages/jobs/src/index.ts | QueueName.ANALYTICS, 8 AnalyticsJob kinds |
| Cache | n/a | This module writes no Redis cache and no PostgreSQL table |
| Config | ip-throttler.config.ts, env.validation.ts | Rate limits, ANALYTICS_* constants |
2. Backend Scope and Boundaries
Owns
- The MongoDB analytics store: 13 fact/state streams projected from PostgreSQL, the daily rollups, sync-state documents, pending work, and export records.
- The ETL control plane: watermarks, locks, run statistics, the pending-work queue.
- The read surface: dashboard, KPIs, trends, funnel, splits, rankings, the report registry.
- The export pipeline: request, build (CSV/XLSX), sweep, download.
- The operator sync surface: state inspection and refresh/backfill/reconcile triggers.
Does Not Own
- PostgreSQL — it reads the transactional store and writes nothing to it.
- The source domains' semantics: orders, payments, refunds, reviews, POS all remain owned by their modules; analytics projects their rows.
- Cache invalidation: no Redis domain is touched.
- Storage of user uploads — export files deliberately bypass
StorageManager(see §9).
Source of Truth
| Concern | Source of Truth | Notes |
|---|---|---|
| Business facts | PostgreSQL | Every analytics figure is rebuildable from it |
| Analytics figures | MongoDB facts + rollups | Trails PG by up to one sync interval |
| Sync progress | analytics_sync_state | Watermarks, locks, failures |
| Work a watermark cannot see | analytics_pending_work | Child-touched parents, old rollup days |
| Export state | analytics_report_export | Status, idempotency, TTL |
3. Module Composition
| Module | Type | Path | Controllers | Providers | Exports | Responsibility |
|---|---|---|---|---|---|---|
AnalyticsModule | Aggregate | analytics.module.ts | None | — | AnalyticsAdminAggregateModule | Composes admin leaves with shared |
AnalyticsAdminAggregateModule | Aggregate | admin/analytics-admin-aggregate.module.ts | None | — | 4 leaves | No controllers of its own |
AnalyticsDashboardAdminModule | Leaf | admin/dashboard/ | AnalyticsDashboardAdminController | AnalyticsDashboardService | — | 9 read routes |
AnalyticsReportsAdminModule | Leaf | admin/reports/ | AnalyticsReportsAdminController | AnalyticsReportService | — | Registry + run |
AnalyticsExportsAdminModule | Leaf | admin/exports/ | AnalyticsExportsAdminController | AnalyticsExportService | — | Export request/list/poll/download |
AnalyticsSyncAdminModule | Leaf | admin/sync/ | AnalyticsSyncAdminController | AnalyticsOperatorService | — | State + triggers |
AnalyticsSharedModule | Shared | analytics-shared.module.ts | None | 12 services | 12 services | Every decision |
AnalyticsWorkerModule | Worker | analytics-worker.module.ts | None | Processor + scheduler | — | One @Processor on QueueName.ANALYTICS |
4. File and Directory Map
apps/api/src/modules/analytics/
analytics.module.ts
analytics-shared.module.ts
analytics-worker.module.ts
admin/
analytics-admin-aggregate.module.ts
dashboard/
analytics-dashboard-admin.controller.ts
analytics-dashboard-admin.module.ts
analytics-dashboard.service.ts
reports/
analytics-reports-admin.controller.ts
analytics-reports-admin.module.ts
exports/
analytics-exports-admin.controller.ts
analytics-exports-admin.module.ts
sync/
analytics-sync-admin.controller.ts
analytics-sync-admin.module.ts
dto/
analytics-query.dto.ts
analytics-sync.dto.ts
report-export.dto.ts
report-run.dto.ts
shared/
analytics.constants.ts
analytics-freshness.service.ts
analytics-lifetime.service.ts
analytics-period.service.ts
analytics-permission.service.ts
analytics-query.service.ts
analytics-rollup-aggregate.service.ts
analytics-rollup.service.ts
reports/
analytics-export-builder.service.ts
analytics-export.service.ts
analytics-report.service.ts
report-registry.ts
report-registry.types.ts
sync/
analytics-catalog.projector.ts
analytics-commerce.projector.ts
analytics-order.projector.ts
analytics-operator.service.ts
analytics-pending-drain.service.ts
analytics-refund.projector.ts
analytics-return.projector.ts
analytics-sync.service.ts
analytics-sync-state.service.ts
analytics-writer.service.ts
workers/
analytics-maintenance.scheduler.ts
analytics.processor.ts| File | Purpose | Key Exports | Notes |
|---|---|---|---|
analytics.constants.ts | All policy numbers | overlap 900s, batch 1000, max pages 50, lock TTL 5 min, cron 2 min, rollup trail 7 days, reconcile 30 days, export cap 50k, TTL 48h, ANALYTICS_STREAMS (13, in dependency order) | Every constant documented with its why |
analytics-sync-state.service.ts | ETL control plane | claim, renewLock, buildScanWindow, completeRun, failRun, completeReconcile, completeBackfillSlice, enqueuePending, peekPending, resolvePending, markPendingAttempted, pendingBacklog | Single-document atomicity only (standalone Mongo) |
analytics-sync.service.ts | Watermarked scans | per-stream projection loop | Loops until a page comes back short |
analytics-order.projector.ts | orders + order_lines | parent-driven lines | Settled-refund net revenue lives here |
analytics-refund.projector.ts | refunds | queues order + rollup_day pending | The reason analytics_pending_work exists |
analytics-catalog.projector.ts | products, customers | snapshot fields via $setOnInsert | Must project before order_lines |
analytics-commerce.projector.ts | carts, checkouts, payments, pos_sales, promotion_usage, reviews, inventory_movements | — | — |
analytics-writer.service.ts | Mongo writes | idempotent upserts | — |
analytics-pending-drain.service.ts | re-projects parents | order + rollup_day kinds | Drained every 5 min |
analytics-rollup.service.ts | daily rollups | rebuildDay via replaceOne(upsert) | Recomputed, never incremented |
analytics-rollup-aggregate.service.ts | per-day aggregations | 12 aggregate reads | net = gross - settledRefunded |
analytics-lifetime.service.ts | customer/product lifetime figures | refresh pass | Wishlist count not restorable |
analytics-query.service.ts | read side | totalsForPeriod, dailySeries, byChannel, paymentMethodMix, topProducts, byDistrict, stockCounts, ratingDistribution, pendingModerationCount | Rollups where possible, facts for rankings |
analytics-period.service.ts | period validation | resolve, compare, describe | Asia/Kathmandu; 3-year cap |
analytics-freshness.service.ts | staleness | forStreams, assertSynced | Oldest contributing stream; 503 before first sync |
analytics-report.service.ts | registry runner | listAvailable, run, describe | One code path, 19 reports |
report-registry.ts | 19 descriptors as data | REPORT_REGISTRY, findReport | Pipelines built from typed context |
analytics-export.service.ts | export records | request, findForAdmin, listForAdmin, reEnqueueStuck, assertStillPermitted | No outbox — declared exemption |
analytics-export-builder.service.ts | file build | CSV/XLSX, 50k cap + in-file warning | Written to storage/analytics-exports/ |
analytics-operator.service.ts | operator surface | describeStreams, forceSync, backfill, reconcile | Read + trigger, never destroy |
analytics.processor.ts | ONE @Processor | 8 job kinds, exhaustive switch | Deterministic failures not retried |
analytics-maintenance.scheduler.ts | 5 crons | sync (2 min), rollup (5), drain (5), reconcile orders (hourly), sweep exports (hourly) | Enqueue failures swallowed |
5. Data Model
5.1 Schema Source
packages/mongodb/src/schemas/analytics/
analytics.shared.ts # ANALYTICS_CHANNELS, ANALYTICS_PAYMENT_METHODS
analytics-sync-state.schema.ts
analytics-pending-work.schema.ts
analytics-order-fact.schema.ts
analytics-order-line-fact.schema.ts
analytics-cart-fact.schema.ts
analytics-checkout-fact.schema.ts
analytics-payment-fact.schema.ts
analytics-pos-sale-fact.schema.ts
analytics-return-fact.schema.ts
analytics-refund-fact.schema.ts
analytics-review-fact.schema.ts
analytics-product-fact.schema.ts
analytics-customer-fact.schema.ts
analytics-promotion-fact.schema.ts
analytics-promotion-usage-fact.schema.ts
analytics-inventory-movement-fact.schema.ts
analytics-daily-rollup.schema.ts
analytics-report-export.schema.ts5.2 Collections
analytics_sync_state
| Field | Type | Notes |
|---|---|---|
_id | String | The stream name |
watermarkKind | enum | timestamp (all current streams) |
watermarkAt / watermarkSequence | Date / mixed | Scan floor; watermarkSequence is the keyset tiebreaker — a string for customers (uuid v7 is lexicographically monotonic) |
overlapSeconds | Number | 900 default |
batchSize | Number | 1000 |
status | enum | idle / running / failed / backfilling |
lockedUntil / lockedBy | Date / String | Lease; lockedBy = pid-uuid8 |
lastSuccessAt / lastRunAt / lastDurationMs | Date / Number | Diagnosis |
lastBatchTruncated | Boolean | behind |
consecutiveFailures | Number | 5 in a row = stopped pipeline |
lastError / lastErrorAt | String / Date | Truncated to 1000 chars |
lastReconciledAt / lastReconcileDriftCount | Date / Number | Reconcile diagnosis |
backfillCursor / backfillCompletedAt | String / Date | Resumable backfill |
totalDocumentsWritten | Number | Cumulative |
Retention: none, never TTL'd — deleting a document forces a full backfill.
analytics_pending_work
| Field | Type | Notes |
|---|---|---|
_id | String | "<kind>:<key>" — a burst touching one order leaves one entry |
kind | enum | order / customer / product / rollup_day |
key | String | The order/customer/product id, or "dateKey|channel" |
requestedBy | String | Which projector queued it |
requestedAt | Date | $setOnInsert — never pushed forward on re-enqueue |
attempts / lastError | Number / String | Failed-drain visibility |
Retention: none — an entry lives until its work commits; resolvePending runs AFTER the
work, never before.
analytics_order_fact
The central money document. Fields include orderNumber, invoiceNumber, channel (enum
online/pos), status, customerId, customerName, paymentMethod, dateKey,
yearMonth/yearQuarter/year, isCancelled, netRevenue, approvedRefundedAmount,
settledRefundedAmount, grossRevenue-family. Retention: none — the historical business
record every period comparison reads.
analytics_order_line_fact
Per-line projections: productPublicId, productName, sku, brandName, categoryName
(snapshot via $setOnInsert), netQuantity, netLineRevenue, orderStatus,
orderPublicId. Retention: none.
analytics_pos_sale_fact
Till dimensions only — saleNumber, status, fulfilment, customerId, customerName,
customerCreated, createdByAdminId, createdByAdminName, dateKey. Never a revenue
source — POS revenue lives on the order facts via channel: "pos".
analytics_refund_fact
orderPublicId, orderNumber, status, dateKey, line items with amount/quantity.
Retention: none — money movement is a permanent finance record.
analytics_daily_rollup
One document per (dateKey, channel) (_id = buildRollupId(dateKey, channel)), rebuilt via
replaceOne(upsert): orders { created, completed, cancelled, returned, refunded },
revenue { gross, net, subtotal, discount, shipping, shippingDiscount, paid, settledRefunded, approvedRefunded, cancelled, completed }, units, customers, payments { byMethod }, funnel { cartsCreated, checkoutsStarted, reachedPayment, paymentsSucceeded, ordersCompleted, checkoutsAbandoned }, returns, refunds,
promotions, reviews, averageOrderValue, computedAt, calendar keys.
analytics_report_export
_id (uuid), reportType, format (csv/xlsx), filters (Mixed), periodPreset,
periodFrom/periodTo, sortBy/sortDirection, timeZone, status (queued →
processing → ready/failed/expired), requestedByAdminId/requestedByAdminName,
requestedAt, startedAt, completedAt, rowCount, truncated, fileSizeBytes,
fileKey, error, expiresAt, idempotencyKey. Unique index on
(requestedByAdminId, idempotencyKey). TTL on expiresAt.
5.3 Relationship Diagram
6. Services and Responsibilities
6.1 AnalyticsSyncStateService
The ETL control plane. Every method is safe to call concurrently from more than one process — single-document atomicity is the only atomicity available.
| Method | Reads | Writes | Side Effects | Errors |
|---|---|---|---|---|
claim | sync-state | lock + status: running | — | Returns null if held |
renewLock | sync-state | lease extension | — | False if lease lost |
buildScanWindow | sync-state | — | — | — |
completeRun | sync-state | watermark (conditional), stats | — | — |
failRun | — | status, consecutiveFailures++ | — | — |
completeReconcile | — | drift count | Deliberately does NOT touch the watermark | — |
completeBackfillSlice | — | cursor, watermark on done | — | — |
enqueuePending | — | pending work (upsert) | — | — |
resolvePending | — | pending work (delete AFTER work) | — | — |
pendingBacklog | pending work | — | — | — |
6.2 AnalyticsSyncService
The watermarked scan loop. Reads rows since watermark - overlap, projects, and loops
until a page comes back short — one page per run would skip everything past the cap
permanently. A null watermark routes to backfill rather than scanning all of history.
6.3 Projectors (order, refund, return, catalog, commerce)
Each stream has its own projector. Order projector drives both orders and order_lines
(parent-driven, no independent watermark). Refund projector, on settlement, queues
{kind: "order"} and {kind: "rollup_day"} pending work — the mechanism that carries a
settlement into net revenue without touching the order row.
6.4 AnalyticsRollupService
Rebuilds (day, channel) rollup documents with replaceOne(upsert) — recomputed, never
incremented. Trailing 7 days plus queued rollup_day entries (deduplicated into one Map).
resolvePending runs only after every rebuild committed. No Math.max(0) clamp on net — a
negative result means the two populations drifted and must surface.
6.5 AnalyticsQueryService
The read side. totalsForPeriod sums pre-aggregated days (exact, because each day is a
complete recompute); averageOrderValue is recomputed from the summed pair, never averaged
from daily averages. Rankings (top products, districts, ratings, stock) read facts directly,
always bounded by a period and a limit. byDistrict groups by id, never by name —
provinces have genuinely been renamed.
6.6 AnalyticsReportService
One code path for all 19 reports: resolve descriptor → assert permission (before the
pipeline runs — a check after the aggregation is a DoS surface) → validate filters against
the report's own allowlist → build pipeline → count + page in parallel → wrap in the
standard envelope. Filter coercion is the injection boundary: field names come from the
descriptor, never the request; search terms are escaped and prefix-anchored
(^escaped on a lowercased field).
6.7 AnalyticsExportService
request writes the record FIRST (Mongo), then enqueues — the reverse order would let a job
run against a record that does not exist. isDuplicateKey on the idempotency index returns
the existing export. findForAdmin scopes to the requester; another admin's export is the
same 404 as a missing one. reEnqueueStuck (hourly) is the compensation for having no
outbox. assertStillPermitted re-checks at download time.
6.8 AnalyticsOperatorService
describeStreams maps 13 stream states + pending backlog into a healthy rollup.
forceSync / backfill / reconcile enqueue and return queued immediately. 409 ANALYTICS_SYNC_ALREADY_RUNNING is an honest answer, not a race guard (the lock is the guard).
assertKnownStream rejects typos with the valid list.
6.9 AnalyticsFreshnessService
forStreams takes the OLDEST contributing stream's lastSuccessAt; a stream with no control
document means never synced. assertSynced throws 503 before any aggregation over an empty
collection could return a plausible zero.
6.10 AnalyticsPeriodService
Resolves presets and custom windows, applies the 3-year cap, builds the calendar-aligned
comparison. referenceInstant is injectable so an export rebuilt from a stored request
resolves the same window it did when queued — a last_7_days export must not silently cover a
different week when the worker runs it.
7. Runtime Flows
7.1 The incremental sync
| Step | Code Path | Behavior | Failure Case |
|---|---|---|---|
| 1 | claim | Exclusive lease, 5 min TTL | Another runner holds it → null → skip |
| 2 | buildScanWindow | Floor = watermark − 15 min | Never the bare watermark (commit-lag) |
| 3 | scan loop | Loops until a page comes back short | Page budget (50) → truncated |
| 4 | completeRun | Watermark = min(processed, runStarted − overlap), never backward | failRun: no advance, failures++ |
7.2 The refund-settlement path (why pending work exists)
7.3 The export pipeline
8. Caching
None. This module writes no Redis cache domain and no PostgreSQL table. Its read model is MongoDB itself; the "cache" is the daily-rollup layer, and its invalidation is the 5-minute recompute.
9. BullMQ, Schedulers, and Async Work
| Queue | Job | Producer | Processor | Payload | Retry/Backoff | Idempotency |
|---|---|---|---|---|---|---|
QueueName.ANALYTICS | analytics.sync_stream | cron / operator | AnalyticsProcessor | { stream, correlationId } | BullMQ policy; deterministic failures returned not retried | stream lock |
QueueName.ANALYTICS | analytics.sync_all | cron 2 min | same | { correlationId } | same | jobId per minute |
QueueName.ANALYTICS | analytics.rollup | cron 5 min | same | { windowDays } | same | jobId per minute |
QueueName.ANALYTICS | analytics.drain_pending | cron 5 min | same | { batchSize } | same | jobId per minute |
QueueName.ANALYTICS | analytics.reconcile | cron hourly + operator | same | { stream, windowDays } | same | jobId per hour/trigger |
QueueName.ANALYTICS | analytics.backfill | operator | same | { stream } | same | jobId per trigger |
QueueName.ANALYTICS | analytics.build_export | export service + sweep | same | { exportPublicId } | same | jobId = export id |
QueueName.ANALYTICS | analytics.sweep_exports | cron hourly | same | { correlationId } | same | jobId per hour |
AnalyticsProcessor is the one @Processor on the queue (the codebase hard rule), with an
exhaustive switch over AnalyticsJob — a second decorated class would race and silently
drop jobs. Deterministic HttpException failures are returned (not retried); transient
failures rethrow.
Outbox exemption, declared: exports have no outbox because the module writes no
PostgreSQL table — the export record is Mongo, written first, and reEnqueueStuck (in the
hourly sweep) compensates for a lost enqueue. The crons also enqueue directly (the documented
cron-with-no-write exemption).
Export files are written to storage/analytics-exports/, not through
StorageManager.handleUpload — that path sniffs magic bytes and has no CSV entry; it protects
user uploads and must not be loosened for a server-generated artefact. The trade (files served
by the API rather than object storage) is documented in analytics.constants.ts.
10. Realtime and Events
No realtime surface exists. The module's "events" are the Mongo collections and queue jobs described above.
11. Security, Auth, and Abuse Controls
- Guards:
JwtAuthGuard+RoleGuardon all four controllers;IpThrottlerGuardper route. - Permissions:
Analytics_READgates every read;Analytics_UPDATEgates the three sync triggers — a backfill walks every row a stream has ever produced and must not be reachable by a read-only account. Ten modules:Analytics+ nineReports*;_CREATE,_DELETE,_RESTOREunused (catalogue cross product). - Report permissions: each report descriptor names its own
Reports*_READ;GET /reportsfilters to the caller's grants; running a report checks BEFORE the aggregation runs. - No id enumeration: an unpermitted report is the SAME 404 as a nonexistent one; another admin's export is the SAME 404 as a missing one.
- Download re-check: the export download route resolves the caller's LIVE role and re-checks the report's permission — queueing before a role change must not preserve access.
- Injection boundary: report filters are validated against the descriptor allowlist; field names come from the descriptor, never the request; search terms escaped and prefix-anchored.
- Rate limits (per admin account, not IP):
ADMIN_ANALYTICS_READ60/min; export submissionADMIN_ANALYTICS_EXPORT5/hour; sync triggersADMIN_HEAVY_OP20/min. - DoS bounds: 3-year period cap; report limit 500; export cap 50k rows; search prefix-anchored so it stays index-served.
- PII: customer facts carry names/emails for reporting; lookup-style surfaces are admin-gated and the export records are scoped to their requester.
13. Error Handling
| Error Code | HTTP Status | Thrown By | Condition | Client Action |
|---|---|---|---|---|
ANALYTICS_NOT_YET_SYNCED | 503 | freshness service | first sync not complete | Retry shortly; never render zeros |
ANALYTICS_REPORT_NOT_FOUND | 404 | report/export service | no such report, OR not permitted | Re-read GET /reports |
ANALYTICS_INVALID_PERIOD | 400 | period service | from after to, or custom without both | Fix dates |
ANALYTICS_PERIOD_TOO_LONG | 400 | period service | over 3 years | Narrow, or export |
ANALYTICS_FILTER_NOT_SUPPORTED | 400 | report service | filter key not on the report, or bad JSON | Message names the key |
ANALYTICS_SORT_NOT_SUPPORTED | 400 | report service | sortBy not in sortable | Use a listed key |
ANALYTICS_EXPORT_NOT_FOUND | 404 | export service | no such export, or another admin's | — |
ANALYTICS_EXPORT_NOT_READY | 400 | export service/controller | still queued/building | Keep polling |
ANALYTICS_EXPORT_EXPIRED | 410 | controller | file swept after 48h | Request again |
ANALYTICS_EXPORT_FAILED | 400 | controller | build failed | error carries the reason |
ANALYTICS_SYNC_ALREADY_RUNNING | 409 | operator service | a run is in progress | Show the message — it is an answer |
ANALYTICS_STREAM_NOT_FOUND | 404 | operator service | unknown stream name | Message lists valid streams |
14. Observability
| Signal | Location | Purpose |
|---|---|---|
| Log | Logger in scheduler, processor, sync-state, rollup, export services | [analytics] ... — every failure logs; cron failures are swallowed so analytics never takes a process down |
| State documents | analytics_sync_state | Watermarks, lastError, consecutiveFailures, lastBatchTruncated, drift counts |
| Pending backlog | analytics_pending_work | count + oldestAt — the monitoring signal for the queue |
| Export records | analytics_report_export | Status, error, truncated, expiry |
| Queue visibility | BullMQ | Job ids carry the tick/trigger identity; deterministic failures visible in return values |
15. Testing and Validation
| Test Type | Files | Coverage |
|---|---|---|
| Integration | analytics-sync.int.spec.ts | Sync, watermark, pending work against real stores |
| Constraint/consistency | projectors + rollup specs | Net revenue, settled-only deduction, idempotent upserts |
| Live HTTP | .omc/plans/Reports-analytics/checks-*.sh | 19 routes against a booted API |
Validation: pnpm check-types · pnpm check (Biome) · pnpm build · pnpm test (serial) ·
pnpm rules — all passed in the feature's verification run.
16. Mandatory Backend Deep-Dive Pack
16.1 Submodule Coverage Matrix
| Unit | Type | Owns | Depends On | Called By | Calls | State Touched | Failure Modes |
|---|---|---|---|---|---|---|---|
AnalyticsSyncStateService | service | control plane | Mongo models | sync, drain, operator | 5 methods | sync-state, pending-work | lease expiry, no tx |
AnalyticsSyncService | service | scan loop | PG, writer, state | processor | state, writer | facts | page-budget truncation |
| 5 projectors | services | stream projections | PG, writer, state | sync/backfill/reconcile | writer, enqueuePending | facts, pending | missing parent rows |
AnalyticsWriterService | service | Mongo writes | Mongo models | projectors | — | facts | — |
AnalyticsRollupService | service | rollups | aggregates, lifetime, state | processor | 3 services | daily rollups, pending | half-projected day |
AnalyticsRollupAggregateService | service | per-day sums | Mongo models | rollup | — | — | — |
AnalyticsLifetimeService | service | lifetime figures | Mongo models | rollup | — | customer/product facts | wishlist not restorable |
AnalyticsPendingDrainService | service | parent re-projection | state, order fact | processor | state, order fact | facts, pending | — |
AnalyticsQueryService | service | read side | Mongo models | dashboard | — | — | — |
AnalyticsPeriodService | service | windows | period util | dashboard, reports, exports | — | — | 3-year cap |
AnalyticsFreshnessService | service | staleness | sync-state | all reads | — | — | 503 before first sync |
AnalyticsReportService | service | registry runner | registry, freshness, period | reports controller, exports | aggregate per model | — | filter/sort/permission errors |
AnalyticsExportService | service | export records | Mongo, queue, reports | exports controller, sweep | queue | export records | stuck queued |
AnalyticsExportBuilderService | service | file build | report service, fs | processor | report run, writeFile | files, export record | cap, disk |
AnalyticsOperatorService | service | operator surface | state, queue | sync controller | enqueue | jobs | 409 already running |
AnalyticsPermissionService | service | live grant resolution | role module | controllers | — | — | — |
AnalyticsProcessor | worker | ONE processor | handlers | BullMQ | switch | jobs | deterministic vs retryable |
AnalyticsMaintenanceScheduler | scheduler | 5 crons | queue | cron | enqueue | jobs | swallowed failures |
16.2 UML and Architecture Diagram Pack
- Component diagram: §3 composition graph.
- Deployment/runtime diagram: §9 queue topology.
- Sequence diagrams: §7.1 sync, §7.2 refund settlement, §7.3 export.
- ER diagram: §5.3.
- State diagrams: feature §6.2 (export), feature §7.2 (stream).
16.3 Code Flow Narrative
The three critical paths — incremental sync (§7.1), refund settlement (§7.2) and export
(§7.3) — have full narratives. The shared shape of every other flow: resolve → validate →
read → wrap with meta (freshness + period + duration). The watermark advance (completeRun)
is the module's most delicate logic and its 4-mechanism design (overlap, conditional advance,
reconcile, backfill) is documented at ANALYTICS_WATERMARK_OVERLAP_SECONDS and in the
sync-state service comments.
16.4 Data Layer Deep Dive
Field-level detail for the control-plane and money collections is in §5.2. Collection rationale:
| Collection | Access Pattern Served | Retention |
|---|---|---|
analytics_sync_state | one doc per stream, read every run | never TTL'd |
analytics_pending_work | oldest-first drain | until work commits |
| order/order-line facts | period group + period comparisons | never (permanent commercial record) |
| pos sale facts | sales-by-admin over dateKey | never |
| refund facts | sums by status over range | never (finance record) |
| daily rollups | dashboard reads by dateKey+channel | never (recomputed in place) |
| report exports | by id, by requester | TTL on expiresAt |
16.5 Business Logic and Invariant Catalog
| Invariant | Enforced By | Why It Exists | Failure Error | Tests |
|---|---|---|---|---|
| Net revenue deducts settled refunds only | rollup aggregate | Approved is a liability, not a deduction | — | rollup spec |
| Watermark never moves backward | Math.max in completeRun/backfill | A no-op run must not rewind | — | sync int spec |
| Truncated run advances only to processed | conditional advance | Unprocessed rows must stay above the floor | — | sync int spec |
Rollup replaced, never $inc-ed | replaceOne(upsert) | At-least-once dispatch + no Mongo transactions | — | rollup spec |
| Pending resolves only after work | resolvePending ordering | Crash re-does work (free); lost work is not | — | sync int spec |
| Report permission checked before aggregation | assertPermitted first | No paid-for refusals | 404 | report spec |
| Filters validated by name | allowlist in validateFilters | No confident wrong answers | 400 | report spec |
| Export capped loudly | builder: flag + in-file warning | Silent truncation is worse | truncated | builder spec |
| Download re-checks live permission | assertStillPermitted | No snapshot access around revocation | 404 | export spec |
Sync triggers need Analytics_UPDATE | route permissions | Backfill walks all history | 403 | controller |
| No destructive analytics action | no such route exists | Repair = re-project | — | — |
16.6 Tradeoffs, Alternatives, and ADR Notes
| Decision | Context | Chosen Option | Alternatives | Why Chosen | Tradeoffs | Revisit Trigger |
|---|---|---|---|---|---|---|
| Mongo projection | Cheap reads off the live store | ETL + facts + rollups | Direct PG queries | Analytics never competes with orders | ≤2 min staleness | visible via dataAsOf |
| Rollups recomputed | Standalone Mongo | full recompute, replaceOne | $inc counters | No exactly-once needed | 5-min tick cost | replica set + transactions |
| 4-mechanism watermark | updated_at predates commit | overlap + conditional + reconcile + backfill | bare watermark | No silent row loss | complexity | trigger-less ORM |
| Pending-work queue | child touches invisible to watermark | Mongo queue | watermark only | Settlements reach net revenue | another queue | — |
| Report registry | ~60 spec'd reports | 19 descriptors as data | controller per report | no 60-copy drift | Swagger can't enumerate | columns via GET /reports |
| Filters as one JSON param | forbidNonWhitelisted | JSON object | loose keys | works with the pipe; per-report validation | uglier URLs | — |
| No outbox for exports | module writes no PG | record-first + reEnqueueStuck | outbox | nothing to join atomically | stuck-queued window | declared exemption |
| Files on API disk | admin-scale exports | storage/analytics-exports/ | StorageManager | no magic-byte loosening | API-host disk | large/frequent exports |
| Prefix-only search | index-served cost | ^escaped regex | substring | bounded queries | "starts with" semantics | text index |
| Calendar-aligned comparison | honest week-to-date | truncatedForFairness | full previous period | no fake collapse | — | — |
| Day boundaries Kathmandu | business day is local | pinned at projection | UTC | matches invoices | client must not UTC-bucket | — |
16.7 Operational Runbook
| Operation | How to Inspect | Healthy State | Failure Signal | Recovery |
|---|---|---|---|---|
| Sync | GET /sync | idle, failures 0, behind false, drift 0 | failures ≥ 5 / behind true / drift > 0 | refresh, backfill, reconcile |
| Watermark | sync-state docs | advancing each run | stuck while behind | backfill the stream |
| Pending queue | GET /sync → pendingWork | count 0 | count growing, old oldestAt | drain runs every 5 min; check processor |
| Rollups | daily-rollup docs | recomputed every 5 min | stale computedAt | trigger refresh |
| Exports | export records + disk | statuses reach ready | stuck queued | sweep re-enqueues hourly |
| Export files | storage/analytics-exports/ | swept after 48h | disk growth | sweep job |
| Queue | Bull Board / logs | jobs completing | [failure] rethrows | transient → automatic retry; deterministic → code fix |
16.8 Backend Risk Register
| Risk | Area | Impact | Current Mitigation | Remaining Gap |
|---|---|---|---|---|
| Watermark skips committed rows | sync | silent revenue loss | overlap + conditional advance | — |
| Two runners on one stream | sync | livelock | single-document lock with lease + renew | multi-process on one replica set not load-tested |
| Export enqueue lost | exports | stuck queued | record-first + hourly re-enqueue | up to 15 min delay |
| Filter injection | reports | scope tampering | allowlist, descriptor field names, escaped search | — |
| Expensive report | reports | DB load | caps (period, limit, rows) + rate limits | — |
| Refund settled months later | rollups | January overstated | pending rollup_day drain | — |
| Revoked admin downloads | exports | snapshot access | live permission re-check | — |
17. Zero-Omission Backend Checklist
- Every file in the module directory is represented or explicitly marked non-runtime (§4, §16.1).
- Every controller, service, provider, processor, scheduler, helper, mapper, DTO, enum, and schema is documented.
- Every method with business behavior has a code-flow narrative (§6, §7, §16.3).
- Every collection and job payload has field-level detail (§5.2, §9).
- Every index, constraint, relation, and retention behavior has rationale (§5.2, §16.4).
- Every lifecycle/status transition has a state diagram and transition table (feature §7).
- Every read/write/action/job flow has sequence and activity diagrams (§7, §9).
- Every business invariant is cataloged (§16.5).
- Every cache key, invalidation path, queue job, realtime event, and external call is documented (§8, §9, §10) — no cache or realtime exists by design.
- Every architectural tradeoff is documented with alternatives and revisit triggers (§16.6).
- Every operational failure mode has a runbook entry (§16.7).
18. Backend Completion Checklist
- Module boundaries are documented (§2).
- Every controller, service, DTO, schema file, job, cache key, and event is covered (§3–§11).
- Every database collection has a field table and relationship diagram (§5).
- Every runtime flow has a diagram and branch notes (§7).
- API and features/flows docs are linked (See Also).
- No claim is made without a source file or documented source reference.
See Also
- API doc:
/docs/developer/analytics/api - Features and flows doc:
/docs/developer/analytics/feature - TDD: not yet published
Reports & Analytics API Reference
Complete API contracts for Reports & Analytics, including routes, auth, DTOs, responses, errors, examples, and integration notes.
Reports & Analytics Features and Flows
Complete feature list, actor journeys, state flows, business rules, edge cases, and diagrams for Reports & Analytics.