update All feture and service
This commit is contained in:
+209
@@ -0,0 +1,209 @@
|
||||
# Development Log — `service-satusehat`
|
||||
|
||||
> Append-only journal of meaningful changes, decisions, and incidents.
|
||||
> Newest entries on top. Each entry: **date — author — scope — summary**,
|
||||
> followed by **what / why / how / impact** as needed.
|
||||
>
|
||||
> Conventions:
|
||||
> - `feat` new feature · `fix` bug fix · `refactor` no behaviour change ·
|
||||
> `chore` housekeeping · `infra` ops / build · `docs` documentation ·
|
||||
> `incident` production issue · `decision` ADR-lite.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-13 — meninjar — `refactor` · scope: `episodeofcare`
|
||||
|
||||
**Summary.** Refactored `EpisodeOfCare` usecase to remove environment-variable
|
||||
reads from the mapper and to inject the organisation ID via the service
|
||||
constructor.
|
||||
|
||||
**What changed (uncommitted as of this entry):**
|
||||
|
||||
- `internal/satusehat/usecase/episodeofcare/mapper.go`
|
||||
- Removed `import "os"` and the `os.Getenv("SATUSEHAT_ORG_ID")` lookup.
|
||||
- `MapRequestToFHIR` now takes `OrganizationID` purely from `req`.
|
||||
- `internal/satusehat/usecase/episodeofcare/repository.go`
|
||||
- Removed the `hiddenCtx struct{ context.Context }` wrapper.
|
||||
- `executeRequest` now passes `ctx` to `client.DoRequest` directly.
|
||||
- `internal/satusehat/usecase/episodeofcare/service.go`
|
||||
- Constructor `NewService(repo, orgID string)` (was `NewService(repo)`).
|
||||
- Both `Create` and `Update` inject `s.orgID` into `req.OrganizationID`
|
||||
when the caller leaves it blank.
|
||||
- `cmd/api/main.go`
|
||||
- Updated wiring to pass `cfg.SatuSehat.OrgID` into `episodeofcare.NewService`.
|
||||
|
||||
**Why.**
|
||||
|
||||
1. Pure mappers (no env reads, no I/O) are trivial to unit-test.
|
||||
2. Removes hidden global coupling — one binary can now serve multiple
|
||||
facility identities by constructing multiple services with different
|
||||
`orgID`s.
|
||||
3. `hiddenCtx` was an undocumented wrapper that obscured the context chain
|
||||
and broke `context.Value` propagation.
|
||||
|
||||
**Impact.**
|
||||
|
||||
- No public API change for HTTP callers.
|
||||
- Other 18 FHIR usecases still read env in their mappers — they are flagged
|
||||
in `DEVPLAN.md` task **T-04** for the same treatment.
|
||||
|
||||
**Follow-ups.**
|
||||
|
||||
- Add unit tests for `MapRequestToFHIR` (golden-file style).
|
||||
- Replicate the pattern in remaining usecases.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-13 — meninjar — `docs` · scope: project
|
||||
|
||||
**Summary.** Authored four governance documents under `docs/`:
|
||||
|
||||
- `docs/PRD.md` — Product Requirements Document.
|
||||
- `docs/analysis.qmd` — Quarto technical analysis (architecture, modules, gaps).
|
||||
- `docs/DEVLOG.md` — this file.
|
||||
- `docs/DEVPLAN.md` — forward roadmap with milestones M1–M5.
|
||||
|
||||
**Why.** No high-level product or planning docs existed in the repo. The
|
||||
`README.md` and `DOCUMENT.md` cover how to run the service but not
|
||||
**what / why / next**.
|
||||
|
||||
**Impact.** Onboarding new engineers, communicating with stakeholders, and
|
||||
prioritising the next two quarters of work all become tractable.
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-06 — meninjar — `infra` · scope: Docker / Compose
|
||||
|
||||
**Summary.** Bumped builder image and reshaped the production Compose file.
|
||||
|
||||
**What changed:**
|
||||
|
||||
- `Dockerfile`
|
||||
- `golang:1.22-alpine` → `golang:1.25-alpine`.
|
||||
- Added `WORKDIR /app` in the final stage.
|
||||
- Dropped the `HEALTHCHECK` directive (was firing `/app/main -health`
|
||||
which is not a real flag — false-failing the container).
|
||||
- Exposed port changed `8080 → 8196` to match the production listener.
|
||||
- `docker-compose.dev.yml`
|
||||
- Container name renamed `service-satusehat → service-satusehat-dev`.
|
||||
- Removed the orphan `CONFIG_PATH=/app/config.yaml` env (config is loaded
|
||||
relative to the binary, not from an absolute path).
|
||||
- `docker-compose.prod.yml`
|
||||
- Replaced the inline `redis:7-alpine` sidecar with an external network
|
||||
`service-general_default` that points to the **shared** Redis run by
|
||||
`service-general`.
|
||||
- Ports remapped to `8196:8196` (REST) and `8197:8197` (gRPC, future).
|
||||
- Container renamed `service-general → service-satusehat-prod`.
|
||||
|
||||
**Why.**
|
||||
|
||||
- Hospital DC standardises on Go 1.25 toolchain.
|
||||
- Health-check directive was generating false alarms and feeding monitoring
|
||||
noise — the in-app `/api/v1/health` endpoint already exists.
|
||||
- Redis must be shared with `service-general` to avoid duplicate caches and
|
||||
inconsistent rate-limit state.
|
||||
|
||||
**Impact.**
|
||||
|
||||
- Production stack now requires the external network to exist:
|
||||
`docker network create -d bridge service-general_default` (or it is
|
||||
created by the `service-general` repo's compose).
|
||||
|
||||
---
|
||||
|
||||
## 2026-05-04 → 2026-05-12 — meninjar — `chore` · scope: logs
|
||||
|
||||
**Summary.** Daily log files accumulated under `logs/2026/05/` from dev runs.
|
||||
|
||||
These are *not* committed; they exist only on the developer machine. They
|
||||
should be ignored via `.gitignore` (already excluded by repo policy).
|
||||
|
||||
---
|
||||
|
||||
## af32d9c — meninjar — `feat` · scope: SATUSEHAT (commit on `main`)
|
||||
|
||||
**Title:** *penambahan All case Satu sehat*
|
||||
|
||||
**Summary.** Added the remaining FHIR usecases so that the service now
|
||||
covers all 19 SATUSEHAT resources end-to-end.
|
||||
|
||||
**Resources covered in this commit batch:**
|
||||
AllergyIntolerance, CarePlan, ClinicalImpression, Composition, Condition,
|
||||
DiagnosticReport, Encounter, EpisodeOfCare, ImagingStudy, Immunization,
|
||||
Medication, MedicationDispense, MedicationRequest, MedicationStatement,
|
||||
Observation, Procedure, QuestionnaireResponse, ServiceRequest, Specimen
|
||||
(plus minimal Studies for DICOM linkage).
|
||||
|
||||
**Pattern adopted:** `dto.go` + `mapper.go` + `repository.go` + `service.go`
|
||||
per resource (see Appendix B of `docs/analysis.qmd`).
|
||||
|
||||
---
|
||||
|
||||
## 135c631 — meninjar — `fix` · scope: SATUSEHAT (commit on `main`)
|
||||
|
||||
**Title:** *Perbaikan Service Satu Sehat*
|
||||
|
||||
**Summary.** Bug fixes across the SATUSEHAT client and early usecase
|
||||
modules. Details inferred from the commit name; per-file detail to be
|
||||
back-filled when a follow-up retrospective is held.
|
||||
|
||||
---
|
||||
|
||||
## 0adf9ef — meninjar — `fix` · scope: SATUSEHAT (commit on `main`)
|
||||
|
||||
**Title:** *Perbaikan Service Satu sehat*
|
||||
|
||||
**Summary.** Further fixes after the initial SATUSEHAT integration
|
||||
landing. Same back-fill caveat as above.
|
||||
|
||||
---
|
||||
|
||||
## 4e59b96 — meninjar — `chore` · scope: gRPC (commit on `main`)
|
||||
|
||||
**Title:** *perbaikan GRPC Generate*
|
||||
|
||||
**Summary.** Adjusted the gRPC code-generation tooling. Net result is that
|
||||
the generation pipeline *runs*, but no `.proto` files are committed and
|
||||
no services are registered yet — see DEVPLAN M3.
|
||||
|
||||
---
|
||||
|
||||
## edfaa88 — meninjar — `feat` · scope: project (initial commit)
|
||||
|
||||
**Title:** *first commit*
|
||||
|
||||
**Summary.** Initial scaffold of the service: Clean Architecture layout,
|
||||
multi-DB layer, error system, query builder, auth module, master/role
|
||||
modules, BPJS client skeleton, SATUSEHAT client skeleton, Dockerfile,
|
||||
docker-compose files, Makefile, README.md, DOCUMENT.md.
|
||||
|
||||
---
|
||||
|
||||
# How to add a new entry
|
||||
|
||||
Copy this template to the top of the file:
|
||||
|
||||
```markdown
|
||||
## YYYY-MM-DD — author — `feat|fix|refactor|chore|infra|docs|incident|decision` · scope: <module>
|
||||
|
||||
**Summary.** One-paragraph headline.
|
||||
|
||||
**What changed.** Bullet list of file-level changes (use `file:line` when
|
||||
useful).
|
||||
|
||||
**Why.** Motivation, constraint, or incident that drove the change.
|
||||
|
||||
**Impact.** API breakage? Ops action required? New env vars?
|
||||
|
||||
**Follow-ups.** Anything intentionally left for later, linked to a
|
||||
DEVPLAN task ID when applicable.
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- **Append-only.** Never rewrite a historical entry; correct via a new
|
||||
entry that references the older one.
|
||||
- **Decisions** that are architecturally load-bearing get their own entry
|
||||
with the `decision` label (think ADR-lite).
|
||||
- Keep entries terse: enough that a new joiner can reconstruct the
|
||||
reasoning without paging in the PR.
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
# 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`.
|
||||
+252
@@ -0,0 +1,252 @@
|
||||
# Product Requirements Document (PRD)
|
||||
## Service: `service-satusehat`
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Document Version | 1.0 |
|
||||
| Status | Draft |
|
||||
| Owner | Backend Engineering — Meninjar |
|
||||
| Last Updated | 2026-05-13 |
|
||||
| Repository | `goprint/service-satusehat` |
|
||||
| Primary Language | Go 1.25 |
|
||||
| Target Audience | Backend engineers, integration partners, ops, hospital IT (RS) |
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
### 1.1 Product Summary
|
||||
|
||||
`service-satusehat` is a backend microservice that acts as the **integration gateway** between an Indonesian hospital information system (SIMRS / HIS) and Indonesia's national health platforms — primarily **SATUSEHAT** (HL7 FHIR R4) operated by the Ministry of Health, with secondary integration paths for **BPJS Kesehatan** (VClaim, Antrol, Apotek, Aplicare, IHS) and supporting infrastructure (Keycloak SSO, MinIO object storage, Redis cache, Prometheus observability).
|
||||
|
||||
The service exposes a **dual transport surface** (REST via Gin and gRPC) and is built on **Clean Architecture + Domain-Driven Design + CQRS**. It centralizes all outbound calls to SATUSEHAT, normalizes FHIR resource construction, manages OAuth2 tokens, and protects upstream callers from FHIR/JSON-Patch complexity.
|
||||
|
||||
### 1.2 Problem Statement
|
||||
|
||||
Hospitals in Indonesia are mandated by the Ministry of Health to send clinical encounter data to SATUSEHAT (HL7 FHIR). Direct integration is hard because:
|
||||
|
||||
1. **Heterogeneous internal data sources** — most hospitals have data spread across multiple databases (PostgreSQL / MySQL / SQL Server) and legacy systems.
|
||||
2. **Strict FHIR R4 contract** — resource structure, references, codings, and identifier systems must follow SATUSEHAT IG.
|
||||
3. **OAuth2 token lifecycle** — SATUSEHAT requires short-lived tokens with automatic refresh and concurrency-safe caching.
|
||||
4. **Auxiliary services** — KFA (drug master), KYC, Consent, DICOM, BPJS all have different auth, signing, and payload conventions.
|
||||
5. **Observability & auditability** — every regulated submission must be logged, traceable, and retryable.
|
||||
|
||||
A single integration service that solves the above once, and is reusable across hospital products, is significantly cheaper than re-implementing the contract in every product team.
|
||||
|
||||
### 1.3 Goals
|
||||
|
||||
| # | Goal | Success indicator |
|
||||
|---|---|---|
|
||||
| G1 | Provide one canonical Go service that submits every required FHIR resource to SATUSEHAT | All 19 FHIR resources implemented with Create/Update/Patch/Get/Search |
|
||||
| G2 | Abstract OAuth2 + auxiliary auth (KFA, KYC, Consent, DICOM) from callers | Single client handles token caching and header injection |
|
||||
| G3 | Be deployable in a hospital data center with minimal ops effort | Single Docker image, distroless, Compose stack with dev/prod profiles |
|
||||
| G4 | Stay observable in production | Prometheus metrics, structured logs, health endpoints, request tracing fields |
|
||||
| G5 | Stay vendor-agnostic at the storage layer | Multi-DB support (Postgres, MySQL, SQL Server, MongoDB, SQLite) with read replicas |
|
||||
| G6 | Be safe to refactor and extend | Clean Architecture boundaries, generated code, CQRS separation |
|
||||
|
||||
### 1.4 Non-Goals
|
||||
|
||||
- ❌ **Front-end UI** — this is a backend service only.
|
||||
- ❌ **Long-term clinical data storage** — the hospital's primary database remains the source of truth.
|
||||
- ❌ **Generic FHIR server** — only SATUSEHAT-required resources and profiles are supported, not the full HL7 FHIR spec.
|
||||
- ❌ **BPJS claim submission UI/UX** — only the integration layer is in scope.
|
||||
- ❌ **HL7 v2 messaging** — out of scope.
|
||||
|
||||
---
|
||||
|
||||
## 2. Target Users & Personas
|
||||
|
||||
### 2.1 Primary Users (callers)
|
||||
|
||||
| Persona | Description | What they need |
|
||||
|---|---|---|
|
||||
| **SIMRS Backend** | Internal hospital ERP/HIS server that emits clinical events | A stable REST/gRPC contract that hides SATUSEHAT details |
|
||||
| **Mobile / Web App teams** | Patient portal, clinician app | A simple "submit encounter" API; no FHIR construction |
|
||||
| **Data engineering** | Builds dashboards / reports on submitted resources | Read-back, search, and audit logs |
|
||||
|
||||
### 2.2 Secondary Users
|
||||
|
||||
| Persona | Description |
|
||||
|---|---|
|
||||
| **Ops / SRE** | Runs the container in the hospital data center. Cares about logs, metrics, health, restart safety |
|
||||
| **Integration developers** | Add new SATUSEHAT resources, debug failures |
|
||||
| **Compliance / regulators** | Need audit trail of submissions for inspection |
|
||||
|
||||
---
|
||||
|
||||
## 3. Functional Requirements
|
||||
|
||||
### 3.1 SATUSEHAT FHIR Submission (P0 — core value)
|
||||
|
||||
The service MUST expose endpoints for the following 19 FHIR resources, each with **Create / Update (full) / Patch (JSON Patch RFC 6902) / Get-by-ID / Search**:
|
||||
|
||||
| # | Resource | Use case |
|
||||
|---|---|---|
|
||||
| 1 | AllergyIntolerance | Patient allergy registry |
|
||||
| 2 | CarePlan | Treatment plan |
|
||||
| 3 | ClinicalImpression | Clinician assessment |
|
||||
| 4 | Composition | Document grouping |
|
||||
| 5 | Condition | Diagnosis (ICD-10) |
|
||||
| 6 | DiagnosticReport | Lab / radiology report |
|
||||
| 7 | Encounter | Patient visit (rawat jalan/inap/IGD) |
|
||||
| 8 | EpisodeOfCare | Continuous treatment episode |
|
||||
| 9 | ImagingStudy | Radiology study link |
|
||||
| 10 | Immunization | Vaccination record |
|
||||
| 11 | Medication | Drug definition |
|
||||
| 12 | MedicationDispense | Pharmacy dispense event |
|
||||
| 13 | MedicationRequest | Prescription |
|
||||
| 14 | MedicationStatement | Patient-reported medication |
|
||||
| 15 | Observation | Vital signs, lab results |
|
||||
| 16 | Procedure | Performed procedure (ICD-9-CM) |
|
||||
| 17 | QuestionnaireResponse | Form / triage answers |
|
||||
| 18 | ServiceRequest | Lab / radiology order |
|
||||
| 19 | Specimen | Lab specimen |
|
||||
|
||||
Plus minimal **Studies (DICOM)** for imaging linkage.
|
||||
|
||||
### 3.2 SATUSEHAT Reference Lookups (P0)
|
||||
|
||||
The service MUST proxy reads to the following SATUSEHAT registries:
|
||||
|
||||
- **Patient** — search by NIK, name, DOB
|
||||
- **Practitioner** — by NIK or NPP
|
||||
- **Organization** — facility registry
|
||||
- **Location** — room/bed
|
||||
- **KFA** — Katalog Farmasi Alkes (drug & device master)
|
||||
- **KYC** — verifikasi identitas
|
||||
- **Auth** — token refresh / validate
|
||||
|
||||
### 3.3 Authentication & Authorization (P0)
|
||||
|
||||
- **Inbound**: JWT-based auth (with Keycloak, static-token, and hybrid modes). RBAC via Role / Page / Permission / Access master tables.
|
||||
- **Outbound**: OAuth2 client-credentials flow to SATUSEHAT with concurrency-safe in-memory token caching (≥1 minute buffer before expiry).
|
||||
- **BPJS**: HMAC-SHA256 signature header per BPJS spec (cons-id + secret).
|
||||
|
||||
### 3.4 Storage & Master Data (P1)
|
||||
|
||||
- Multi-database support (Postgres preferred) for:
|
||||
- Auth (users, sessions, tokens)
|
||||
- Role / Page / Permission / Access (RBAC master)
|
||||
- Audit log of SATUSEHAT submissions (TBD — see §7 Gaps)
|
||||
- Read replicas with round-robin load balancing.
|
||||
- GORM as primary ORM; raw SQL via sqlx / squirrel where ORM is too slow.
|
||||
|
||||
### 3.5 Cross-Cutting (P1)
|
||||
|
||||
- Structured logging (logrus) to console + daily file `logs/YYYY/MM/YYYY-MM-DD.log`.
|
||||
- Prometheus metrics: HTTP request counts/latency, DB pool stats, cache hit ratio, error counters by code.
|
||||
- Health endpoints: overall + per-database + external (SATUSEHAT, KFA, KYC).
|
||||
- i18n-ready error system (English + Indonesian).
|
||||
- Soft-delete + audit trail at the master-data layer.
|
||||
|
||||
### 3.6 Object Storage (P2)
|
||||
|
||||
- MinIO integration for storing supporting artifacts (e.g., DICOM tarballs, KYC scans, consent PDFs). Buckets configurable per environment.
|
||||
|
||||
---
|
||||
|
||||
## 4. Non-Functional Requirements
|
||||
|
||||
| Category | Requirement |
|
||||
|---|---|
|
||||
| **Performance** | P95 latency for outbound SATUSEHAT create ≤ 2 s when SATUSEHAT responds within SLA; service overhead ≤ 50 ms |
|
||||
| **Throughput** | ≥ 100 sustained writes/sec per service instance (single-node, 4 vCPU) |
|
||||
| **Availability** | 99.5 % (hospital data-center, single instance acceptable; horizontal scale-out supported) |
|
||||
| **Recovery** | Graceful shutdown ≤ 5 s; in-flight requests drained on SIGTERM |
|
||||
| **Security** | No plaintext secrets in logs; parameterised queries; CORS configurable; rate-limit configurable |
|
||||
| **Compliance** | Comply with Permenkes 24/2022 (rekam medis elektronik) and SATUSEHAT IG R4 |
|
||||
| **Observability** | Every outbound SATUSEHAT call MUST be traceable by `request_id` (TBD) |
|
||||
| **Portability** | Single static binary, distroless image, runs on amd64 Linux |
|
||||
| **Time zone** | All timestamps in `Asia/Jakarta` (WIB); FHIR payloads use ISO-8601 with offset |
|
||||
|
||||
---
|
||||
|
||||
## 5. Out-of-Scope Items (deliberately excluded)
|
||||
|
||||
- Web frontend / admin UI.
|
||||
- Long-term clinical archival.
|
||||
- HL7 v2 messaging.
|
||||
- Direct BPJS claim file generation (RBAC adjustments only).
|
||||
- Patient-facing mobile API.
|
||||
|
||||
---
|
||||
|
||||
## 6. Dependencies
|
||||
|
||||
### 6.1 External (third-party)
|
||||
|
||||
| Dependency | Purpose | Notes |
|
||||
|---|---|---|
|
||||
| SATUSEHAT FHIR R4 | Primary integration target | Staging + production base URLs configured |
|
||||
| SATUSEHAT KFA | Drug & device master | Separate endpoint and headers |
|
||||
| SATUSEHAT KYC | Identity verification | Public/private key pair (config) |
|
||||
| SATUSEHAT Consent | Patient consent | Webhook secret configured |
|
||||
| SATUSEHAT DICOM | Imaging study upload | Separate endpoint |
|
||||
| BPJS VClaim / Antrol / Apotek / Aplicare / IHS | Insurance integration | HMAC-SHA256 auth |
|
||||
| Keycloak | SSO / OAuth2 issuer (optional) | JWKS validation |
|
||||
|
||||
### 6.2 Internal
|
||||
|
||||
- PostgreSQL 12+ (recommended)
|
||||
- Redis 6+ (cache, rate limit) — optional but recommended
|
||||
- MinIO (S3-compatible) — optional
|
||||
- Prometheus (scrape `/metrics`)
|
||||
|
||||
---
|
||||
|
||||
## 7. Known Gaps & Risks (carried into DEVPLAN)
|
||||
|
||||
| # | Gap | Severity | Owner |
|
||||
|---|---|---|---|
|
||||
| R1 | gRPC defined in config but **no `.proto` files committed** and registry empty | High | Backend |
|
||||
| R2 | Test coverage ≈ **2 files total** — query builder only | High | Backend |
|
||||
| R3 | Rate-limit config exists, no middleware implementation | Medium | Backend |
|
||||
| R4 | Auth logout does **not** revoke refresh tokens in DB (TODO at `internal/auth/service.go:233`) | Medium | Backend |
|
||||
| R5 | KYC service does **not** persist verification to local DB | Medium | Backend |
|
||||
| R6 | Role / Page / Permission services have TODO cache invalidation | Medium | Backend |
|
||||
| R7 | 60+ `fmt.Printf` debug calls in `pkg/utils/query` — leaks to stdout in production | Low | Backend |
|
||||
| R8 | NoOp cache fallback is not thread-safe (map without mutex) | Medium | Backend |
|
||||
| R9 | Folder name typo `internal/master/role/accses/` (should be `access`) | Low | Backend |
|
||||
| R10 | Audit-log table for SATUSEHAT submissions not yet defined | High | Backend |
|
||||
| R11 | No CI pipeline committed | High | Ops |
|
||||
|
||||
---
|
||||
|
||||
## 8. Success Metrics
|
||||
|
||||
| Metric | Target |
|
||||
|---|---|
|
||||
| SATUSEHAT submission success rate | ≥ 99 % (per resource, per hour) |
|
||||
| Coverage on `internal/satusehat/usecase/**` | ≥ 60 % unit tests, ≥ 1 contract test per resource |
|
||||
| Mean time to add a new FHIR resource | ≤ 1 day (via code-gen template) |
|
||||
| Production crash-loop incidents | 0 per month |
|
||||
| Time to detect a SATUSEHAT outage | ≤ 1 minute via health probe |
|
||||
|
||||
---
|
||||
|
||||
## 9. Release Plan (high-level — detail in DEVPLAN)
|
||||
|
||||
| Milestone | Description |
|
||||
|---|---|
|
||||
| **M1 — Hardening** | Fix R1–R3, R7, R8; add CI; unit-test FHIR mappers |
|
||||
| **M2 — Auditability** | Implement audit-log table + middleware emitting one row per outbound SATUSEHAT call (R10) |
|
||||
| **M3 — gRPC** | Author proto files; generate; register services for at least Encounter, EpisodeOfCare, Condition, Observation |
|
||||
| **M4 — Resilience** | Retry with exponential backoff for SATUSEHAT 5xx + circuit breaker |
|
||||
| **M5 — Production rollout** | Single-instance deployment in hospital DC, then dual-instance behind nginx |
|
||||
|
||||
---
|
||||
|
||||
## 10. Glossary
|
||||
|
||||
| Term | Meaning |
|
||||
|---|---|
|
||||
| **SATUSEHAT** | Indonesia national health-data platform (Kemenkes) |
|
||||
| **FHIR** | Fast Healthcare Interoperability Resources (HL7 R4) |
|
||||
| **BPJS** | Badan Penyelenggara Jaminan Sosial — Indonesian social insurance |
|
||||
| **VClaim** | BPJS claim API |
|
||||
| **KFA** | Katalog Farmasi Alkes (drug/device catalogue) |
|
||||
| **KYC** | Know Your Customer — identity verification |
|
||||
| **SIMRS** | Sistem Informasi Manajemen Rumah Sakit (hospital management system) |
|
||||
| **IHS** | Indonesia Health Services (BPJS) |
|
||||
| **CQRS** | Command Query Responsibility Segregation |
|
||||
| **DDD** | Domain-Driven Design |
|
||||
@@ -0,0 +1,520 @@
|
||||
---
|
||||
title: "service-satusehat — Technical Analysis"
|
||||
subtitle: "Architecture, modules, gaps, and risks"
|
||||
author: "Backend Engineering"
|
||||
date: "2026-05-13"
|
||||
format:
|
||||
html:
|
||||
toc: true
|
||||
toc-depth: 3
|
||||
number-sections: true
|
||||
code-fold: false
|
||||
theme: cosmo
|
||||
embed-resources: true
|
||||
pdf:
|
||||
toc: true
|
||||
number-sections: true
|
||||
colorlinks: true
|
||||
execute:
|
||||
echo: true
|
||||
eval: false
|
||||
---
|
||||
|
||||
# Executive Summary
|
||||
|
||||
`service-satusehat` is a production-track Go microservice that implements an
|
||||
**integration gateway** between a hospital information system (SIMRS) and the
|
||||
Indonesian SATUSEHAT FHIR R4 platform, with secondary integration paths to
|
||||
BPJS, Keycloak, MinIO, and Redis.
|
||||
|
||||
The codebase is built on **Clean Architecture + Domain-Driven Design + CQRS**,
|
||||
serves both **REST (Gin)** and **gRPC** transports, and supports five
|
||||
relational/NoSQL databases. As of 2026-05-13:
|
||||
|
||||
- **19 FHIR resources** are wired end-to-end (Create / Update / Patch / Get / Search).
|
||||
- **7 reference registries** are proxied (Patient, Practitioner, Organization,
|
||||
Location, KFA, KYC, Auth).
|
||||
- The error system, logger, query builder, and Prometheus metrics layer are
|
||||
mature and reusable.
|
||||
- **Critical gaps**: virtually zero test coverage, empty gRPC implementation,
|
||||
no audit-log table, no CI.
|
||||
|
||||
This document presents the technical analysis in depth; the companion files
|
||||
`PRD.md`, `DEVPLAN.md`, and `DEVLOG.md` cover product framing, forward plan,
|
||||
and ongoing change record respectively.
|
||||
|
||||
---
|
||||
|
||||
# Repository Layout
|
||||
|
||||
```{.text}
|
||||
service-satusehat/
|
||||
├── cmd/
|
||||
│ ├── api/main.go # primary entry point (REST + gRPC)
|
||||
│ ├── seeder/main.go # data seeder
|
||||
│ └── monitoring/main.go # standalone monitoring entry
|
||||
├── internal/
|
||||
│ ├── auth/ # authentication module (CQRS)
|
||||
│ ├── master/role/ # RBAC master: master, pages, permission, accses
|
||||
│ ├── infrastructure/
|
||||
│ │ ├── cache/ # Redis + NoOp cache
|
||||
│ │ ├── config/ # YAML + env loader
|
||||
│ │ ├── container/ # DI container
|
||||
│ │ ├── database/ # multi-DB manager + migrations
|
||||
│ │ ├── monitoring/ # health, metrics, middleware
|
||||
│ │ └── transport/ # http (Gin) + grpc
|
||||
│ ├── interfaces/
|
||||
│ │ ├── bpjs/ # BPJS client (HMAC-signed)
|
||||
│ │ ├── minio/ # object storage
|
||||
│ │ └── satusehat/ # SATUSEHAT FHIR client + token cache
|
||||
│ └── satusehat/
|
||||
│ ├── common/ # shared mappers / DTOs
|
||||
│ ├── reference/ # patient, practitioner, organization,
|
||||
│ │ # location, kfa, kyc, auth
|
||||
│ └── usecase/ # 19 FHIR resource modules
|
||||
│ └── <resource>/
|
||||
│ ├── dto.go
|
||||
│ ├── mapper.go
|
||||
│ ├── repository.go
|
||||
│ └── service.go
|
||||
├── pkg/
|
||||
│ ├── crypto/ # RSA helpers
|
||||
│ ├── errors/ # AppError, codes, builder, i18n, grpc/http mapping
|
||||
│ ├── logger/ # logrus-based structured logger
|
||||
│ ├── response/ # standard HTTP envelopes
|
||||
│ └── utils/
|
||||
│ ├── custom/ # time helpers (WIB)
|
||||
│ ├── query/ # multi-dialect SQL + Mongo query builder
|
||||
│ └── validator/ # validator-v10 translator
|
||||
├── docs/ # this folder
|
||||
├── tools/ # generator + generator config
|
||||
├── scripts/ # build / proto / migrate / seed shell scripts
|
||||
├── Dockerfile / Dockerfile.dev
|
||||
├── docker-compose.{dev,prod,monitoring,redis,network}.yml
|
||||
├── Makefile
|
||||
└── config.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
# Architecture
|
||||
|
||||
## Layering
|
||||
|
||||
```{.mermaid}
|
||||
flowchart TB
|
||||
T["Transport Layer<br/>REST (Gin) • gRPC"]
|
||||
A["Application Layer<br/>auth • role • SATUSEHAT usecases"]
|
||||
D["Domain Layer<br/>CQRS: Command + Query repositories"]
|
||||
I["Infrastructure Layer<br/>DB • Cache • SATUSEHAT client • BPJS • MinIO • Logger • Metrics"]
|
||||
T --> A --> D --> I
|
||||
```
|
||||
|
||||
- **Transport** is thin: parse HTTP/gRPC, validate input, call service.
|
||||
- **Application** orchestrates use-cases; one usecase per FHIR resource.
|
||||
- **Domain** is the CQRS boundary — every repository is split into
|
||||
`CommandRepository` (write) and `QueryRepository` (read).
|
||||
- **Infrastructure** owns I/O concerns and exposes interfaces upward.
|
||||
|
||||
## CQRS in practice
|
||||
|
||||
Every domain module wires **two repository implementations** at boot:
|
||||
|
||||
```{.go}
|
||||
authCmdRepo := auth.NewCommandRepository(primaryDB, logger)
|
||||
authQueryRepo := auth.NewQueryRepository(primaryDB, logger)
|
||||
authService := auth.NewService(authCmdRepo, authQueryRepo, cacheManager, cfg)
|
||||
```
|
||||
|
||||
This is wired uniformly for `auth`, `role/master`, `role/pages`,
|
||||
`role/permission`. The pattern lets reads route to read replicas while writes
|
||||
target the primary — see `database.GetReadDB()` for the round-robin selector.
|
||||
|
||||
## SATUSEHAT integration flow
|
||||
|
||||
```{.mermaid}
|
||||
sequenceDiagram
|
||||
participant Caller
|
||||
participant Service as service-satusehat
|
||||
participant Client as internal/interfaces/satusehat.Client
|
||||
participant SS as SATUSEHAT FHIR
|
||||
|
||||
Caller->>Service: POST /api/v1/encounter
|
||||
Service->>Service: validate DTO
|
||||
Service->>Service: MapRequestToFHIR(req)
|
||||
Service->>Client: DoRequest(ctx, POST, "/Encounter", payload)
|
||||
Client->>Client: GetAccessToken() — cached if valid
|
||||
alt token expired or missing
|
||||
Client->>SS: POST /oauth2/v1/accesstoken
|
||||
SS-->>Client: access_token (≈1h)
|
||||
end
|
||||
Client->>SS: POST /Encounter (Bearer ...)
|
||||
SS-->>Client: FHIR response
|
||||
Client-->>Service: FHIRResponse{ResourceID, Outcome}
|
||||
Service-->>Caller: 200 OK with mapped envelope
|
||||
```
|
||||
|
||||
Key properties:
|
||||
|
||||
- **Token caching** is in-memory, mutex-protected, and uses a 1-minute safety
|
||||
buffer before expiry (`internal/interfaces/satusehat/client.go`).
|
||||
- **Auth URL normalisation** auto-appends `/accesstoken` + query parameters
|
||||
if the configured URL is missing them.
|
||||
- Auxiliary paths (`DoKFA`, `DoKYC`, `DoConsent`) inject the right scoped
|
||||
headers transparently.
|
||||
|
||||
---
|
||||
|
||||
# Module Inventory
|
||||
|
||||
## SATUSEHAT FHIR Usecases
|
||||
|
||||
All 19 modules follow the same four-file template:
|
||||
|
||||
| File | Responsibility |
|
||||
|---------------|----------------|
|
||||
| `dto.go` | Request, Update, Patch DTOs with validator-v10 tags |
|
||||
| `mapper.go` | `MapRequestToFHIR(req) → FHIRPayload` |
|
||||
| `repository.go` | Thin wrapper over `SatuSehatClient.DoRequest` |
|
||||
| `service.go` | Validation + orchestration + Mapper + Repository |
|
||||
|
||||
| # | Resource | Status | Notes |
|
||||
|---|-----------------------|--------|-------|
|
||||
| 1 | AllergyIntolerance | ✓ CRUDS | |
|
||||
| 2 | CarePlan | ✓ CRUDS | |
|
||||
| 3 | ClinicalImpression | ✓ CRUDS | |
|
||||
| 4 | Composition | ✓ CRUDS | |
|
||||
| 5 | Condition | ✓ CRUDS | |
|
||||
| 6 | DiagnosticReport | ✓ CRUDS | |
|
||||
| 7 | Encounter | ✓ CRUDS | most complex mapper |
|
||||
| 8 | EpisodeOfCare | ✓ CRUDS | **active refactor** — see DEVLOG 2026-05-13 |
|
||||
| 9 | ImagingStudy | ✓ CRUDS | linked with Studies |
|
||||
| 10 | Immunization | ✓ CRUDS | |
|
||||
| 11 | Medication | ✓ CRUDS | |
|
||||
| 12 | MedicationDispense | ✓ CRUDS | |
|
||||
| 13 | MedicationRequest | ✓ CRUDS | |
|
||||
| 14 | MedicationStatement | ✓ CRUDS | |
|
||||
| 15 | Observation | ✓ CRUDS | vitals + lab |
|
||||
| 16 | Procedure | ✓ CRUDS | |
|
||||
| 17 | QuestionnaireResponse | ✓ CRUDS | |
|
||||
| 18 | ServiceRequest | ✓ CRUDS | |
|
||||
| 19 | Specimen | ✓ CRUDS | |
|
||||
| | Studies (DICOM) | minimal | repo only, no full service |
|
||||
|
||||
> Legend: **CRUDS** = Create, Update, Patch, Get-by-ID, Search.
|
||||
|
||||
## SATUSEHAT Reference Modules
|
||||
|
||||
| Module | Purpose |
|
||||
|---------------|---------|
|
||||
| `patient` | Search by NIK / name / DOB |
|
||||
| `practitioner`| Search by NIK or NPP |
|
||||
| `organization`| Facility registry |
|
||||
| `location` | Room / bed registry |
|
||||
| `kfa` | Drug & device master |
|
||||
| `kyc` | Identity verification (TODO: persist result locally) |
|
||||
| `auth` | Token introspection / refresh |
|
||||
|
||||
## Application Modules
|
||||
|
||||
| Module | Files | Notes |
|
||||
|--------------------------|-------|-------|
|
||||
| `internal/auth` | 4 | JWT + Keycloak + static + hybrid auth |
|
||||
| `internal/master/role/master` | 5 | Role CRUD |
|
||||
| `internal/master/role/pages` | 5 | Page/menu master |
|
||||
| `internal/master/role/permission` | 5 | Role↔Page↔Action mappings |
|
||||
| `internal/master/role/accses` | 5 | Access list (folder typo) |
|
||||
|
||||
## Infrastructure
|
||||
|
||||
| Component | Maturity | Notes |
|
||||
|------------------|----------|-------|
|
||||
| `database` | High | 5 DB types, read replicas, pool tuning, migrations, LISTEN/NOTIFY |
|
||||
| `cache` | Medium | Redis + NoOp factory; NoOp is not goroutine-safe |
|
||||
| `config` | High | Full env + YAML + validate |
|
||||
| `monitoring` | High | Prometheus + health + repo wrapper |
|
||||
| `transport/http` | High | Gin + handlers + middleware + Swagger |
|
||||
| `transport/grpc` | **Low** | proto/ empty, registry empty, reflection only |
|
||||
| `container` | Medium | Hand-rolled DI |
|
||||
|
||||
## Cross-cutting (`pkg/`)
|
||||
|
||||
| Package | LOC (approx) | Notes |
|
||||
|------------------|--------------|-------|
|
||||
| `errors` | ~2000 | AppError, codes, http+grpc mapping, i18n, recovery, metrics |
|
||||
| `logger` | ~400 | logrus-backed, daily-file output, context aware |
|
||||
| `utils/query` | ~3500 | SQL + Mongo + dialects + filters + injection guard |
|
||||
| `utils/validator`| ~150 | go-playground/validator translator |
|
||||
| `response` | ~100 | Standard `{success, data, error, meta}` envelope |
|
||||
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
## Sources and precedence
|
||||
|
||||
1. **Environment variables** (highest precedence).
|
||||
2. **`config.yaml`** in working directory.
|
||||
3. **Defaults baked into `config.Config`**.
|
||||
|
||||
Loading happens at startup via `config.LoadConfig()` and is followed by
|
||||
`cfg.Validate()` which fails fast on missing required fields per auth /
|
||||
SATUSEHAT type.
|
||||
|
||||
## Key configuration sections
|
||||
|
||||
| Section | Notable keys |
|
||||
|---------------|--------------|
|
||||
| `server.rest` | port, mode, read/write timeout |
|
||||
| `server.grpc` | port, reflection_enabled |
|
||||
| `databases.*` | type, host, credentials, pool, replicas, SSL |
|
||||
| `auth` | type=jwt/static/keycloak/hybrid, keycloak.*, jwt.secret |
|
||||
| `bpjs` | base_url, cons_id, secret_key, user_keys.* |
|
||||
| `satusehat` | org_id, fasyankes_id, client_id, secret, auth_url, base_url, kfa_url, kyc_url, dicom_url |
|
||||
| `cache` | enabled, redis.*, ttl.default/session/rate_limit |
|
||||
| `minio` | endpoint, access/secret, ssl, buckets |
|
||||
| `security` | trusted_origins, max_input_length, rate_limit.requests_per_minute |
|
||||
| `logger` | level, format, output |
|
||||
|
||||
## Secrets handling
|
||||
|
||||
- All secrets sourced from environment (no plaintext in `config.yaml`).
|
||||
- BPJS HMAC signing isolated in `internal/interfaces/bpjs/crypto.go`.
|
||||
- KYC RSA key pair encoded as base64 env (`pkg/crypto/rsa.go`).
|
||||
- Logger does **not** dump credentials at startup.
|
||||
|
||||
---
|
||||
|
||||
# Data Layer
|
||||
|
||||
## Supported databases
|
||||
|
||||
| Engine | Driver(s) | Purpose |
|
||||
|------------|--------------------------|------------------------|
|
||||
| PostgreSQL | `pgx/v5`, `pq`, GORM | Primary, recommended |
|
||||
| MySQL | GORM mysql | Alternate primary |
|
||||
| SQL Server | GORM sqlserver | Legacy SIMRS sources |
|
||||
| SQLite | GORM sqlite | Local dev / tests |
|
||||
| MongoDB | `mongo-go-driver` | Optional document store|
|
||||
|
||||
## Features
|
||||
|
||||
- **Singleton `dbManager`** behind `once.Do` for one-time initialisation.
|
||||
- **Pool tuning** driven by `runtime.NumCPU()` if not configured:
|
||||
`maxOpen = cores*2 + 1`, `maxIdle = maxOpen/2`.
|
||||
- **Read replicas** with round-robin via `GetReadDB(name)`.
|
||||
- **Migrations**: SQL files in `internal/infrastructure/database/sql/`
|
||||
plus GORM AutoMigrate; tracked in a `Migrations` table.
|
||||
- **LISTEN/NOTIFY** support for Postgres (`ListenForChanges`,
|
||||
`NotifyChange`).
|
||||
- **Health** per-database with pool stats in `Health()`.
|
||||
|
||||
## Query builder (`pkg/utils/query`)
|
||||
|
||||
A custom builder that supports:
|
||||
|
||||
- **Dialects**: PostgreSQL, MySQL, SQL Server, SQLite (syntax differences
|
||||
centralised in `dialects.go`).
|
||||
- **Operators**: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`, `in`, `like`,
|
||||
`between`, full-text, date range.
|
||||
- **Logical composition**: `AND`, `OR`, nested groups.
|
||||
- **Pagination**: offset + limit.
|
||||
- **Sorting**: multi-field ASC/DESC.
|
||||
- **Mongo**: aggregation pipeline emission.
|
||||
- **Injection guard**: regex blacklist + parameterised binding
|
||||
(`sql_security.go`).
|
||||
|
||||
> ⚠ Multiple `fmt.Printf` debug calls remain in `sql_builder.go` and
|
||||
> `mongo_execution.go`. These should be replaced with `logger.Debug()`
|
||||
> before production rollout (DEVPLAN M1).
|
||||
|
||||
---
|
||||
|
||||
# Transport
|
||||
|
||||
## REST (Gin)
|
||||
|
||||
- Entry: `internal/infrastructure/transport/http/servers`.
|
||||
- Handlers grouped by domain under `handlers/main/` and `handlers/satusehat/`.
|
||||
- Middleware stack: CORS → recovery → request-id → auth → rate-limit (TBD).
|
||||
- Routes registered via `RegisterRoutes(router, registry)` where `registry`
|
||||
is the `ServiceRegistry` struct populated in `cmd/api/main.go`.
|
||||
- Swagger: `swaggo/gin-swagger`, served at `/swagger/index.html`.
|
||||
|
||||
## gRPC
|
||||
|
||||
- Server bootstraps in `cmd/api/main.go` with optional reflection.
|
||||
- `proto/v1/` directory **exists but contains no `.proto` files**.
|
||||
- No services are currently registered with the gRPC server — clients
|
||||
receive `Unimplemented`. See DEVPLAN M3 for the gRPC catch-up plan.
|
||||
|
||||
---
|
||||
|
||||
# Authentication & Authorization
|
||||
|
||||
## Inbound
|
||||
|
||||
| Mode | Description |
|
||||
|-----------|-------------|
|
||||
| `jwt` | Service signs and validates its own JWTs |
|
||||
| `static` | Bearer-token list from env `AUTH_STATIC_TOKENS` |
|
||||
| `keycloak`| Validate against Keycloak JWKS, audience checked |
|
||||
| `hybrid` | Try Keycloak; fallback to static/JWT |
|
||||
|
||||
Service exposes:
|
||||
|
||||
- `Login(username, password) → access + refresh`
|
||||
- `RefreshToken(refresh) → access + refresh`
|
||||
- `ValidateToken(token) → claims`
|
||||
- `Logout(token) → ok` ← **does not yet persist revocation** (TODO at
|
||||
`internal/auth/service.go:233`)
|
||||
- `GenerateJWT`, `VerifyJWT` for the local-only path.
|
||||
|
||||
## Outbound (SATUSEHAT)
|
||||
|
||||
- OAuth2 client-credentials, single concurrent `GetAccessToken()`,
|
||||
mutex-protected.
|
||||
- 1-minute pre-expiry refresh to avoid token race.
|
||||
- Auxiliary services (`KFA`, `KYC`, `Consent`) reuse the same token where
|
||||
scopes allow.
|
||||
|
||||
---
|
||||
|
||||
# Observability
|
||||
|
||||
| Surface | Detail |
|
||||
|--------------|--------|
|
||||
| **Logging** | `logrus`, JSON or text; daily file rotation under `logs/YYYY/MM/`. Fields: service, env, request_id (TBD), latency, status, error_code |
|
||||
| **Metrics** | Prometheus client; HTTP req count/latency, DB pool stats, cache hit ratio, error count by code |
|
||||
| **Health** | `GET /api/v1/health`, `/health/database`, `/health/external` |
|
||||
| **Tracing** | Not yet — only request-id propagation planned |
|
||||
|
||||
---
|
||||
|
||||
# Build, Packaging, Deployment
|
||||
|
||||
## Build
|
||||
|
||||
- Go 1.25, modules.
|
||||
- Multi-stage Dockerfile: `golang:1.25-alpine` → `gcr.io/distroless/static:nonroot`.
|
||||
- CGO disabled, static binary, runs as `nonroot`.
|
||||
- Exposes `8196` (REST) and `8197` (gRPC, when wired).
|
||||
|
||||
## Compose stacks
|
||||
|
||||
| File | Purpose |
|
||||
|-------------------------------|---------|
|
||||
| `docker-compose.dev.yml` | Hot-reload dev with `Dockerfile.dev` + air |
|
||||
| `docker-compose.prod.yml` | Production single instance, joins external network `service-general_default` |
|
||||
| `docker-compose.monitoring.yml` | Prometheus / Grafana side-car |
|
||||
| `docker-compose.redis.yml` | Stand-alone Redis |
|
||||
| `docker-compose.network.yml` | Creates shared external network |
|
||||
|
||||
## Make targets
|
||||
|
||||
`dev`, `prod`, `logs`, `clean`, `security-check` (gosec), `audit`
|
||||
(govulncheck), `test`, `test-coverage`, `proto`, `proto-all`, `seeder-*`.
|
||||
|
||||
---
|
||||
|
||||
# Security Review (quick pass)
|
||||
|
||||
| Control | State |
|
||||
|------------------------|-------|
|
||||
| Parameterised queries | ✓ enforced by GORM + sqlx + builder |
|
||||
| SQL injection regex guard | ✓ in `pkg/utils/query/sql_security.go` |
|
||||
| CORS | ✓ configurable `trusted_origins` |
|
||||
| Rate limit | ⚠ config exists, middleware missing |
|
||||
| Secrets in env only | ✓ |
|
||||
| TLS for outbound | ✓ via Go default; configurable for DB |
|
||||
| Token revocation | ⚠ logout not persisted |
|
||||
| Audit log of writes | ⚠ no dedicated table |
|
||||
| Dependency scan | ✓ `make audit` (govulncheck) |
|
||||
| SAST | ✓ `make security-check` (gosec) |
|
||||
|
||||
---
|
||||
|
||||
# Testing
|
||||
|
||||
| Path | Tests |
|
||||
|---------------------------------------|-------|
|
||||
| `pkg/utils/query/dialects_test.go` | ✓ |
|
||||
| `pkg/utils/query/sql_builder_test.go` | ✓ |
|
||||
| **Everything else** | ✗ |
|
||||
|
||||
> Test coverage is the single highest risk in the codebase. DEVPLAN M1
|
||||
> prioritises mapper unit tests for every FHIR resource (cheap, high value).
|
||||
|
||||
---
|
||||
|
||||
# Gaps, Risks, and Recommendations
|
||||
|
||||
| ID | Gap | Severity | Recommendation |
|
||||
|-----|--------------------------------------------------------------------|----------|----------------|
|
||||
| R1 | gRPC empty | High | Author proto, generate, register at least the 4 most-used resources |
|
||||
| R2 | Near-zero tests | High | Mapper unit tests first; integration tests behind build tag |
|
||||
| R3 | No rate-limit middleware | Medium | Add Redis-backed token-bucket middleware; key by user/IP |
|
||||
| R4 | Logout does not revoke | Medium | Persist revoked refresh tokens; check on every refresh |
|
||||
| R5 | KYC result not persisted | Medium | Add `kyc_verification` table; write on success |
|
||||
| R6 | Cache invalidation TODOs in role services | Medium | Standardise cache keys + invalidate on every Cmd write |
|
||||
| R7 | `fmt.Printf` debug leaks | Low | Replace with `logger.Debug()` |
|
||||
| R8 | NoOp cache not thread-safe | Medium | Add `sync.RWMutex` or refuse fallback in production |
|
||||
| R9 | Folder name `accses` typo | Low | Rename module after one cycle of approved PRs |
|
||||
| R10 | No SATUSEHAT submission audit table | High | Add `satusehat_submission` table + repository wrapper logging |
|
||||
| R11 | No CI | High | GitHub Actions: lint → test → build → docker → push |
|
||||
| R12 | Commented-out Kafka producer + ImagingStudy worker in `main.go` | Low | Either delete or move behind feature flags |
|
||||
| R13 | Empty `tools/generate.go` | Low | Either remove or finish the code-gen story |
|
||||
|
||||
---
|
||||
|
||||
# Open Questions
|
||||
|
||||
1. **Audit retention** — how long must SATUSEHAT submission logs be kept?
|
||||
(Permenkes 24/2022 mandates ≥ 25 years for medical records — does the
|
||||
integration log count?)
|
||||
2. **Idempotency** — do callers retry safely? Should the service de-duplicate
|
||||
by `(resource, identifier, hash)` before re-submission?
|
||||
3. **Multi-tenant** — is one deployment per hospital, or shared across
|
||||
hospitals with `org_id` partitioning?
|
||||
4. **DR** — Redis is shared external; does the hospital DC have a failover?
|
||||
|
||||
---
|
||||
|
||||
# Appendix A — Active Refactor: `EpisodeOfCare`
|
||||
|
||||
The most recently touched module. Diff summary (uncommitted as of
|
||||
2026-05-13):
|
||||
|
||||
- `mapper.go` — removed `os.Getenv("SATUSEHAT_ORG_ID")` fallback; now relies
|
||||
purely on `req.OrganizationID`. Mapping becomes pure / deterministic.
|
||||
- `repository.go` — removed `hiddenCtx` wrapper; calls
|
||||
`client.DoRequest(ctx, …)` directly.
|
||||
- `service.go` — constructor now takes `orgID string`; injects into
|
||||
`req.OrganizationID` when caller leaves it blank. Both `Create` and
|
||||
`Update` honour the default.
|
||||
|
||||
Why it matters:
|
||||
|
||||
- **Testability**: pure mappers without env reads are trivial to unit-test.
|
||||
- **Multi-tenant readiness**: each service instance can hold its own org
|
||||
identity, paving the way for one binary serving multiple facilities.
|
||||
- **Pattern**: this refactor should be replicated across the other 18 FHIR
|
||||
modules (DEVPLAN M1 task T-04).
|
||||
|
||||
---
|
||||
|
||||
# Appendix B — Conventions for New FHIR Modules
|
||||
|
||||
When adding a new FHIR resource:
|
||||
|
||||
1. **Create folder** `internal/satusehat/usecase/<resource>/`.
|
||||
2. **Define DTOs** in `dto.go` with `validator` tags.
|
||||
3. **Write mapper** in `mapper.go`: pure function `Map…ToFHIR(req)` returning
|
||||
`satusehat.FHIRPayload`. **No env reads, no I/O.**
|
||||
4. **Repository** in `repository.go`: only thin `executeRequest` wrappers for
|
||||
POST / PUT / PATCH / GET / GET search.
|
||||
5. **Service** in `service.go`: validate, inject defaults (e.g. `orgID`),
|
||||
call mapper, call repository.
|
||||
6. **Wire in `cmd/api/main.go`** under the appropriate factory block.
|
||||
7. **Add Swagger** comment block above the handler.
|
||||
8. **Add unit tests** for the mapper at minimum.
|
||||
@@ -0,0 +1,697 @@
|
||||
# Flat JSON Bodies — All SatuSehat Use Cases
|
||||
|
||||
> Flat (non-nested) POST body for **every** FHIR resource currently
|
||||
> exposed by `service-satusehat`.
|
||||
>
|
||||
> **Status:** ✅ **Implemented**. As of 2026-05-13, all 19 FHIR usecases +
|
||||
> EpisodeOfCare + QuestionnaireResponse accept the flat shape below. Internal
|
||||
> mappers compose the nested FHIR R4 payload before submission to SATUSEHAT.
|
||||
> No `*DTO` types from `internal/satusehat/common` are exposed at the API
|
||||
> surface anymore.
|
||||
>
|
||||
> Conventions used in every body below:
|
||||
>
|
||||
> - `*_id` carries the **raw identifier**; the mapper composes
|
||||
> `Patient/{id}`, `Practitioner/{id}`, `Encounter/{id}`, etc.
|
||||
> - `*_name` (or `*_display`) is the human-readable display.
|
||||
> - `*_code` + `*_display` (+ optional `*_system`) replace nested
|
||||
> `CodeableConceptDTO`.
|
||||
> - `*_value` + `*_unit` (+ optional `*_system`, `*_code`) replace nested
|
||||
> `QuantityDTO`.
|
||||
> - Date-times use ISO-8601 `2026-05-13T08:00:00+07:00` (RFC 3339).
|
||||
> Encounter alone accepts the simpler `YYYY-MM-DD HH:MM:SS` form for
|
||||
> `period_start` / `period_end` via the project's `CustomTime` (WIB).
|
||||
> - Status / class / intent values match the validator `oneof=` lists in the
|
||||
> DTOs verbatim.
|
||||
> - `organization_id` is optional — if omitted, the service injects
|
||||
> `cfg.SatuSehat.OrgID` at the boundary.
|
||||
|
||||
---
|
||||
|
||||
## 1. Encounter
|
||||
|
||||
`POST /satusehat/encounter`
|
||||
|
||||
```json
|
||||
{
|
||||
"encounter_id": "ENC-0012345",
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"patient_name": "Budi Santoso",
|
||||
"practitioner_id": "N10000001",
|
||||
"practitioner_name": "Dr. Andi",
|
||||
"location_id": "a6bab5d0-ba3c-4f73-8450-f44d6ca8e9d4",
|
||||
"location_name": "Poli Umum",
|
||||
"status": "arrived",
|
||||
"class": "AMB",
|
||||
"period_start": "2026-05-13 08:00:00",
|
||||
"period_end": "2026-05-13 09:30:00",
|
||||
"diagnosis_condition_id": "C-0001",
|
||||
"diagnosis_use_code": "AD",
|
||||
"diagnosis_use_display": "Admission diagnosis",
|
||||
"diagnosis_rank": 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. EpisodeOfCare
|
||||
|
||||
`POST /satusehat/episodeofcare`
|
||||
|
||||
```json
|
||||
{
|
||||
"episode_of_care_id": "EOC-12345",
|
||||
"organization_id": "10000004",
|
||||
"managing_organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"patient_name": "Budi Santoso",
|
||||
"care_manager_id": "N10000001",
|
||||
"care_manager_name": "Dr. Andi",
|
||||
"status": "active",
|
||||
"type_code": "HACC",
|
||||
"type_display": "Home and Community Care",
|
||||
"period_start": "2026-05-13T08:00:00+07:00",
|
||||
"period_end": "2026-08-13T17:00:00+07:00",
|
||||
"diagnosis_condition_id": "C-0001",
|
||||
"diagnosis_role_code": "CC",
|
||||
"diagnosis_role_display": "Chief complaint",
|
||||
"diagnosis_rank": 1
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Condition
|
||||
|
||||
`POST /satusehat/condition`
|
||||
|
||||
```json
|
||||
{
|
||||
"condition_id": "COND-9001",
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"patient_name": "Budi Santoso",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"clinical_status": "active",
|
||||
"category_code": "encounter-diagnosis",
|
||||
"category_display": "Encounter Diagnosis",
|
||||
"code_system": "http://hl7.org/fhir/sid/icd-10",
|
||||
"code": "J06.9",
|
||||
"code_display": "Acute upper respiratory infection, unspecified",
|
||||
"onset_date_time": "2026-05-13T08:15:00+07:00",
|
||||
"recorded_date": "2026-05-13T08:20:00+07:00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Observation
|
||||
|
||||
`POST /satusehat/observation`
|
||||
|
||||
```json
|
||||
{
|
||||
"observation_id": "OBS-7001",
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"performer_id": "N10000001",
|
||||
"performer_name": "Dr. Andi",
|
||||
"status": "final",
|
||||
"category_code": "vital-signs",
|
||||
"category_display": "Vital Signs",
|
||||
"code_system": "http://loinc.org",
|
||||
"code": "8867-4",
|
||||
"code_display": "Heart rate",
|
||||
"effective_datetime": "2026-05-13T08:10:00+07:00",
|
||||
"issued": "2026-05-13T08:12:00+07:00",
|
||||
"value_quantity_value": 80,
|
||||
"value_quantity_unit": "beats/minute",
|
||||
"value_quantity_system": "http://unitsofmeasure.org",
|
||||
"value_quantity_code": "/min",
|
||||
"body_site_code": "40983000",
|
||||
"body_site_display": "Arm",
|
||||
"interpretation_code": "N",
|
||||
"interpretation_display": "Normal",
|
||||
"reference_range_low_value": 60,
|
||||
"reference_range_high_value": 100,
|
||||
"reference_range_unit": "beats/minute",
|
||||
"reference_range_text": "Normal adult resting heart rate"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. AllergyIntolerance
|
||||
|
||||
`POST /satusehat/allergyintolerance`
|
||||
|
||||
```json
|
||||
{
|
||||
"allergy_id": "ALG-0001",
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"patient_name": "Budi Santoso",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"encounter_display": "Poli Umum 13 Mei 2026",
|
||||
"recorder_id": "N10000001",
|
||||
"recorder_display": "Dr. Andi",
|
||||
"clinical_status": "active",
|
||||
"verification_status": "confirmed",
|
||||
"category": "medication",
|
||||
"code_system": "http://snomed.info/sct",
|
||||
"code": "294505008",
|
||||
"code_display": "Allergy to amoxicillin",
|
||||
"recorded_date": "2026-05-13T08:25:00+07:00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. CarePlan
|
||||
|
||||
`POST /satusehat/careplan`
|
||||
|
||||
```json
|
||||
{
|
||||
"care_plan_id": "CP-0001",
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"patient_display": "Budi Santoso",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"encounter_display": "Poli Umum 13 Mei 2026",
|
||||
"author_id": "N10000001",
|
||||
"author_display": "Dr. Andi",
|
||||
"status": "active",
|
||||
"intent": "plan",
|
||||
"category_code": "assess-plan",
|
||||
"category_display": "Assessment and Plan of Treatment",
|
||||
"title": "Rencana perawatan ISPA",
|
||||
"description": "Antibiotik 5 hari, kontrol H+3",
|
||||
"created_date": "2026-05-13T08:30:00+07:00",
|
||||
"goal_ids": ["GOAL-001", "GOAL-002"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. ClinicalImpression
|
||||
|
||||
`POST /satusehat/clinicalimpression`
|
||||
|
||||
```json
|
||||
{
|
||||
"clinical_impression_id": "CI-0001",
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"patient_display": "Budi Santoso",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"encounter_display": "Poli Umum 13 Mei 2026",
|
||||
"assessor_id": "N10000001",
|
||||
"assessor_display": "Dr. Andi",
|
||||
"status": "completed",
|
||||
"code_system": "http://snomed.info/sct",
|
||||
"code": "162673000",
|
||||
"code_display": "General examination of patient",
|
||||
"description": "Pasien sadar, demam, batuk produktif",
|
||||
"effective_datetime": "2026-05-13T08:35:00+07:00",
|
||||
"date": "2026-05-13T08:40:00+07:00",
|
||||
"summary": "ISPA non-pneumonia",
|
||||
"problem_condition_ids": ["COND-9001"],
|
||||
"finding_code": "386661006",
|
||||
"finding_display": "Fever",
|
||||
"prognosis_code": "170968001",
|
||||
"prognosis_display": "Prognosis good"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Composition
|
||||
|
||||
`POST /satusehat/composition`
|
||||
|
||||
```json
|
||||
{
|
||||
"composition_id": "COMP-0001",
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"patient_display": "Budi Santoso",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"encounter_display": "Poli Umum 13 Mei 2026",
|
||||
"author_id": "N10000001",
|
||||
"author_display": "Dr. Andi",
|
||||
"status": "final",
|
||||
"type_system": "http://loinc.org",
|
||||
"type_code": "11488-4",
|
||||
"type_display": "Consult note",
|
||||
"category_code": "LP173421-1",
|
||||
"category_display": "Report",
|
||||
"title": "Catatan Konsultasi Poli Umum",
|
||||
"date": "2026-05-13T09:00:00+07:00",
|
||||
"section_title": "Anamnesis & Pemeriksaan",
|
||||
"section_code": "55109-3",
|
||||
"section_display": "Reason for visit Narrative",
|
||||
"section_text": "Pasien datang dengan keluhan batuk dan demam selama 3 hari."
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. DiagnosticReport
|
||||
|
||||
`POST /satusehat/diagnosticreport`
|
||||
|
||||
```json
|
||||
{
|
||||
"diagnostic_id": "DR-5001",
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"performer_id": "N10000001",
|
||||
"performer_name": "Dr. Andi",
|
||||
"status": "final",
|
||||
"category_code": "LAB",
|
||||
"category_display": "Laboratory",
|
||||
"code_system": "http://loinc.org",
|
||||
"code": "58410-2",
|
||||
"code_display": "Complete blood count (hemogram) panel",
|
||||
"effective_datetime": "2026-05-13T09:10:00+07:00",
|
||||
"issued": "2026-05-13T09:30:00+07:00",
|
||||
"result_observation_ids": ["OBS-7001", "OBS-7002"],
|
||||
"specimen_ids": ["SPC-3001"],
|
||||
"imaging_study_ids": [],
|
||||
"conclusion_code": "162673000",
|
||||
"conclusion_display": "General examination of patient",
|
||||
"conclusion": "Hasil dalam batas normal"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. ImagingStudy
|
||||
|
||||
`POST /satusehat/imagingstudy`
|
||||
|
||||
```json
|
||||
{
|
||||
"accession_number": "ACC-0001",
|
||||
"organization_id": "10000004",
|
||||
"service_request_id": "SR-0001",
|
||||
"patient_id": "P03647103112",
|
||||
"patient_name": "Budi Santoso",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"practitioner_id": "N10000001",
|
||||
"practitioner_name": "Dr. Andi",
|
||||
"status": "available",
|
||||
"started": "2026-05-13T09:00:00+07:00",
|
||||
"number_of_series": 1,
|
||||
"number_of_instances": 24,
|
||||
"procedure_code": "168731009",
|
||||
"procedure_display": "Chest X-ray",
|
||||
"description": "Thorax PA"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 11. Immunization
|
||||
|
||||
`POST /satusehat/immunization`
|
||||
|
||||
```json
|
||||
{
|
||||
"immunization_id": "IMM-0001",
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"patient_name": "Budi Santoso",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"encounter_display": "Poli Umum 13 Mei 2026",
|
||||
"performer_id": "N10000001",
|
||||
"performer_name": "Dr. Andi",
|
||||
"location_id": "a6bab5d0-ba3c-4f73-8450-f44d6ca8e9d4",
|
||||
"location_name": "Poli Umum",
|
||||
"status": "completed",
|
||||
"vaccine_code_system": "http://sys-ids.kemkes.go.id/vaccine",
|
||||
"vaccine_code": "1010101010",
|
||||
"vaccine_display": "Sinovac",
|
||||
"occurrence_date_time": "2026-05-13T09:05:00+07:00",
|
||||
"primary_source": true,
|
||||
"lot_number": "BATCH-XYZ-2026-001",
|
||||
"dose_quantity_value": 0.5,
|
||||
"dose_quantity_unit": "mL",
|
||||
"dose_quantity_system": "http://unitsofmeasure.org",
|
||||
"dose_quantity_code": "mL",
|
||||
"route_code": "IM",
|
||||
"route_display": "Intramuscular"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Medication
|
||||
|
||||
`POST /satusehat/medication`
|
||||
|
||||
```json
|
||||
{
|
||||
"medication_id": "MED-0001",
|
||||
"organization_id": "10000004",
|
||||
"manufacturer_id": "100099999",
|
||||
"status_code": "active",
|
||||
"kfa_code": "92000001",
|
||||
"kfa_display": "Paracetamol 500 mg tablet",
|
||||
"form_code": "TAB",
|
||||
"form_display": "Tablet",
|
||||
"batch_number": "BTC-XYZ-2026-09",
|
||||
"expiration_date": "2027-12-31T00:00:00+07:00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 13. MedicationDispense
|
||||
|
||||
`POST /satusehat/medicationdispense`
|
||||
|
||||
```json
|
||||
{
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"patient_name": "Budi Santoso",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"practitioner_id": "N10000001",
|
||||
"practitioner_name": "Apt. Sari",
|
||||
"location_id": "a6bab5d0-ba3c-4f73-8450-f44d6ca8e9d4",
|
||||
"location_name": "Apotek Rawat Jalan",
|
||||
"medication_id": "MED-0001",
|
||||
"medication_display": "Paracetamol 500 mg tablet",
|
||||
"medication_request_id": "MR-0001",
|
||||
"prescription_id": "RX-2026-000123",
|
||||
"prescription_item_id": "RX-2026-000123-1",
|
||||
"status": "completed",
|
||||
"category": "outpatient",
|
||||
"prepared_date": "2026-05-13 09:40:00",
|
||||
"handed_over_date": "2026-05-13 09:45:00",
|
||||
"quantity_value": 10,
|
||||
"quantity_unit": "TAB",
|
||||
"days_supply_value": 5,
|
||||
"dosage_text": "1 tablet, 3x sehari sesudah makan",
|
||||
"timing_frequency": 3,
|
||||
"timing_period": 1,
|
||||
"timing_period_unit": "d",
|
||||
"dose_quantity_value": 1,
|
||||
"dose_quantity_unit": "TAB"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 14. MedicationRequest
|
||||
|
||||
`POST /satusehat/medicationrequest`
|
||||
|
||||
```json
|
||||
{
|
||||
"medicationrequest_id": "MR-0001",
|
||||
"prescription_item_id": "RX-2026-000123-1",
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"patient_name": "Budi Santoso",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"practitioner_id": "N10000001",
|
||||
"practitioner_name": "Dr. Andi",
|
||||
"medication_id": "MED-0001",
|
||||
"medication_display": "Paracetamol 500 mg tablet",
|
||||
"category": "outpatient",
|
||||
"priority": "routine",
|
||||
"status": "active",
|
||||
"intent": "order",
|
||||
"authored_on": "2026-05-13T09:35:00+07:00",
|
||||
"reason_code": "J06.9",
|
||||
"reason_display": "Acute upper respiratory infection",
|
||||
"course_of_therapy_code": "acute",
|
||||
"course_of_therapy_display": "Short course (acute) therapy",
|
||||
"dosage_text": "1 tablet, 3x sehari sesudah makan",
|
||||
"additional_instruction": "Habiskan",
|
||||
"patient_instruction": "Minum dengan air putih",
|
||||
"timing_frequency": 3,
|
||||
"timing_period": 1,
|
||||
"timing_period_unit": "d",
|
||||
"route_code": "O",
|
||||
"route_display": "Oral",
|
||||
"dose_quantity_value": 1,
|
||||
"dose_quantity_unit": "TAB",
|
||||
"dispense_interval": 0,
|
||||
"dispense_value": 15,
|
||||
"dispense_unit": "TAB",
|
||||
"supply_duration": 5,
|
||||
"validity_period_start": "2026-05-13T09:35:00+07:00",
|
||||
"validity_period_end": "2026-05-20T23:59:59+07:00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 15. MedicationStatement
|
||||
|
||||
`POST /satusehat/medicationstatement`
|
||||
|
||||
```json
|
||||
{
|
||||
"statement_id": "MS-0001",
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"patient_name": "Budi Santoso",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"information_source_id": "P03647103112",
|
||||
"information_source_name": "Budi Santoso",
|
||||
"status": "active",
|
||||
"category_code": "outpatient",
|
||||
"category_display": "Outpatient",
|
||||
"medication_code_system": "http://sys-ids.kemkes.go.id/kfa",
|
||||
"medication_code": "92000001",
|
||||
"medication_display": "Paracetamol 500 mg tablet",
|
||||
"effective_date_time": "2026-05-13T09:35:00+07:00",
|
||||
"date_asserted": "2026-05-13T09:36:00+07:00",
|
||||
"dosage_text": "1 tablet, 3x sehari sesudah makan",
|
||||
"dosage_patient_instruction": "Minum dengan air putih",
|
||||
"dosage_route_code": "O",
|
||||
"dosage_route_display": "Oral",
|
||||
"dose_quantity_value": 1,
|
||||
"dose_quantity_unit": "TAB"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 16. Procedure
|
||||
|
||||
`POST /satusehat/procedure`
|
||||
|
||||
```json
|
||||
{
|
||||
"procedure_id": "PROC-0001",
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"performer_id": "N10000001",
|
||||
"performer_name": "Dr. Andi",
|
||||
"status": "completed",
|
||||
"category_code": "103693007",
|
||||
"category_display": "Diagnostic procedure",
|
||||
"code_system": "http://hl7.org/fhir/sid/icd-9-cm",
|
||||
"code": "89.61",
|
||||
"code_display": "Continuous blood gas monitoring",
|
||||
"performed_date_time": "2026-05-13T09:15:00+07:00",
|
||||
"performed_start": "2026-05-13T09:15:00+07:00",
|
||||
"performed_end": "2026-05-13T09:25:00+07:00",
|
||||
"reason_code": "J06.9",
|
||||
"reason_display": "Acute upper respiratory infection",
|
||||
"body_site_code": "302551006",
|
||||
"body_site_display": "Entire thorax",
|
||||
"note": "Pasien kooperatif, prosedur selesai tanpa komplikasi"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17. QuestionnaireResponse
|
||||
|
||||
`POST /satusehat/questionnaireresponse`
|
||||
|
||||
```json
|
||||
{
|
||||
"questionnaire_response_id": "QR-0001",
|
||||
"organization_id": "10000004",
|
||||
"questionnaire_url": "https://fhir.kemkes.go.id/Questionnaire/Q0007",
|
||||
"patient_id": "P03647103112",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"author_id": "N10000001",
|
||||
"author_name": "Dr. Andi",
|
||||
"source_id": "P03647103112",
|
||||
"source_name": "Budi Santoso",
|
||||
"status": "completed",
|
||||
"authored": "2026-05-13T09:00:00+07:00",
|
||||
"items": [
|
||||
{
|
||||
"link_id": "1",
|
||||
"text": "Apakah Anda merokok?",
|
||||
"answer_boolean": false
|
||||
},
|
||||
{
|
||||
"link_id": "2",
|
||||
"text": "Berapa suhu tubuh Anda hari ini?",
|
||||
"answer_quantity_value": 38.2,
|
||||
"answer_quantity_unit": "Cel"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> Note: `items` remains an array because QuestionnaireResponse is
|
||||
> intrinsically list-shaped — but each item is itself flat.
|
||||
|
||||
---
|
||||
|
||||
## 18. ServiceRequest
|
||||
|
||||
`POST /satusehat/servicerequest`
|
||||
|
||||
```json
|
||||
{
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"patient_name": "Budi Santoso",
|
||||
"encounter_id": "ENC-0012345",
|
||||
"requester_id": "N10000001",
|
||||
"requester_name": "Dr. Andi",
|
||||
"performer_id": "N10000099",
|
||||
"performer_name": "Lab Pathology Unit",
|
||||
"status": "active",
|
||||
"intent": "order",
|
||||
"code": "58410-2",
|
||||
"display": "Complete blood count panel",
|
||||
"authored_on": "2026-05-13T09:05:00+07:00"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 19. Specimen
|
||||
|
||||
`POST /satusehat/specimen`
|
||||
|
||||
```json
|
||||
{
|
||||
"specimen_id": "SPC-3001",
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"status": "available",
|
||||
"type_system": "http://terminology.hl7.org/CodeSystem/v2-0487",
|
||||
"type_code": "BLD",
|
||||
"type_display": "Whole blood",
|
||||
"received_date_time": "2026-05-13T09:20:00+07:00",
|
||||
"collected_date_time": "2026-05-13T09:10:00+07:00",
|
||||
"collector_id": "N10000001",
|
||||
"collector_name": "Perawat Sari",
|
||||
"collection_quantity_value": 5,
|
||||
"collection_quantity_unit": "mL",
|
||||
"collection_method_code": "129316008",
|
||||
"collection_method_display": "Aspiration - action",
|
||||
"body_site_code": "368208006",
|
||||
"body_site_display": "Left upper arm",
|
||||
"fasting_status_code": "F",
|
||||
"fasting_status_display": "Fasting",
|
||||
"processing_procedure_code": "9718006",
|
||||
"processing_procedure_display": "Centrifugation",
|
||||
"processing_time_datetime": "2026-05-13T09:25:00+07:00",
|
||||
"conditions": ["refrigerated"],
|
||||
"request_service_request_ids": ["SR-0001"]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 20. Studies (DICOM upload)
|
||||
|
||||
The `studies` module today is a thin DICOM tarball uploader and does not
|
||||
take a structured body. The recommended request is `multipart/form-data`:
|
||||
|
||||
```http
|
||||
POST /satusehat/dicom/studies/upload
|
||||
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryXyz
|
||||
|
||||
------WebKitFormBoundaryXyz
|
||||
Content-Disposition: form-data; name="organization_id"
|
||||
|
||||
10000004
|
||||
------WebKitFormBoundaryXyz
|
||||
Content-Disposition: form-data; name="patient_id"
|
||||
|
||||
P03647103112
|
||||
------WebKitFormBoundaryXyz
|
||||
Content-Disposition: form-data; name="accession_number"
|
||||
|
||||
ACC-0001
|
||||
------WebKitFormBoundaryXyz
|
||||
Content-Disposition: form-data; name="file"; filename="study.tar.gz"
|
||||
Content-Type: application/gzip
|
||||
|
||||
<binary DICOM tarball>
|
||||
------WebKitFormBoundaryXyz--
|
||||
```
|
||||
|
||||
If a JSON metadata-only endpoint is added later, the flat shape would be:
|
||||
|
||||
```json
|
||||
{
|
||||
"organization_id": "10000004",
|
||||
"patient_id": "P03647103112",
|
||||
"accession_number": "ACC-0001",
|
||||
"study_instance_uid": "1.2.840.113619.2.5.1762583153.1762583153.1762583153.1",
|
||||
"modality": "CR",
|
||||
"started": "2026-05-13T09:00:00+07:00",
|
||||
"description": "Thorax PA"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Implementation notes
|
||||
|
||||
All 20 usecases now share the same internal pattern:
|
||||
|
||||
1. **`dto.go`** — pure flat struct with `binding:"required,..."` tags only on
|
||||
scalar fields. No `common.ReferenceDTO` / `common.CodeableConceptDTO`
|
||||
imports at the request boundary.
|
||||
2. **`mapper.go`** — `MapRequestToFHIR(req)` builds the nested FHIR R4
|
||||
payload. Mapper is pure: no env reads, no I/O.
|
||||
3. **`repository.go`** — thin wrapper over `SatuSehatClient.DoRequest`. The
|
||||
old `hiddenCtx` wrapper has been removed; `context.Context` flows through
|
||||
unchanged for proper deadline / cancellation propagation.
|
||||
4. **`service.go`** — `NewService(repo, orgID string)` takes the default
|
||||
organisation identifier from `cfg.SatuSehat.OrgID` at boot. When a caller
|
||||
omits `organization_id` in the request body, the service fills it in.
|
||||
|
||||
The full audit trail is in `docs/DEVLOG.md`.
|
||||
|
||||
## Field-shape cheat-sheet (nested → flat)
|
||||
|
||||
| Nested FHIR field | Flat replacement |
|
||||
| --- | --- |
|
||||
| `subject.reference = "Patient/X"` | `patient_id = "X"` (+ optional `patient_name`) |
|
||||
| `encounter.reference = "Encounter/X"` | `encounter_id = "X"` (+ `encounter_display`) |
|
||||
| `performer[].reference = "Practitioner/X"` | `performer_id = "X"` (+ `performer_name`) |
|
||||
| `location[].location.reference = "Location/X"` | `location_id = "X"` (+ `location_name`) |
|
||||
| `managingOrganization.reference = "Organization/X"` | `managing_organization_id = "X"` |
|
||||
| `code.system / .code / .display` | `code_system / code / code_display` |
|
||||
| `category.code / .display` | `category_code / category_display` |
|
||||
| `valueQuantity.value / .unit / .system / .code` | `value_quantity_value / _unit / _system / _code` |
|
||||
| `period.start / .end` | `period_start / period_end` |
|
||||
| `dosage.route.code` | `route_code` / `dosage_route_code` |
|
||||
|
||||
When a list of references exists (e.g. `result_observation_ids`,
|
||||
`specimen_ids`), keep it as a flat array of IDs — the mapper turns them
|
||||
into `["Observation/{id}", …]`.
|
||||
Reference in New Issue
Block a user