SERVICE_OVERVIEW — reporting-service
Bundle index: SERVICE_OVERVIEW · DOMAIN_MODEL · APPLICATION_LOGIC · API_CONTRACTS · EVENT_SCHEMAS · DATA_MODEL · SYNC_CONTRACT · AI_INTEGRATION · SECURITY_MODEL · OBSERVABILITY · TESTING_STRATEGY · DEPLOYMENT_TOPOLOGY · FAILURE_MODES · LOCAL_DEV_SETUP · SERVICE_READINESS · SERVICE_RISK_REGISTER · MIGRATION_PLAN
Strategic anchors: 02 Enterprise Architecture · 04 Event-Driven Architecture · 05 API Design · 06 Data Models · 07 Security, Compliance & Tenancy · 08 AI Architecture
1. Purpose
reporting-service is the document rendering and delivery surface for Ghasi Melmastoon — the multi-tenant hotel SaaS whose backoffice is an Electron offline-first desktop app and whose cloud is GCP. It turns curated, already-computed facts about a tenant's operations into portable, immutable, traceable documents (PDF / Excel / CSV) and delivers them to staff, owners, and (where the law requires) to government registers.
It exists for four reasons no other service can satisfy:
- One rendering pipeline. Every PDF, Excel, and CSV the platform produces flows through one Puppeteer +
exceljs+ streaming-CSV pipeline so branding, signing, locale handling (Pashto/Dari/Persian/Arabic/EN/FR — all RTL/LTR aware), and accessibility are uniform. We refuse to letbilling-serviceship its own PDF library andhousekeeping-serviceship another. - Templates are versioned and immutable. Once a
ReportTemplateis published, every later run cites the exact version. An auditor in 2032 can re-render a 2026 tax statement byte-for-byte from the snapshot. - Regulatory submission is risky and shared. Several target jurisdictions require daily guest-registration data delivered to a tourism authority or police register (Afghanistan, Tajikistan, Iran, GCC variants in Phase 3+). A missed submission is a fine and a license risk. We centralize the submission adapter, retry, escalation, and proof-of-delivery once.
- Sync to the desktop matters. Front-desk and GMs work through outages; subscribed reports plus the last 30 runs per template are replicated to the Electron SQLite so a manager opening her laptop offline still sees yesterday's arrivals report and the latest cash-drawer summary.
2. Bounded context
Context name: Reporting Domain class: Supporting (revenue-adjacent, audit-critical, slow-evolving once stable) Ubiquitous language: Template, TemplateVersion, Report, Run, Artifact, Schedule, Subscription, Filter, Recipient, Delivery, RegulatorySubmission, Jurisdiction, Branding, Locale, ColumnSpec, FilterSpec, LayoutBlock.
What is in: template registry & versioning; on-demand and scheduled run dispatch; rendering (PDF/XLSX/CSV); subscription & delivery routing; regulatory submission adapter framework; artifact lifecycle (signed URLs, retention); audit log of every artifact emission and download.
What is out:
- Computing the underlying numbers →
analytics-service(curated BigQuery facts),billing-service(folio + ledger),reservation-service(operational data),inventory-service,housekeeping-service,staff-service. - Sending email/SMS/push →
notification-service. - Theme/branding source-of-truth →
theme-config-service. - Cross-tenant search →
search-aggregation-service(out of scope; reports are tenant-scoped only). - AI inference →
ai-orchestrator-serviceviaports/AIClient(we use AI for query assistance and anomaly callouts; we never compute the figures themselves with a model).
3. Aggregates owned
| Aggregate | Cardinality | Purpose | Identity prefix |
|---|---|---|---|
ReportTemplate | one per template name (with N versions) | Versioned spec: columns, filters, layout, locale variants, branding, retention | tpl_rep_ |
TemplateVersion | append-only N per template | Frozen snapshot of a template at publish time | (composite, ULID) |
Report | one per logical report (e.g., "daily arrivals") | Pointer to default template, recent runs index, default subscription set | rep_ |
ReportRun | many per report | Single execution: inputs, outputs, status, signing chain, AIProvenance if any | run_ |
ReportSchedule | one per cron rule | Cron expression + target template + filters + subscription set | sch_ |
ReportSubscription | many per report | Recipient + channel + format + delivery audit | sub_ |
ReportFilter | many per tenant | Reusable saved filter (shareable across runs) | flt_ |
ExportArtifact | 1..N per run | Concrete file in GCS (PDF/XLSX/CSV) with checksum, signed URL state, retention class | art_ |
RegulatorySubmission | 0..1 per regulatory run | Submission adapter outcome + retry/audit chain + proof-of-delivery | reg_ |
4. Responsibilities (numbered)
- Template publish flow —
POST /api/v1/reports/templatesvalidatesColumnSpec[],FilterSpec[], layout blocks, locale variants, regulatory metadata, then writes a newTemplateVersionand emitstemplate.published.v1. - On-demand run —
POST /api/v1/reports/runscreates aReportRuninqueued, validates filters against the template'sFilterSpec, places it on a Pub/Sub queue consumed by the render-worker Cloud Run service. - Scheduled run dispatch — Cloud Scheduler hits
/internal/jobs/schedule-fire; the handler resolves the schedule, snapshots the current template version (or pinned version), instantiates aReportRun, and queues it. - Render worker — pulls
ReportRunfrom queue, fetches inputs (BigQuery query againstanalytics-servicecurated tables and/or read-replica fan-out), composes the layout, renders the artifact, uploads to GCS, signs it, emitsreport.completed.v1. - Subscription delivery — for every
ReportSubscriptionmatching the run's report, dispatch a delivery via the appropriate channel: email (vianotification-service), in-app push, desktop sync queue. - Regulatory submission — for
templateVersion.regulatory == true, afterreport.completed.v1the submission adapter for the tenant's jurisdiction runs (HTTP-form, signed XML, SFTP, paper-print fallback) and emitsregulatory.submission_succeeded.v1orregulatory.submission_failed.v1. - Artifact lifecycle — V4 signed URLs minted on demand (15-min TTL); retention enforced by GCS lifecycle rules per retention class (operational 2y/7y; regulatory 10y with object-lock).
- Audit log — every artifact creation, signed-URL minting, download, and delivery is an audit event (subjectKind=
report_artifact). - Subscription opt-out, recipient validation, channel rotation — coordinate with
notification-servicefor opt-out compliance. - AI assistance — AI-assisted query generation and anomaly callouts (see AI_INTEGRATION); AI never computes the figures themselves.
5. Upstream / downstream context map
┌──────────────────────┐
│ tenant-service │ jurisdiction, locale,
│ │ branding inheritance
└──────────┬───────────┘
│ (settings.changed.v1)
│
┌────────────────────┴───────────────────────────┐
│ │
▼ ▼
┌─────────────────────────┐ ┌────────────────────────────────────┐
│ analytics-service │ ──────▶ │ reporting-service │
│ (BigQuery curated │ read │ (templates, runs, schedules, │
│ fact_*/dim_* tables) │ facts │ render workers, regulatory) │
└─────────────────────────┘ └────┬───────────────┬──────────────┘
│ │
┌─────────────────────────┘ │
│ │
▼ ▼
┌──────────────────┐ ┌─────────────────────────┐
│ GCS (artifacts) │ │ notification-service │ email/SMS/push
│ KMS-encrypted │ │ (subscription delivery)│
│ V4 signed URLs │ └────────────┬────────────┘
└──────────────────┘ │
▼
┌─────────────────────┐
│ Recipients (staff, │
│ owners, gov regs) │
└─────────────────────┘
▲
│ (regulatory adapters POST/SFTP/signed-XML)
│
┌─────────┴────────────┐
│ Government register │
│ (per-jurisdiction) │
└──────────────────────┘
6. End-to-end render flow — ASCII sequence
Caller (BFF backoffice / Cloud Scheduler) reporting-service API Pub/Sub: report.run.queued render-worker GCS notification-service audit-service
│ │ │ │ │ │ │
│ POST /api/v1/reports/runs │ │ │ │ │ │
├─────────────────────────────────────────────────▶│ CreateReportRun.use-case │ │ │ │ │
│ │ - validate filters │ │ │ │ │
│ │ - snapshot template ver │ │ │ │ │
│ │ - persist ReportRun(queued) │ │ │ │
│ │ - outbox: report.requested.v1 │ │ │ │
│ │ - publish: report.run.queued (internal worker queue) │ │ │ │
│ ◀── 202 { runId: run_…, status:'queued' } ──── │ ├──────────────────────────▶ │ │ │ │
│ │ │ StartReportRun.uc │ │ │ │
│ │ │ - mark started → outbox: report.started.v1 │
│ │ │ - run BigQuery / read │ │ │ │
│ │ │ - render PDF/XLSX/CSV │ │ │ │
│ │ │ - upload to GCS ────────▶│ │ │ │
│ │ │ - persist artifact rows │ │ │ │
│ │ │ - outbox: report.completed.v1 │
│ │ │ - per subscription: │ │ │
│ │ │ dispatch delivery ──────────────────▶│ │ │
│ │ │ │ │ send email/push │ │
│ │ │ │ │ ── delivered ──▶ │ delivery.recorded.v1 │
│ │ ◀──── inbox: notification.delivery.recorded.v1 ──────────│ │ │ │
│ │ - mark sub delivered → outbox: report.delivered.v1 │ │ │ │
│ │ │ │ │ │ audit (artifact, │
│ │ - audit emit on every step ─────────────────────────────────────────────────────────────────────────────────────▶│ download, deliver) │
For regulatory templates, after report.completed.v1 an additional handler dispatches the submission adapter and emits regulatory.submission_succeeded.v1 / regulatory.submission_failed.v1.
7. Key invariants enforced in the domain layer
- No cross-tenant references. Every
ReportRun,ReportTemplate,ReportSchedulecarries aTenantId; constructors refuse missing or mismatched values. (MELMASTOON.GENERAL.CROSS_TENANT_REFERENCE) - Templates are immutable post-publish. A
TemplateVersioncannot be edited afterpublished_at. Edits create a new version. (MELMASTOON.REPORTING.TEMPLATE_LOCKED) - Run is bound to one template version. Once
ReportRun.templateVersionIdis set, it cannot change; even on retry, the same version is reused. (MELMASTOON.REPORTING.TEMPLATE_VERSION_LOCKED) - Filters validated against template
FilterSpec. Unknown filter keys, type mismatches, or out-of-range values are rejected at use-case entry. (MELMASTOON.REPORTING.FILTER_INVALID) - Regulatory runs require a jurisdiction-bound submission adapter. A run on a
regulatory=truetemplate without a registered adapter for the tenant's jurisdiction is refused. (MELMASTOON.REPORTING.REGULATORY_ADAPTER_MISSING) - Artifacts are append-only. Once persisted, an
ExportArtifactrow is never updated except itssigned_url_*cache fields andretention_class_*. (MELMASTOON.REPORTING.ARTIFACT_LOCKED) - Retention class is derived, not asserted. It comes from
templateVersion.retentionClass; a caller cannot override it. (MELMASTOON.REPORTING.RETENTION_DERIVED_ONLY) - OCC version checked on every save for
ReportScheduleandReportSubscription. (MELMASTOON.GENERAL.PRECONDITION_FAILED)
8. Hot read paths
| Read | Frequency | Caching strategy |
|---|---|---|
GET /reports/runs/:id (poll for status) | high during a run | Memorystore key rep:run:<runId>, TTL 5 s, invalidated on every status change |
GET /reports/runs/:id/artifacts/:artId/download | medium | Signed URL minted fresh; URL itself cached 60 s in Memorystore |
GET /reports/templates | low | Postgres; HTTP cache 60 s with Cache-Control |
GET /reports/runs?reportId=…&limit=30 (subscription list) | medium | Memorystore key rep:runs:<reportId>:recent, invalidated on report.completed.v1 |
| Desktop sync pull (subscribed reports + last 30 runs) | every 60 s per device | Sync engine deltas via since cursor; no service-side cache |
9. Cost & scale envelope
| Dimension | Target |
|---|---|
| On-demand runs per tenant per day | 10 (small) → 2,000 (chain) |
| Scheduled runs per tenant per day | 5 (small) → 200 (chain incl. property fan-out) |
| Concurrent renders globally (peak) | 50–200 (autoscale 0→20 worker instances; ~10 runs/instance) |
| p95 tabular render (≤ 50k rows) | < 12 s end-to-end |
| p95 regulatory PDF | < 25 s |
| Average artifact size (PDF) | 100–500 KB |
| Average artifact size (XLSX large) | 1–10 MB |
| GCS storage growth per tenant | ~2 GB/year typical, ~20 GB/year for chains |
| Cloud Run min replicas (API) | 2 |
| Cloud Run min replicas (worker) | 0 (autoscale to 20 on queue depth) |
10. Decision log (anchors)
- Why we don't compute figures here — separating render from compute lets analytics & billing change query plans without affecting the document layout, and lets reporting evolve template versions without touching the warehouse. (See 02 §7.)
- Why Puppeteer for PDF, not wkhtmltopdf or LibreOffice — Puppeteer is the only option with first-class CSS Grid + RTL support and predictable font handling for Pashto/Persian/Arabic. We sandbox it in a dedicated Cloud Run worker with
--no-sandboxonly inside a gVisor-isolated container. - Why scheduled runs go through Cloud Scheduler, not an in-process cron — schedules survive deploys, are independently inspectable in GCP, and integrate with our existing IAM/audit story.
- Why regulatory submission adapters live here — a regulatory submission is a delivery of an already-rendered artifact. Putting the adapter where the artifact is born minimizes blast radius and keeps proof-of-delivery in one audit.
- Why we replicate to the desktop — see ADR-0003 Electron Offline-First. A GM doing month-end review on a flaky link must still see her subscribed reports.
11. What this service depends on
- NestJS for presentation + DI composition root.
- Drizzle ORM for Postgres access in the infrastructure layer.
@google-cloud/pubsubfor outbox publishing and worker queue.@google-cloud/storagefor GCS artifact upload + V4 signed URL.@google-cloud/bigqueryfor analytics fact reads.- Puppeteer (chromium-headless) in the worker only — never imported in the API service.
exceljsfor XLSX,fast-csvfor streaming CSV.- Ports the application layer depends on (interfaces only):
ReportRepository,TemplateRepository,ScheduleRepository,SubscriptionRepository,ArtifactRepository,RegulatorySubmissionRepositoryEventPublisher(outbox-backed)Clock,IdGeneratorAnalyticsClient(BigQuery-backed)BillingReadClient,ReservationReadClient,InventoryReadClient,HousekeepingReadClient,StaffReadClientRendererPort(PDF/XLSX/CSV)ArtifactStorage(GCS)NotificationClientAIClient(callsai-orchestrator-service)RegulatorySubmissionPort(per-jurisdiction adapter set)IdentityResolver
The domain layer depends on nothing outside @ghasi/domain-primitives and the standard library. CI fails on any I/O import inside src/domain/.
12. References
- Reporting summary: 03-microservices/reporting-service.md
- Analytics fact tables:
services/analytics-service/DATA_MODEL.md - API conventions: 05 API Design
- Schema, RLS, ID prefixes: 06 Data Models
- Notification delivery:
docs/03-microservices/notification-service.md - Standards: standards/NAMING, standards/ERROR_CODES, standards/SERVICE_TEMPLATE