Skip to main content

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:

  1. 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 let billing-service ship its own PDF library and housekeeping-service ship another.
  2. Templates are versioned and immutable. Once a ReportTemplate is 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.
  3. 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.
  4. 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-service via ports/AIClient (we use AI for query assistance and anomaly callouts; we never compute the figures themselves with a model).

3. Aggregates owned

AggregateCardinalityPurposeIdentity prefix
ReportTemplateone per template name (with N versions)Versioned spec: columns, filters, layout, locale variants, branding, retentiontpl_rep_
TemplateVersionappend-only N per templateFrozen snapshot of a template at publish time(composite, ULID)
Reportone per logical report (e.g., "daily arrivals")Pointer to default template, recent runs index, default subscription setrep_
ReportRunmany per reportSingle execution: inputs, outputs, status, signing chain, AIProvenance if anyrun_
ReportScheduleone per cron ruleCron expression + target template + filters + subscription setsch_
ReportSubscriptionmany per reportRecipient + channel + format + delivery auditsub_
ReportFiltermany per tenantReusable saved filter (shareable across runs)flt_
ExportArtifact1..N per runConcrete file in GCS (PDF/XLSX/CSV) with checksum, signed URL state, retention classart_
RegulatorySubmission0..1 per regulatory runSubmission adapter outcome + retry/audit chain + proof-of-deliveryreg_

4. Responsibilities (numbered)

  1. Template publish flowPOST /api/v1/reports/templates validates ColumnSpec[], FilterSpec[], layout blocks, locale variants, regulatory metadata, then writes a new TemplateVersion and emits template.published.v1.
  2. On-demand runPOST /api/v1/reports/runs creates a ReportRun in queued, validates filters against the template's FilterSpec, places it on a Pub/Sub queue consumed by the render-worker Cloud Run service.
  3. Scheduled run dispatch — Cloud Scheduler hits /internal/jobs/schedule-fire; the handler resolves the schedule, snapshots the current template version (or pinned version), instantiates a ReportRun, and queues it.
  4. Render worker — pulls ReportRun from queue, fetches inputs (BigQuery query against analytics-service curated tables and/or read-replica fan-out), composes the layout, renders the artifact, uploads to GCS, signs it, emits report.completed.v1.
  5. Subscription delivery — for every ReportSubscription matching the run's report, dispatch a delivery via the appropriate channel: email (via notification-service), in-app push, desktop sync queue.
  6. Regulatory submission — for templateVersion.regulatory == true, after report.completed.v1 the submission adapter for the tenant's jurisdiction runs (HTTP-form, signed XML, SFTP, paper-print fallback) and emits regulatory.submission_succeeded.v1 or regulatory.submission_failed.v1.
  7. 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).
  8. Audit log — every artifact creation, signed-URL minting, download, and delivery is an audit event (subjectKind=report_artifact).
  9. Subscription opt-out, recipient validation, channel rotation — coordinate with notification-service for opt-out compliance.
  10. 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

  1. No cross-tenant references. Every ReportRun, ReportTemplate, ReportSchedule carries a TenantId; constructors refuse missing or mismatched values. (MELMASTOON.GENERAL.CROSS_TENANT_REFERENCE)
  2. Templates are immutable post-publish. A TemplateVersion cannot be edited after published_at. Edits create a new version. (MELMASTOON.REPORTING.TEMPLATE_LOCKED)
  3. Run is bound to one template version. Once ReportRun.templateVersionId is set, it cannot change; even on retry, the same version is reused. (MELMASTOON.REPORTING.TEMPLATE_VERSION_LOCKED)
  4. 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)
  5. Regulatory runs require a jurisdiction-bound submission adapter. A run on a regulatory=true template without a registered adapter for the tenant's jurisdiction is refused. (MELMASTOON.REPORTING.REGULATORY_ADAPTER_MISSING)
  6. Artifacts are append-only. Once persisted, an ExportArtifact row is never updated except its signed_url_* cache fields and retention_class_*. (MELMASTOON.REPORTING.ARTIFACT_LOCKED)
  7. Retention class is derived, not asserted. It comes from templateVersion.retentionClass; a caller cannot override it. (MELMASTOON.REPORTING.RETENTION_DERIVED_ONLY)
  8. OCC version checked on every save for ReportSchedule and ReportSubscription. (MELMASTOON.GENERAL.PRECONDITION_FAILED)

8. Hot read paths

ReadFrequencyCaching strategy
GET /reports/runs/:id (poll for status)high during a runMemorystore key rep:run:<runId>, TTL 5 s, invalidated on every status change
GET /reports/runs/:id/artifacts/:artId/downloadmediumSigned URL minted fresh; URL itself cached 60 s in Memorystore
GET /reports/templateslowPostgres; HTTP cache 60 s with Cache-Control
GET /reports/runs?reportId=…&limit=30 (subscription list)mediumMemorystore key rep:runs:<reportId>:recent, invalidated on report.completed.v1
Desktop sync pull (subscribed reports + last 30 runs)every 60 s per deviceSync engine deltas via since cursor; no service-side cache

9. Cost & scale envelope

DimensionTarget
On-demand runs per tenant per day10 (small) → 2,000 (chain)
Scheduled runs per tenant per day5 (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-sandbox only 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/pubsub for outbox publishing and worker queue.
  • @google-cloud/storage for GCS artifact upload + V4 signed URL.
  • @google-cloud/bigquery for analytics fact reads.
  • Puppeteer (chromium-headless) in the worker only — never imported in the API service.
  • exceljs for XLSX, fast-csv for streaming CSV.
  • Ports the application layer depends on (interfaces only):
    • ReportRepository, TemplateRepository, ScheduleRepository, SubscriptionRepository, ArtifactRepository, RegulatorySubmissionRepository
    • EventPublisher (outbox-backed)
    • Clock, IdGenerator
    • AnalyticsClient (BigQuery-backed)
    • BillingReadClient, ReservationReadClient, InventoryReadClient, HousekeepingReadClient, StaffReadClient
    • RendererPort (PDF/XLSX/CSV)
    • ArtifactStorage (GCS)
    • NotificationClient
    • AIClient (calls ai-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