521 lines
21 KiB
Plaintext
521 lines
21 KiB
Plaintext
---
|
|
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.
|