Happy House - Ecommerce Docs

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

AreaFiles InspectedVerified Details
Module wiringapps/api/src/modules/analytics/*.module.tsAggregate + 4 admin leaves + shared + worker; app.module registration
Controllersadmin/{dashboard,reports,exports,sync}/*.controller.ts19 routes, permissions, rate limits
Servicesshared/*.service.ts, shared/sync/*.ts, shared/reports/*.tsETL, watermark, rollups, exports, operator surface
DTOsadmin/dto/*.tsPeriod/channel query, filters-as-JSON, export request
Schemapackages/mongodb/src/schemas/analytics/*.schema.ts18 collections, access patterns, retention
Jobspackages/jobs/src/index.tsQueueName.ANALYTICS, 8 AnalyticsJob kinds
Cachen/aThis module writes no Redis cache and no PostgreSQL table
Configip-throttler.config.ts, env.validation.tsRate 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

ConcernSource of TruthNotes
Business factsPostgreSQLEvery analytics figure is rebuildable from it
Analytics figuresMongoDB facts + rollupsTrails PG by up to one sync interval
Sync progressanalytics_sync_stateWatermarks, locks, failures
Work a watermark cannot seeanalytics_pending_workChild-touched parents, old rollup days
Export stateanalytics_report_exportStatus, idempotency, TTL

3. Module Composition

ModuleTypePathControllersProvidersExportsResponsibility
AnalyticsModuleAggregateanalytics.module.tsNoneAnalyticsAdminAggregateModuleComposes admin leaves with shared
AnalyticsAdminAggregateModuleAggregateadmin/analytics-admin-aggregate.module.tsNone4 leavesNo controllers of its own
AnalyticsDashboardAdminModuleLeafadmin/dashboard/AnalyticsDashboardAdminControllerAnalyticsDashboardService9 read routes
AnalyticsReportsAdminModuleLeafadmin/reports/AnalyticsReportsAdminControllerAnalyticsReportServiceRegistry + run
AnalyticsExportsAdminModuleLeafadmin/exports/AnalyticsExportsAdminControllerAnalyticsExportServiceExport request/list/poll/download
AnalyticsSyncAdminModuleLeafadmin/sync/AnalyticsSyncAdminControllerAnalyticsOperatorServiceState + triggers
AnalyticsSharedModuleSharedanalytics-shared.module.tsNone12 services12 servicesEvery decision
AnalyticsWorkerModuleWorkeranalytics-worker.module.tsNoneProcessor + schedulerOne @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
FilePurposeKey ExportsNotes
analytics.constants.tsAll policy numbersoverlap 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.tsETL control planeclaim, renewLock, buildScanWindow, completeRun, failRun, completeReconcile, completeBackfillSlice, enqueuePending, peekPending, resolvePending, markPendingAttempted, pendingBacklogSingle-document atomicity only (standalone Mongo)
analytics-sync.service.tsWatermarked scansper-stream projection loopLoops until a page comes back short
analytics-order.projector.tsorders + order_linesparent-driven linesSettled-refund net revenue lives here
analytics-refund.projector.tsrefundsqueues order + rollup_day pendingThe reason analytics_pending_work exists
analytics-catalog.projector.tsproducts, customerssnapshot fields via $setOnInsertMust project before order_lines
analytics-commerce.projector.tscarts, checkouts, payments, pos_sales, promotion_usage, reviews, inventory_movements
analytics-writer.service.tsMongo writesidempotent upserts
analytics-pending-drain.service.tsre-projects parentsorder + rollup_day kindsDrained every 5 min
analytics-rollup.service.tsdaily rollupsrebuildDay via replaceOne(upsert)Recomputed, never incremented
analytics-rollup-aggregate.service.tsper-day aggregations12 aggregate readsnet = gross - settledRefunded
analytics-lifetime.service.tscustomer/product lifetime figuresrefresh passWishlist count not restorable
analytics-query.service.tsread sidetotalsForPeriod, dailySeries, byChannel, paymentMethodMix, topProducts, byDistrict, stockCounts, ratingDistribution, pendingModerationCountRollups where possible, facts for rankings
analytics-period.service.tsperiod validationresolve, compare, describeAsia/Kathmandu; 3-year cap
analytics-freshness.service.tsstalenessforStreams, assertSyncedOldest contributing stream; 503 before first sync
analytics-report.service.tsregistry runnerlistAvailable, run, describeOne code path, 19 reports
report-registry.ts19 descriptors as dataREPORT_REGISTRY, findReportPipelines built from typed context
analytics-export.service.tsexport recordsrequest, findForAdmin, listForAdmin, reEnqueueStuck, assertStillPermittedNo outbox — declared exemption
analytics-export-builder.service.tsfile buildCSV/XLSX, 50k cap + in-file warningWritten to storage/analytics-exports/
analytics-operator.service.tsoperator surfacedescribeStreams, forceSync, backfill, reconcileRead + trigger, never destroy
analytics.processor.tsONE @Processor8 job kinds, exhaustive switchDeterministic failures not retried
analytics-maintenance.scheduler.ts5 cronssync (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.ts

5.2 Collections

analytics_sync_state

FieldTypeNotes
_idStringThe stream name
watermarkKindenumtimestamp (all current streams)
watermarkAt / watermarkSequenceDate / mixedScan floor; watermarkSequence is the keyset tiebreaker — a string for customers (uuid v7 is lexicographically monotonic)
overlapSecondsNumber900 default
batchSizeNumber1000
statusenumidle / running / failed / backfilling
lockedUntil / lockedByDate / StringLease; lockedBy = pid-uuid8
lastSuccessAt / lastRunAt / lastDurationMsDate / NumberDiagnosis
lastBatchTruncatedBooleanbehind
consecutiveFailuresNumber5 in a row = stopped pipeline
lastError / lastErrorAtString / DateTruncated to 1000 chars
lastReconciledAt / lastReconcileDriftCountDate / NumberReconcile diagnosis
backfillCursor / backfillCompletedAtString / DateResumable backfill
totalDocumentsWrittenNumberCumulative

Retention: none, never TTL'd — deleting a document forces a full backfill.

analytics_pending_work

FieldTypeNotes
_idString"<kind>:<key>" — a burst touching one order leaves one entry
kindenumorder / customer / product / rollup_day
keyStringThe order/customer/product id, or "dateKey|channel"
requestedByStringWhich projector queued it
requestedAtDate$setOnInsert — never pushed forward on re-enqueue
attempts / lastErrorNumber / StringFailed-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 (queuedprocessingready/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.

MethodReadsWritesSide EffectsErrors
claimsync-statelock + status: runningReturns null if held
renewLocksync-statelease extensionFalse if lease lost
buildScanWindowsync-state
completeRunsync-statewatermark (conditional), stats
failRunstatus, consecutiveFailures++
completeReconciledrift countDeliberately does NOT touch the watermark
completeBackfillSlicecursor, watermark on done
enqueuePendingpending work (upsert)
resolvePendingpending work (delete AFTER work)
pendingBacklogpending 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

StepCode PathBehaviorFailure Case
1claimExclusive lease, 5 min TTLAnother runner holds it → null → skip
2buildScanWindowFloor = watermark − 15 minNever the bare watermark (commit-lag)
3scan loopLoops until a page comes back shortPage budget (50) → truncated
4completeRunWatermark = min(processed, runStarted − overlap), never backwardfailRun: 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

QueueJobProducerProcessorPayloadRetry/BackoffIdempotency
QueueName.ANALYTICSanalytics.sync_streamcron / operatorAnalyticsProcessor{ stream, correlationId }BullMQ policy; deterministic failures returned not retriedstream lock
QueueName.ANALYTICSanalytics.sync_allcron 2 minsame{ correlationId }samejobId per minute
QueueName.ANALYTICSanalytics.rollupcron 5 minsame{ windowDays }samejobId per minute
QueueName.ANALYTICSanalytics.drain_pendingcron 5 minsame{ batchSize }samejobId per minute
QueueName.ANALYTICSanalytics.reconcilecron hourly + operatorsame{ stream, windowDays }samejobId per hour/trigger
QueueName.ANALYTICSanalytics.backfilloperatorsame{ stream }samejobId per trigger
QueueName.ANALYTICSanalytics.build_exportexport service + sweepsame{ exportPublicId }samejobId = export id
QueueName.ANALYTICSanalytics.sweep_exportscron hourlysame{ correlationId }samejobId 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 + RoleGuard on all four controllers; IpThrottlerGuard per route.
  • Permissions: Analytics_READ gates every read; Analytics_UPDATE gates 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 + nine Reports*; _CREATE, _DELETE, _RESTORE unused (catalogue cross product).
  • Report permissions: each report descriptor names its own Reports*_READ; GET /reports filters 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_READ 60/min; export submission ADMIN_ANALYTICS_EXPORT 5/hour; sync triggers ADMIN_HEAVY_OP 20/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 CodeHTTP StatusThrown ByConditionClient Action
ANALYTICS_NOT_YET_SYNCED503freshness servicefirst sync not completeRetry shortly; never render zeros
ANALYTICS_REPORT_NOT_FOUND404report/export serviceno such report, OR not permittedRe-read GET /reports
ANALYTICS_INVALID_PERIOD400period servicefrom after to, or custom without bothFix dates
ANALYTICS_PERIOD_TOO_LONG400period serviceover 3 yearsNarrow, or export
ANALYTICS_FILTER_NOT_SUPPORTED400report servicefilter key not on the report, or bad JSONMessage names the key
ANALYTICS_SORT_NOT_SUPPORTED400report servicesortBy not in sortableUse a listed key
ANALYTICS_EXPORT_NOT_FOUND404export serviceno such export, or another admin's
ANALYTICS_EXPORT_NOT_READY400export service/controllerstill queued/buildingKeep polling
ANALYTICS_EXPORT_EXPIRED410controllerfile swept after 48hRequest again
ANALYTICS_EXPORT_FAILED400controllerbuild failederror carries the reason
ANALYTICS_SYNC_ALREADY_RUNNING409operator servicea run is in progressShow the message — it is an answer
ANALYTICS_STREAM_NOT_FOUND404operator serviceunknown stream nameMessage lists valid streams

14. Observability

SignalLocationPurpose
LogLogger in scheduler, processor, sync-state, rollup, export services[analytics] ... — every failure logs; cron failures are swallowed so analytics never takes a process down
State documentsanalytics_sync_stateWatermarks, lastError, consecutiveFailures, lastBatchTruncated, drift counts
Pending backloganalytics_pending_workcount + oldestAt — the monitoring signal for the queue
Export recordsanalytics_report_exportStatus, error, truncated, expiry
Queue visibilityBullMQJob ids carry the tick/trigger identity; deterministic failures visible in return values

15. Testing and Validation

Test TypeFilesCoverage
Integrationanalytics-sync.int.spec.tsSync, watermark, pending work against real stores
Constraint/consistencyprojectors + rollup specsNet revenue, settled-only deduction, idempotent upserts
Live HTTP.omc/plans/Reports-analytics/checks-*.sh19 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

UnitTypeOwnsDepends OnCalled ByCallsState TouchedFailure Modes
AnalyticsSyncStateServiceservicecontrol planeMongo modelssync, drain, operator5 methodssync-state, pending-worklease expiry, no tx
AnalyticsSyncServiceservicescan loopPG, writer, stateprocessorstate, writerfactspage-budget truncation
5 projectorsservicesstream projectionsPG, writer, statesync/backfill/reconcilewriter, enqueuePendingfacts, pendingmissing parent rows
AnalyticsWriterServiceserviceMongo writesMongo modelsprojectorsfacts
AnalyticsRollupServiceservicerollupsaggregates, lifetime, stateprocessor3 servicesdaily rollups, pendinghalf-projected day
AnalyticsRollupAggregateServiceserviceper-day sumsMongo modelsrollup
AnalyticsLifetimeServiceservicelifetime figuresMongo modelsrollupcustomer/product factswishlist not restorable
AnalyticsPendingDrainServiceserviceparent re-projectionstate, order factprocessorstate, order factfacts, pending
AnalyticsQueryServiceserviceread sideMongo modelsdashboard
AnalyticsPeriodServiceservicewindowsperiod utildashboard, reports, exports3-year cap
AnalyticsFreshnessServiceservicestalenesssync-stateall reads503 before first sync
AnalyticsReportServiceserviceregistry runnerregistry, freshness, periodreports controller, exportsaggregate per modelfilter/sort/permission errors
AnalyticsExportServiceserviceexport recordsMongo, queue, reportsexports controller, sweepqueueexport recordsstuck queued
AnalyticsExportBuilderServiceservicefile buildreport service, fsprocessorreport run, writeFilefiles, export recordcap, disk
AnalyticsOperatorServiceserviceoperator surfacestate, queuesync controllerenqueuejobs409 already running
AnalyticsPermissionServiceservicelive grant resolutionrole modulecontrollers
AnalyticsProcessorworkerONE processorhandlersBullMQswitchjobsdeterministic vs retryable
AnalyticsMaintenanceSchedulerscheduler5 cronsqueuecronenqueuejobsswallowed 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:

CollectionAccess Pattern ServedRetention
analytics_sync_stateone doc per stream, read every runnever TTL'd
analytics_pending_workoldest-first drainuntil work commits
order/order-line factsperiod group + period comparisonsnever (permanent commercial record)
pos sale factssales-by-admin over dateKeynever
refund factssums by status over rangenever (finance record)
daily rollupsdashboard reads by dateKey+channelnever (recomputed in place)
report exportsby id, by requesterTTL on expiresAt

16.5 Business Logic and Invariant Catalog

InvariantEnforced ByWhy It ExistsFailure ErrorTests
Net revenue deducts settled refunds onlyrollup aggregateApproved is a liability, not a deductionrollup spec
Watermark never moves backwardMath.max in completeRun/backfillA no-op run must not rewindsync int spec
Truncated run advances only to processedconditional advanceUnprocessed rows must stay above the floorsync int spec
Rollup replaced, never $inc-edreplaceOne(upsert)At-least-once dispatch + no Mongo transactionsrollup spec
Pending resolves only after workresolvePending orderingCrash re-does work (free); lost work is notsync int spec
Report permission checked before aggregationassertPermitted firstNo paid-for refusals404report spec
Filters validated by nameallowlist in validateFiltersNo confident wrong answers400report spec
Export capped loudlybuilder: flag + in-file warningSilent truncation is worsetruncatedbuilder spec
Download re-checks live permissionassertStillPermittedNo snapshot access around revocation404export spec
Sync triggers need Analytics_UPDATEroute permissionsBackfill walks all history403controller
No destructive analytics actionno such route existsRepair = re-project

16.6 Tradeoffs, Alternatives, and ADR Notes

DecisionContextChosen OptionAlternativesWhy ChosenTradeoffsRevisit Trigger
Mongo projectionCheap reads off the live storeETL + facts + rollupsDirect PG queriesAnalytics never competes with orders≤2 min stalenessvisible via dataAsOf
Rollups recomputedStandalone Mongofull recompute, replaceOne$inc countersNo exactly-once needed5-min tick costreplica set + transactions
4-mechanism watermarkupdated_at predates commitoverlap + conditional + reconcile + backfillbare watermarkNo silent row losscomplexitytrigger-less ORM
Pending-work queuechild touches invisible to watermarkMongo queuewatermark onlySettlements reach net revenueanother queue
Report registry~60 spec'd reports19 descriptors as datacontroller per reportno 60-copy driftSwagger can't enumeratecolumns via GET /reports
Filters as one JSON paramforbidNonWhitelistedJSON objectloose keysworks with the pipe; per-report validationuglier URLs
No outbox for exportsmodule writes no PGrecord-first + reEnqueueStuckoutboxnothing to join atomicallystuck-queued windowdeclared exemption
Files on API diskadmin-scale exportsstorage/analytics-exports/StorageManagerno magic-byte looseningAPI-host disklarge/frequent exports
Prefix-only searchindex-served cost^escaped regexsubstringbounded queries"starts with" semanticstext index
Calendar-aligned comparisonhonest week-to-datetruncatedForFairnessfull previous periodno fake collapse
Day boundaries Kathmandubusiness day is localpinned at projectionUTCmatches invoicesclient must not UTC-bucket

16.7 Operational Runbook

OperationHow to InspectHealthy StateFailure SignalRecovery
SyncGET /syncidle, failures 0, behind false, drift 0failures ≥ 5 / behind true / drift > 0refresh, backfill, reconcile
Watermarksync-state docsadvancing each runstuck while behindbackfill the stream
Pending queueGET /sync → pendingWorkcount 0count growing, old oldestAtdrain runs every 5 min; check processor
Rollupsdaily-rollup docsrecomputed every 5 minstale computedAttrigger refresh
Exportsexport records + diskstatuses reach readystuck queuedsweep re-enqueues hourly
Export filesstorage/analytics-exports/swept after 48hdisk growthsweep job
QueueBull Board / logsjobs completing[failure] rethrowstransient → automatic retry; deterministic → code fix

16.8 Backend Risk Register

RiskAreaImpactCurrent MitigationRemaining Gap
Watermark skips committed rowssyncsilent revenue lossoverlap + conditional advance
Two runners on one streamsynclivelocksingle-document lock with lease + renewmulti-process on one replica set not load-tested
Export enqueue lostexportsstuck queuedrecord-first + hourly re-enqueueup to 15 min delay
Filter injectionreportsscope tamperingallowlist, descriptor field names, escaped search
Expensive reportreportsDB loadcaps (period, limit, rows) + rate limits
Refund settled months laterrollupsJanuary overstatedpending rollup_day drain
Revoked admin downloadsexportssnapshot accesslive 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