# Development Plan — `service-satusehat` | Field | Value | |---|---| | Document Version | 1.0 | | Status | Draft for review | | Owner | Backend Engineering — Meninjar | | Last Updated | 2026-05-13 | | Horizon | ~2 quarters (M1 → M5) | > Read this together with `PRD.md` (the *what / why*), `analysis.qmd` (the > *as-is*), and `DEVLOG.md` (the *what happened*). --- ## 1. Guiding Principles 1. **Stabilise before extending.** The 19 FHIR resources work end-to-end but rest on near-zero tests and an empty gRPC surface. Hardening comes before new features. 2. **Pure mappers.** No I/O, no env reads inside `Map*ToFHIR`. Inject from the service layer. This is the `EpisodeOfCare` pattern applied broadly. 3. **One change, one PR, one CI run.** No more multi-resource omnibus commits like `af32d9c`. 4. **Audit everything that talks to SATUSEHAT.** Outbound calls are regulator-visible artefacts. 5. **Production readiness is a checklist, not a feeling.** See §6. --- ## 2. Milestones ### M1 — Hardening (Weeks 1–3) > Goal: make the codebase *safe to refactor*. After M1, every PR runs > through CI with linting + tests, the obvious bugs are fixed, and the > debug-log noise is gone. | ID | Task | Effort | Owner | Acceptance | |-------|--------------------------------------------------------------------------|--------|----------|------------| | T-01 | Set up CI (GitHub Actions): `go vet`, `golangci-lint`, `go test`, `make audit`, `make security-check`, Docker build | 1 d | Backend | PR triggers green pipeline; status checks required for `main` | | T-02 | Add `golangci-lint.yml` config (errcheck, gocritic, gosec, revive, govet) | 0.5 d | Backend | `make lint` returns clean baseline (existing offenders allowlisted, not silenced) | | T-03 | Unit tests for every `Map*ToFHIR` (19 resources) | 4 d | Backend | Golden-file tests; `make test` ≥ 60 % coverage on `internal/satusehat/usecase/**` | | T-04 | Apply the `EpisodeOfCare` pattern to the remaining 18 usecases (drop env reads, accept `orgID` via constructor) | 3 d | Backend | No usecase mapper imports `os`; main.go wires `cfg.SatuSehat.OrgID` for all | | T-05 | Replace `fmt.Printf` debug in `pkg/utils/query/**` with `logger.Debug()` | 0.5 d | Backend | `grep -R "fmt.Printf" pkg/utils/query` returns 0 | | T-06 | Make `NoOp` cache goroutine-safe with `sync.RWMutex` | 0.5 d | Backend | Race-detector tests pass under load | | T-07 | Delete commented-out Kafka producer + ImagingStudy worker from `cmd/api/main.go`, or move behind a `KAFKA_ENABLED` feature flag | 0.5 d | Backend | `main.go` has no commented blocks > 3 lines | | T-08 | Rename `internal/master/role/accses` → `access` | 0.5 d | Backend | One renaming commit; all imports updated | **Exit criteria:** CI required on `main`, lint clean baseline, FHIR mapper unit-test coverage ≥ 60 %, no debug `Printf` in `pkg/`. --- ### M2 — Auditability (Weeks 4–5) > Goal: every outbound SATUSEHAT call leaves a row we can defend in a > Kemenkes audit. | ID | Task | Effort | Owner | Acceptance | |-------|--------------------------------------------------------------------------|--------|----------|------------| | T-10 | Design table `satusehat_submission` | 0.5 d | Backend | Columns: `id, resource, method, endpoint, request_id, request_body_hash, response_status, response_body_hash, satusehat_resource_id, error_code, created_at, latency_ms, org_id, user_id` | | T-11 | Migration + GORM model + CommandRepository / QueryRepository | 1 d | Backend | Goose migration runs; CQRS pair compiles | | T-12 | Wrap `internal/interfaces/satusehat.Client.DoRequest` with an audit interceptor (decorator) | 1 d | Backend | Every outbound call writes a row; failures still write | | T-13 | Add request-ID middleware (X-Request-ID propagation, ULID generation if absent) | 0.5 d | Backend | All logs + audit rows carry the same `request_id` | | T-14 | Retention policy doc — keep submissions for ≥ 25 years per Permenkes 24/2022 | 0.5 d | Backend / Compliance | `docs/RETENTION.md` exists and references the regulation | | T-15 | Health probe `/health/satusehat` — last successful call within 5 min | 0.5 d | Backend | Returns `200` when at least one submission within the window | **Exit criteria:** Audit rows for every outbound call across all 19 resources, joinable on `request_id`, retained per policy. --- ### M3 — gRPC Catch-up (Weeks 6–8) > Goal: bring the gRPC transport from *empty* to *parity with REST for the > four hottest resources*. | ID | Task | Effort | Owner | Acceptance | |-------|--------------------------------------------------------------------------|--------|----------|------------| | T-20 | Author `proto/v1/satusehat/{encounter,episodeofcare,condition,observation}.proto` | 2 d | Backend | `make proto` generates clean Go code | | T-21 | Implement gRPC handlers for the four resources reusing the existing services | 2 d | Backend | gRPC + REST share the **same** service layer | | T-22 | Register handlers in `cmd/api/main.go` gRPC server | 0.5 d | Backend | `grpcurl … list` shows the four services | | T-23 | Add buf lint + breaking-change check to CI | 0.5 d | Backend | PRs that break protobuf surface fail CI | | T-24 | Postman → grpcurl examples in `docs/api/` | 0.5 d | Backend | One example call per service in markdown | **Exit criteria:** Four resources callable via gRPC, behind the same auth, with parity behaviour to REST. --- ### M4 — Resilience (Weeks 9–10) > Goal: survive a flaky SATUSEHAT without dropping submissions. | ID | Task | Effort | Owner | Acceptance | |-------|--------------------------------------------------------------------------|--------|----------|------------| | T-30 | Retry middleware on `Client.DoRequest`: exponential backoff (250 ms · 2^n, cap 8 s), max 5 attempts, retry on 5xx + network errors only | 1 d | Backend | Unit tests with fake transport | | T-31 | Circuit breaker (`sony/gobreaker`) per upstream (FHIR, KFA, KYC, Consent, DICOM) | 1 d | Backend | Opens after 5 consecutive failures, half-opens after 30 s | | T-32 | Idempotency key support — caller supplies `Idempotency-Key`, service dedupes within 24 h via Redis | 1 d | Backend | Replays return the original response, not a duplicate submission | | T-33 | Rate-limit middleware on inbound (Redis token bucket, key by `user_id` then `ip`) | 1 d | Backend | `cfg.Security.RateLimit.RequestsPerMinute` enforced | | T-34 | Refresh-token revocation on logout — persist to `revoked_tokens` table; check on every refresh (closes TODO at `internal/auth/service.go:233`) | 1 d | Backend | Logout test passes; refresh returns 401 after logout | | T-35 | Persist KYC verification result locally (closes TODO in `kyc/service.go`) | 0.5 d | Backend | New table `kyc_verification`; result row on every success | | T-36 | Standardise cache invalidation in `role/master`, `role/pages`, `role/permission`, `role/access` — invalidate on every command op | 1 d | Backend | TODOs removed; integration test covers stale-cache scenario | **Exit criteria:** Service tolerates 30 s of SATUSEHAT 5xx without losing a submission; logout truly invalidates; cache is never stale after a write. --- ### M5 — Production Rollout (Weeks 11–12) > Goal: from "running on a dev box" to "running in the hospital DC, one > instance, watched." | ID | Task | Effort | Owner | Acceptance | |-------|--------------------------------------------------------------------------|--------|----------|------------| | T-40 | Production `docker-compose.prod.yml` review with ops; document the external network requirement | 0.5 d | Backend / Ops | `docs/deployment.md` updated; one-shot setup script | | T-41 | Grafana dashboard JSON (request volume, p50/p95/p99 latency, error rate by code, DB pool, cache hit ratio) | 1 d | Backend | Dashboard imports cleanly; screenshot in `docs/` | | T-42 | Prometheus alert rules (SATUSEHAT 5xx > 5/min, p95 > 5 s for 5 min, DB pool exhaustion) | 0.5 d | Backend / Ops | Rules under `deploy/prom/alerts.yaml` | | T-43 | Backup strategy for `satusehat_submission` and master DB (daily pg_dump, 30-day retention onsite, 1-year offsite encrypted) | 0.5 d | Ops | `docs/BACKUP.md` exists | | T-44 | DR runbook — what to do when SATUSEHAT is down, when Redis is down, when the DB primary fails | 1 d | Backend / Ops | `docs/RUNBOOK.md` exists | | T-45 | Soft launch — 1 hospital, 1 day of shadow traffic | 1 d | All | No P0 incidents; audit table populated | | T-46 | GA — promote to all sites that depend on SATUSEHAT submission | 0.5 d | All | Stakeholder sign-off | **Exit criteria:** Service running in production with full observability, alerts, backups, and a runbook. --- ## 3. Backlog (post-M5, not yet scheduled) - **B-01** Multi-tenant support: one binary, multiple `org_id`s per request based on JWT claim (depends on T-04 pattern across all 19). - **B-02** Event-driven mode: re-enable the commented-out Kafka producer (`cmd/api/main.go:137–141`) so that submissions are emitted as events for downstream consumers (data warehouse, BI). - **B-03** Background worker for ImagingStudy DICOM uploads (re-enable `cmd/api/main.go:286–300` behind a worker process flag). - **B-04** OpenTelemetry tracing (replace ad-hoc request-id with `traceparent`). - **B-05** Bulk submission endpoints (`POST /v1/Encounter/bundle`) for back-fill scenarios. - **B-06** Mobile-friendly DTOs (slimmer JSON than the full FHIR shape). - **B-07** Finish or remove the `tools/generate.go` code generator. - **B-08** Postman → automated contract tests in CI. --- ## 4. Risk Register | ID | Risk | Likelihood | Impact | Mitigation | |------|-------------------------------------------------------------------|------------|----------|---------------------------------------------------------------| | RK-1 | Refactor breaks an in-production FHIR resource | Med | High | T-03 unit tests must land before T-04 refactor cascade | | RK-2 | SATUSEHAT contract changes mid-quarter | Low | High | Contract tests behind a build tag, run nightly against staging | | RK-3 | Hospital DC ops cannot operate Prometheus stack | Med | Med | Provide a one-shot Compose + Grafana with default dashboards | | RK-4 | Single instance is a SPOF | High | Med | M5+ plan to run two instances behind nginx; cache is shared so it's safe | | RK-5 | Audit table grows unbounded | High | Low | Partition by month + offsite archive (T-14) | | RK-6 | Refresh-token revocation table grows unbounded | Med | Low | TTL cleanup job; keep only until exp + 7 d | | RK-7 | gRPC contract evolves and breaks mobile clients | Med | Med | T-23 buf breaking-change check | --- ## 5. Dependencies & Coordination | Dependency | Needed by | Status | |-------------------------------------|-----------|--------| | SATUSEHAT staging credentials | M1+ | ✓ in `.env` | | Keycloak realm config | M1+ | depends on hospital IAM team | | External Docker network `service-general_default` | M5 | ✓ created by `service-general` compose | | Prometheus + Grafana in hospital DC | M5 | ⚠ to confirm with ops | | Offsite backup target | M5 | ⚠ to confirm with ops | --- ## 6. Definition of Done (per task) A task is **Done** only when: 1. Code merged to `main` via PR. 2. CI green (lint + test + audit + security-check + Docker build). 3. Tests cover the happy path and at least one error path. 4. `docs/DEVLOG.md` has a new entry referencing the task ID. 5. If user-visible: `docs/PRD.md` updated; if architectural: `docs/analysis.qmd` updated. 6. If touching ops: `docs/deployment.md` / `RUNBOOK.md` updated. 7. No new TODO without a backlog ID. --- ## 7. Cadence - **Daily**: 15-min stand-up over chat; update task status in the issue tracker. - **Weekly**: 30-min review — what shipped, what's blocked, what's next. - **Per milestone**: written retrospective appended to `DEVLOG.md` with scope `decision`.