update worker

This commit is contained in:
meninjar
2026-07-29 06:38:42 +00:00
parent cc62620719
commit 7137774c32
15 changed files with 1060 additions and 108 deletions

No files matched your search

+34 -6
View File
@@ -1,6 +1,7 @@
# 📚 GoPrint Service General - Project Documentation
## 📋 Deskripsi Proyek
Service-General adalah microservice berbasis Go yang menyediakan REST API dan gRPC untuk manajemen data person/pasien dengan integrasi ke sistem kesehatan Indonesia (BPJS & SatuSehat).
## 🏗️ Technical Architecture
@@ -13,7 +14,9 @@ Project ini mengimplementasikan **Clean Architecture** dengan **Domain-Driven De
- **Infrastructure Layer**: Database, Cache, External services
### Multi-Database Support
Mendukung 5 jenis database sekaligus dengan connection pooling dan read replicas:
- PostgreSQL (Primary)
- MySQL
- SQL Server
@@ -21,6 +24,7 @@ Mendukung 5 jenis database sekaligus dengan connection pooling dan read replicas
- SQLite
## 📁 Project Structure
```text
service-general/
├── cmd/api/main.go # Entry point aplikasi
@@ -50,6 +54,7 @@ service-general/
## 📦 Dependencies
### Core Dependencies
```
github.com/gin-gonic/gin v1.10.1 # REST API Framework
github.com/google/uuid v1.6.0 # UUID Generator
@@ -59,6 +64,7 @@ gorm.io/driver/postgres v1.5.11 # PostgreSQL Driver
```
### Database Drivers
```
gorm.io/driver/mysql v1.6.0 # MySQL Driver
gorm.io/driver/sqlite v1.6.0 # SQLite Driver
@@ -68,6 +74,7 @@ github.com/jmoiron/sqlx v1.4.0 # SQL Extensions
```
### Utilities
```
github.com/go-playground/validator/v10 v10.27.0 # Input Validation
github.com/golang/protobuf v1.5.4 # Protocol Buffers
@@ -79,10 +86,12 @@ google.golang.org/grpc v1.78.0 # gRPC Framework
## 🔗 API Endpoints
### REST API Base
- **Base URL**: `http://localhost:8080/api/v1`
- **Swagger UI**: `http://localhost:8080/swagger/index.html`
### Person Endpoints
- `GET /persons` - List persons with pagination
- `GET /persons/:id` - Get person detail
- `POST /persons` - Create new person
@@ -91,6 +100,7 @@ google.golang.org/grpc v1.78.0 # gRPC Framework
- `GET /persons/search` - Search with filters
### gRPC Services
- **Port**: 50051 (default)
- **Reflection**: Enabled untuk development
- **Proto Files**: Tersedia di `docs/api/`
@@ -98,6 +108,7 @@ google.golang.org/grpc v1.78.0 # gRPC Framework
## ⚙️ Environment Configuration
### Configuration in config.yaml
```yaml
server:
port: 8080
@@ -122,6 +133,7 @@ databases:
## 🗄️ Database Schema Overview
### Person Entity (Core)
```go
type Person struct {
Id int64 // Primary key
@@ -138,6 +150,7 @@ type Person struct {
```
### Related Entities
- **PersonAddress**: Multiple addresses per person
- **PersonContact**: Contact information (email, phone)
- **PersonInsurance**: Insurance company data
@@ -146,6 +159,7 @@ type Person struct {
## 📋 Business Rules & Validation
### Person Validation Rules
```go
Name string `json:"name" binding:"required,min=2"`
BirthRegency_Code string `json:"birth_regency_code" validate:"omitempty,len=6"`
@@ -155,6 +169,7 @@ Village_Code string `json:"village_code" validate:"required,len=10
```
### Key Validation Rules:
- **Name**: Required, minimum 2 characters
- **NIK**: 16 digits (optional)
- **Gender**: L/P only (Laki-laki/Perempuan)
@@ -165,6 +180,7 @@ Village_Code string `json:"village_code" validate:"required,len=10
## 🔗 Integration Points
### BPJS Integration
```yaml
bpjs:
base_url: https://apijkn.bpjs-kesehatan.go.id
@@ -175,6 +191,7 @@ bpjs:
```
### SatuSehat Integration
```yaml
satu_sehat:
org_id: your_orgid
@@ -188,6 +205,7 @@ satu_sehat:
## 🛠️ Development Commands
### Setup & Run
```bash
# Install dependencies
go mod download
@@ -199,6 +217,7 @@ go run cmd/api/main.go
```
### Build & Test
```bash
# Build binary
make build
@@ -218,6 +237,7 @@ go test ./...
```
## 🔐 Security Features
- JWT Authentication dengan multiple providers
- Rate limiting dengan Redis
- CORS protection
@@ -226,6 +246,7 @@ go test ./...
- Audit trail untuk semua perubahan data
## 📊 Performance Features
- Connection pooling untuk database
- Query optimization dengan index
- Caching strategy (Redis)
@@ -233,12 +254,14 @@ go test ./...
- Soft delete untuk data retention
## 🧪 Testing Strategy
- Unit testing untuk business logic
- Integration testing untuk API
- External service mocking
- Load testing capabilities
## 🚀 Deployment Options
- Docker support (dev & prod)
- Environment-based configuration
- Health checks & monitoring
@@ -246,6 +269,7 @@ go test ./...
- Multi-stage build
## 📚 Documentation
- Swagger/OpenAPI documentation
- gRPC service definitions
- Code comments & examples
@@ -253,6 +277,7 @@ go test ./...
- Architecture diagrams
## 🔮 Future Enhancements
- Event sourcing untuk audit trail
- CQRS implementation
- Multi-tenant support
@@ -265,7 +290,8 @@ go test ./...
**Version**: 1.0.0
**Go Version**: $(go version | cut -d' ' -f3)
**Last Updated**: $(date +%Y-%m-%d)
```
````
Sekarang saya juga akan update script context.sh untuk membuatnya lebih lengkap:
@@ -332,11 +358,11 @@ generate_dependencies() {
echo "## 📦 Dependencies"
echo "### Core Dependencies"
grep -E '^[[:space:]]*github.com/' "$PROJECT_DIR/go.mod" | head -10
echo ""
echo "### Database Drivers"
grep -E 'postgres|mysql|mongo|sqlite|sqlserver' "$PROJECT_DIR/go.mod"
echo ""
echo "### Security & Validation"
grep -E 'validator|crypto|jwt|auth' "$PROJECT_DIR/go.mod"
@@ -548,7 +574,7 @@ generate_complete_context() {
generate_testing_strategy >> "$OUTPUT_FILE"
echo "" >> "$OUTPUT_FILE"
generate_deployment_info >> "$OUTPUT_FILE"
# Tambahkan closing section
cat >> "$OUTPUT_FILE" << EOF
@@ -600,7 +626,7 @@ EOF
# Main execution
main() {
cd "$PROJECT_DIR"
case "${1:-}" in
--help|-h)
echo "Usage: $0 [options]"
@@ -644,4 +670,6 @@ main() {
# Run main function
main "$@"
echo "✨ Context generation completed!"
```
````
docker cp worker-satusehat-prod:app/internal/worker/satusehat/medicationrequest/mapperdata/2026-05/ ~/goprint/worker-satusehat
+14 -8
View File
@@ -1,5 +1,5 @@
# Multi-stage build yang lebih optimal
FROM golang:1.22-alpine AS build
FROM golang:1.25-alpine AS build
# Install build dependencies
RUN apk add --no-cache git ca-certificates tzdata
@@ -19,8 +19,14 @@ RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
-ldflags='-w -s -extldflags "-static"' \
-o main cmd/api/main.go
# Final stage - distroless untuk keamanan
FROM gcr.io/distroless/static:nonroot
# Final stage - menggunakan tag :debug-nonroot agar memiliki akses shell (BusyBox)
FROM gcr.io/distroless/static:debug-nonroot
WORKDIR /app
ENV TZ="Asia/Jakarta"
# Tambahkan /busybox ke dalam PATH agar command 'sh' bisa langsung dijalankan
ENV PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/busybox"
# Copy timezone data
COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
@@ -28,13 +34,13 @@ COPY --from=build /usr/share/zoneinfo /usr/share/zoneinfo
# Copy binary
COPY --from=build /build/main /app/main
# Pastikan folder logs dan data ada agar aplikasi tidak error saat menulis (opsional tapi disarankan)
# COPY --from=build --chown=nonroot:nonroot /build/logs /app/logs
# COPY --from=build --chown=nonroot:nonroot /build/data /app/data
# Use non-root user
USER nonroot:nonroot
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD ["/app/main", "-health"] || exit 1
EXPOSE 8080
EXPOSE 8198
ENTRYPOINT ["/app/main"]
+1
View File
@@ -3,6 +3,7 @@ FROM golang:1.25-alpine AS dev
WORKDIR /app
ENV TZ="Asia/Jakarta"
# Install air untuk hot reload
RUN go install github.com/cosmtrek/[email protected]
+6 -3
View File
@@ -230,9 +230,12 @@ func main() {
// --- [NEW] Start Background Workers ---
if cfg.SatuSehat.Enabled {
// Inisialisasi Worker Manager yang akan mengelola semua background workers.
// satusehatClient bisa di-pass jika diperlukan oleh worker lain.
var satusehatClient satusehat.SatuSehatClient // Ganti nil dengan inisialisasi client jika sudah ada
satusehatClient := satusehat.NewSatuSehatFactory(cfg.SatuSehat).Client()
logger.Default().Info("SatuSehat client initialized",
logger.String("base_url", cfg.SatuSehat.BaseURL),
logger.String("auth_url", cfg.SatuSehat.AuthURL),
)
workerManager := worker.NewManager(cfg, dbService, satusehatClient)
g.Go(func() error {
+2 -2
View File
@@ -16,14 +16,14 @@ networks:
services:
app-dev:
container_name: worker-satusehat
container_name: worker-satusehat-dev
build:
context: .
dockerfile: Dockerfile.dev
env_file:
- ./.env
environment:
- CONFIG_PATH=/app/config.yaml
# - CONFIG_PATH=/app/config.yaml
- REDIS_HOST=shared-redis # Redis hostname di dalam network
- REDIS_PORT=6379
- REDIS_PASSWORD=
+30 -17
View File
@@ -1,27 +1,40 @@
version: "3.8"
# Network akan dibuat otomatis dengan nama 'service-general_default'
# atau gunakan network yang sudah ada
networks:
# simrs-network:
# driver: bridge
# Connect to the same network as service-general Redis
general-app-network:
external: true
name: service-general_default
# Shared network for cross-container communication
# shared-simrs-network:
# external: true
# name: shared-simrs-network
services:
app:
container_name: service-general
app-prod:
container_name: worker-satusehat-prod
build:
context: .
dockerfile: Dockerfile
restart: unless-stopped
ports:
- "8094:8080"
env_file:
- ./.env
depends_on:
- redis
redis:
image: redis:7-alpine
container_name: general-redis
environment:
# - CONFIG_PATH=/app/config.yaml
- REDIS_HOST=shared-redis # Redis hostname di dalam network
- REDIS_PORT=6379
- REDIS_PASSWORD=
ports:
- "6379:6379"
command: redis-server --requirepass ${REDIS_PASSWORD}
- "8198:8198"
- "8199:8199"
volumes:
- redis_data:/data
volumes:
redis_data:
- /app/tmp
# depends_on:
# - redis # Redis is managed separately
networks:
- general-app-network
# - shared-simrs-network
restart: unless-stopped
+176
View File
@@ -0,0 +1,176 @@
# 📅 Development Log (Catatan Status Integrasi)
**Project:** GoPrint Service General (`worker-satusehat`)
**Update Terakhir:** 2026-05-13
Dokumen ini melacak **status nyata** tiap modul/worker terhadap kode di branch `main`. Status diverifikasi dengan membaca file sumber, bukan sekadar tujuan; tanggal kolom *Last Touch* mengacu pada commit Git terkait.
---
## Legenda Status
-**Production / Aktif** — kode lengkap dan ter-wire di runtime.
- 🟢 **Ready to Activate** — implementasi lengkap, tinggal di-*uncomment* di `worker.go` / `main.go`.
- 🟡 **WIP / Parsial** — kerangka ada tapi logika belum diisi.
-**Backlog** — belum disentuh.
-**Bermasalah / Bug Diketahui** — perlu perbaikan sebelum dipakai.
---
## 1. Fondasi Infrastruktur
| Modul | Lokasi | Status | Catatan |
| :--- | :--- | :--- | :--- |
| Database Manager (multi-DB + CQRS) | `internal/infrastructure/database/` | ✅ | Postgres/MySQL/MSSQL/SQLite/Mongo; auto-tune connection pool |
| Cache Manager (Factory: Redis / NoOp) | `internal/infrastructure/cache/` | ✅ | Cache-aside; SHA-256 key hashing untuk permission |
| Config Loader (Viper + env overlay) | `internal/infrastructure/config/` | ✅ | YAML + ENV (`DATABASES_POSTGRES_HOST` dsb) |
| HTTP Server (Gin + ServiceRegistry) | `internal/infrastructure/transport/http/servers/` | ✅ | Port 8080, Swagger UI aktif |
| gRPC Server | `internal/infrastructure/transport/grpc/servers/` | 🟡 | Server berjalan, namun **`PermissionHandler` masih dikomentari** di [main.go:198-200](cmd/api/main.go#L198-L200) |
| MinIO Object Storage | `internal/interfaces/minio/` | ✅ | `minio.Connect()` dipanggil saat startup |
| Logger (Logrus + WIB hook) | `pkg/logger/logger.go` | ✅ | JSON, daily rotation, caller info |
| Error Package | `pkg/errors/` | ✅ | AppError + HTTP/gRPC mapping + i18n + middleware recovery |
| Query Builder | `pkg/utils/query/` | ✅ | Whitelist kolom, multi-dialect, ada unit test |
| Prometheus Metrics | (import only) | 🟡 | Library terpasang, endpoint `/metrics` belum di-wire ke routes |
---
## 2. Bridging Eksternal
### 2.1 SatuSehat Client
| Item | Status | Catatan |
| :--- | :--- | :--- |
| OAuth2 Client-Credentials + cache token | ✅ | `internal/interfaces/satusehat/client.go` |
| Auto-retry pada 401 (refresh + repeat) | ✅ | Mutex aman concurrent |
| `FHIRPayload` builder (fluent) | ✅ | `types.go` |
| `DoRequest` (FHIR R4) / `DoKFA` / `DoConsent` | ✅ | Tiga endpoint dengan base URL berbeda |
| Inisialisasi di `main.go` | ❌ | **Bug**: di-pass sebagai `nil` ke `worker.NewManager` di [main.go:235](cmd/api/main.go#L235) |
### 2.2 BPJS Client
| Layanan | Status | Catatan |
| :--- | :--- | :--- |
| Factory (`NewBPJSFactory`) | ✅ | `internal/interfaces/bpjs/factory.go` |
| VClaim / AntreanRS / AntreanFKTP / Aplicare / Apotek / PCare / ICare / ERekamMedis | ✅ | 8 service siap pakai |
| HMAC Signature (`SetHeader`) | ✅ | X-cons-id, X-timestamp, X-signature otomatis |
| Dekripsi AES-256-CBC + PKCS7 unpad | ✅ | `crypto.go` |
| Decompression cascade (LZString → Gzip → JSON) | ✅ | `decompression.go` |
| Integrasi sebagai worker / consumer aktif | ⚪ | Belum ada use case yang menggunakan; SDK saja |
---
## 3. Master Data
| Modul | Status | Catatan |
| :--- | :--- | :--- |
| **KFA Master Puller (Worker)** | ✅ | **SATU-SATUNYA worker yang aktif** di runtime; rate-limit handler komplit; UPSERT 3 tabel (product, ingredient, packaging) |
| KFA Repository (UPSERT Bundle transaksional) | ✅ | `internal/master/kfa/repository.go` |
| KFA Mapper (DTO → Entity, 58 kolom product) | ✅ | `internal/master/kfa/mapper.go` |
| KFA Tracker | 🟡 | Berbasis file `internal/master/kfa/kfa_tracker.txt` — perlu migrasi ke DB |
---
## 4. RBAC (Master Role)
| Sub-Modul | CQRS Command | CQRS Query | Service | Mapper | Status |
| :--- | :---: | :---: | :---: | :---: | :--- |
| `role/master` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `role/pages` | ✅ | ✅ | ✅ | ✅ | ✅ |
| `role/permission` (tree + cache SHA-256) | ✅ | ✅ | ✅ | ✅ | ✅ |
| `role/accses` (agregasi GetRoleAccess) | ✅ | ✅ | ✅ | ✅ | ✅ |
| `role/component` | — | — | — | — | ⚪ Dikomentari di [main.go:22, 149-151](cmd/api/main.go#L149-L151) |
---
## 5. Auth
| Item | Status | Catatan |
| :--- | :--- | :--- |
| Provider pluggable (JWT/Keycloak/Static/Hybrid) | ✅ | Config `auth.type`, `auth.fallback_to` |
| Service + CQRS Repository | ✅ | `internal/auth/` |
| Login worker (internal `/auth/login`, `/auth/refresh`) | ✅ | `internal/worker/auth.go` — retry 6× pada startup |
| Kredensial worker login (`[email protected]` / `password123`) | ❌ | **Hardcoded** di [worker/auth.go:25-28](internal/worker/auth.go#L25-L28) — wajib pindah ke env |
---
## 6. Worker SatuSehat
### 6.1 Worker Aktif (terpasang di `worker.Manager.jobs`)
| Worker | Status | Catatan |
| :--- | :--- | :--- |
| **KFA Master Puller** | ✅ | Delay 5s, page size 100 |
### 6.2 Worker Ready-to-Activate (logika lengkap, dikomentari di `worker.go`)
| Worker | LOC `service.go` | Tracker | Mapping Harian | Status |
| :--- | :---: | :---: | :---: | :--- |
| `Medication` (Master) | 360 | `last_medication_id.txt` | ✅ | 🟢 |
| `MedicationRequest` | 366 | `last_medicationrequest_id.txt` | ✅ | 🟢 |
| `MedicationDispense` | 474 | `last_medicationdispense_id.txt` | ✅ | 🟢 |
| `ServiceRequest` (Default + Radiology) | 354 | `last_servicerequest_rad_time.txt` | — | 🟢 |
| `ImagingStudy` | 338 | `last_imagingstudy_id.txt` | ✅ | 🟢 |
| `Encounter` | 215 | (internal) | — | 🟢 |
### 6.3 Worker Stub (kerangka 93117 LOC, belum ada logika sinkron)
🟡 Implementasi minimum: `Config`, `Repository`, `Run()` skeleton — tidak ada tracker, KFA fetch, atau rate-limit handler.
| Worker | LOC | Worker | LOC |
| :--- | :---: | :--- | :---: |
| `AllergyIntolerance` | 93 | `Immunization` | 93 |
| `CarePlan` | 93 | `MedicationStatement` | 93 |
| `ClinicalImpression` | 93 | `Procedure` | 93 |
| `Composition` | 93 | `QuestionnaireResponse` | 93 |
| `Condition` | 117 | `Specimen` | 93 |
| `DiagnosticReport` | 93 | `Observation` | 117 |
| `EpisodeOfCare` | 93 | | |
---
## 7. Worker Lain
| Worker | Status | Catatan |
| :--- | :--- | :--- |
| `patient/migrator.go` (sinkron pasien dari SIMRS v4 legacy) | ❌ | Kode ada (~100+ LOC), **tidak dipanggil dari `main.go`/`worker.go`**; password default `1234` hardcoded |
---
## 8. Riwayat Commit Singkat
| Commit | Tanggal Indikatif | Ringkasan |
| :--- | :--- | :--- |
| `cc62620` | 2026-05 | Update worker medication, servicerequest, imagingstudy, medicationrequest, medicationdispense |
| `e8a0642` | 2026-04/05 | Pengambilan data KFA + pengiriman medication & service request |
| `24d25aa` | 2026-04 | Medication update |
| `9a15d74` | 2026-04 | Update worker SatuSehat |
| `35c1017` | 2026-04 | First commit (fondasi service-general) |
---
## 9. Pekerjaan Sedang Berjalan (WIP)
- [ ] Mengisi inisialisasi `satusehatClient` di `main.go` (saat ini `nil`).
- [ ] Mengaktifkan kembali worker `Medication` & turunannya setelah validasi end-to-end di staging.
- [ ] Migrasi tracker dari `.txt` ke tabel `sync_tracker` atau Redis hash (multi-replica readiness).
- [ ] Endpoint `/metrics` (Prometheus) di-wire ke HTTP router.
- [ ] gRPC `PermissionHandler` di-uncomment & di-test.
---
## 10. Bug & Hutang Teknis Diketahui
| ID | Lokasi | Deskripsi | Severity |
| :---: | :--- | :--- | :--- |
| BUG-001 | [main.go:235](cmd/api/main.go#L235) | `satusehatClient` di-pass `nil` ke Worker Manager | High |
| BUG-002 | [worker/auth.go:25-28](internal/worker/auth.go#L25-L28) | Kredensial login worker hardcoded | High |
| BUG-003 | [patient/migrator.go:23-26](internal/worker/patient/migrator.go#L23-L26) | Username/password default hardcoded jika worker diaktifkan | High |
| DEBT-001 | Semua worker | Tracker `.txt` lokal → tidak multi-pod safe | High |
| DEBT-002 | `internal/worker/satusehat/imagingstudy/service.go` & `medication/service.go` | Fungsi `truncate()` duplikat | Low |
| DEBT-003 | `main.go:198-200` | `permissionHandler` gRPC dikomentari, gRPC server tanpa handler aktif | Medium |
| DEBT-004 | `cmd/api/main.go:111-113` | `kafkaProducer` masih placeholder komentar — event-driven pipeline belum jalan | Medium |
| DEBT-005 | `internal/worker/satusehat/<13 resource>/` | Kerangka stub belum diisi | Medium |
---
*Disusun ulang: Tim Engineering GoPrint, 2026-05-13.*
+250
View File
@@ -0,0 +1,250 @@
# 🗺️ Development Plan (Roadmap)
**Project:** GoPrint Service General (`worker-satusehat`)
**Versi:** 1.0
**Tanggal:** 2026-05-13
Peta jalan terurut berdasarkan **dampak vs effort**. Setiap fase punya keluaran yang bisa dirilis berdiri sendiri.
---
## Phase 0 — Stabilisasi Production Saat Ini *(12 minggu)*
**Tujuan:** Service yang sekarang sudah jalan (REST + RBAC + KFA Puller) bebas bug kritikal.
### Langkah Stabilisasi
1. **Fix `satusehatClient = nil`** ([main.go:235](cmd/api/main.go#L235))
- Inisialisasi: `satusehatClient := satusehat.NewClient(cfg)` lalu pass ke `worker.NewManager`.
- *Acceptance:* `go run` tidak panic ketika `cfg.SatuSehat.Enabled = true`.
2. **Pindahkan kredensial worker login ke env**
- `WORKER_AUTH_EMAIL`, `WORKER_AUTH_PASSWORD` di `.env` + validation di `config.Validate()`.
- Hapus default hardcoded di [worker/auth.go:25-28](internal/worker/auth.go#L25-L28).
3. **Audit & bersihkan kode mati**
- Hapus `patient/migrator.go` jika tidak akan dipakai, atau pindahkan ke folder `archive/` + buang default password.
- Hilangkan `truncate()` duplikat: pindah ke `pkg/logger` atau `pkg/utils`.
4. **Endpoint `/metrics` Prometheus aktif**
- Tambahkan handler `promhttp.Handler()` di `routes.go`.
- Definisikan counter dasar: `worker_documents_processed_total`, `worker_rate_limit_hits_total`, `satusehat_token_refresh_total`.
5. **gRPC handler `PermissionHandler` di-uncomment**
- Tes via `grpcurl` ke `:9090`.
**Output:** Tag `v0.9.0-stable`.
---
## Phase 1 — Aktivasi Worker Klinis *(24 minggu)*
**Tujuan:** Sinkronisasi data obat & radiologi ke SatuSehat jalan otomatis 24/7.
### Langkah Aktivasi
1. **Aktifkan worker secara bertahap di [worker.go](internal/worker/worker.go)** dengan urutan dependensi:
1. `Encounter` (prasyarat referensi)
2. `Medication` (master kode obat lokal → KFA)
3. `MedicationRequest`
4. `MedicationDispense`
5. `ServiceRequest (Radiology)`
6. `ImagingStudy`
2. **Validasi end-to-end per worker:**
- Cek `sync_log` (status SUCCESS/FAILED/RATE_LIMITED).
- Pastikan FHIR ID kembali & tersimpan di mapping harian.
- Monitor log error rate ≤ 1% per jam selama 48 jam soak test.
3. **Rate-limit config dinamis** — pindahkan konstanta `rateLimitSleep` per worker ke `config.yaml`:
```yaml
satu_sehat:
worker:
medication: { rate_limit_sleep: 60s, poll_interval: 2s }
imagingstudy: { rate_limit_sleep: 60s, poll_interval: 2s }
```
4. **Sync Log Retention Policy**
- Tabel `sync_log` bisa membengkak — tambahkan TTL/archive job (retain 90 hari, archive ke S3/MinIO).
**Output:** Tag `v1.0.0` — release bridging klinis pertama.
---
## Phase 2 — Stateless Worker (Multi-Replica Ready) *(23 minggu)*
**Tujuan:** Mendukung deployment Kubernetes / multi-pod tanpa risiko race tracker.
### Langkah Refactor State
1. **Tabel `sync_tracker`**
```sql
CREATE TABLE sync_tracker (
module_name VARCHAR(64) PRIMARY KEY,
last_id BIGINT,
last_timestamp TIMESTAMP,
updated_at TIMESTAMP DEFAULT NOW()
);
```
- Implementasi via row-lock `SELECT ... FOR UPDATE SKIP LOCKED` (Postgres) sehingga hanya satu replica per modul.
2. **Refactor helper `readLastID()` / `writeTracker()`**
- Bikin abstraksi `TrackerStore` di `pkg/tracker/` (Interface: `Get`, `Set` per module name).
- Implementasi backend `FileTracker` (lama) & `DBTracker` (baru). Pilih via env `TRACKER_BACKEND=db|file`.
3. **Migrasi data tracker yang ada**
- Script one-shot: baca semua `*.txt` di `internal/worker/satusehat/*/last_*` & `internal/master/kfa/kfa_tracker.txt` → seed ke `sync_tracker`.
4. **Mapping FHIR ID → Redis hash atau tabel**
- `mapperdata/YYYY-MM/YYYY-MM-DD.txt` tidak cocok multi-pod. Pindah ke tabel `fhir_id_map(local_id, fhir_id, resource_type, created_at)`.
5. **Leader election sederhana untuk worker eksklusif**
- Gunakan Redis `SET key NX EX 60` sebagai *lease* untuk worker singleton (KFA Puller, dst).
**Output:** Tag `v1.1.0` — deployable di Kubernetes dengan ≥ 2 replica.
---
## Phase 3 — Event-Driven Architecture *(46 minggu)*
**Tujuan:** Mengurangi polling DB → reactive berdasarkan trigger SIMRS.
### Langkah Event-Driven
1. **PostgreSQL LISTEN/NOTIFY**
- Trigger pada tabel sumber (mis. `t_pesan_obat` AFTER INSERT) → `NOTIFY pesan_obat_new, NEW.id::text`.
- Worker `Medication` subscribe via `pq.NewListener` daripada polling per 2 detik.
- Fallback: tetap ada polling tiap N detik sebagai *catch-up* untuk event yang tertinggal.
2. **Kafka Producer (opsional, untuk multi-microservice)**
- Uncomment [main.go:111-113](cmd/api/main.go#L111-L113).
- Publish event `fhir.resource.synced` setelah pengiriman sukses untuk konsumsi service lain (audit, BI, dashboard).
- Library: `segmentio/kafka-go` (lightweight).
3. **Dead Letter Queue**
- Pesan yang gagal kirim 3× → masuk topik `fhir.dlq` untuk inspeksi manual.
4. **Webhook callback dari `service-satusehat`**
- Jika service downstream support webhook untuk async FHIR commit, langganan untuk update `sync_log.status` lebih cepat.
**Output:** Tag `v1.2.0`.
---
## Phase 4 — Worker Coverage Lengkap *(rolling, 1 resource / sprint)*
**Tujuan:** Mengisi 13 worker stub menjadi production-ready.
### Urutan Prioritas Worker (berdasarkan kebutuhan pelaporan SatuSehat)
1. `Condition` (diagnosa)
2. `Observation` (vital sign, lab)
3. `Procedure` (tindakan medis)
4. `DiagnosticReport` (laporan lab/radiologi)
5. `AllergyIntolerance`
6. `Immunization`
7. `Specimen` (lab)
8. `ClinicalImpression`
9. `EpisodeOfCare`
10. `CarePlan`
11. `Composition`
12. `MedicationStatement`
13. `QuestionnaireResponse`
### Template per Worker
- Salin pola dari worker yang sudah lengkap (saran: `Medication` untuk transactional, `ServiceRequest` untuk time-based).
- Komponen wajib: tracker, rate-limit handler, KFA/master fetch jika perlu, token refresh, sync log, mapping persistence.
**Output:** Tiap worker = sprint sendiri, release patch (`v1.x.y`).
---
## Phase 5 — Observability & Operasional *(23 minggu, paralel)*
### Langkah Observability
1. **Grafana Dashboard**
- Source: Prometheus `worker_*_total`, `worker_processing_duration_seconds`, `satusehat_response_status_total{code=...}`.
- Panel: throughput per worker, error rate, p95 latency, queue depth (lag dari tracker terhadap MAX(id) source).
2. **Alerting**
- Worker tidak progress > 10 menit.
- Rate-limit hits > 10/min selama 5 menit.
- Token refresh failed > 3 berturut-turut.
3. **Distributed Tracing (opsional)**
- Integrasi OpenTelemetry, trace span dari REST handler → service → repository → API gov't.
4. **Health & Readiness Probe**
- `/health/live`: cek service hidup.
- `/health/ready`: cek DB ping, Redis ping, MinIO ping, last token refresh sukses.
**Output:** Stack monitoring siap pakai (Prometheus + Grafana + Alertmanager).
---
## Phase 6 — UI Dashboard *(repository terpisah, ditangani tim frontend)*
**Tujuan:** Operasional team & admin RS bisa monitor + intervensi tanpa SSH.
### Fitur Inti Dashboard
1. **Sync Monitor**
- Tabel `sync_log` realtime, filter by status, worker, tanggal.
- Re-queue dokumen gagal (button "Retry").
2. **Tracker Cockpit**
- Posisi setiap worker (last_id, last_timestamp) vs. MAX di tabel sumber → lag visual.
- Pause / Resume worker via toggle (butuh control-plane API baru: `PUT /api/v1/worker/{name}/state`).
3. **RBAC Management**
- Hirarki halaman dengan checkbox CRUD level per role (konsumsi `GetRolePermissionTree`).
- Cache invalidation otomatis lewat REST API existing.
4. **KFA Browser**
- Search produk farmasi yang sudah masuk ke DB lokal.
- Trigger ulang puller dari halaman tertentu.
5. **BPJS Console (opsional)**
- Dropdown service VClaim/Antrol → form input → call SDK → tampilkan hasil dekripsi.
---
## Phase 7 — Hardening & Compliance *(jangka panjang)*
- **Audit Trail** lengkap untuk RBAC: siapa, kapan, mengubah apa.
- **Field-Level Encryption** untuk data PII (NIK, alamat) yang tersimpan di sync log.
- **SLA Report** untuk Kemenkes: persentase data terkirim dalam 1×24 jam.
- **Disaster Recovery** runbook: replay dari sync log, re-sync dari ID tertentu.
---
## Ringkasan Timeline Indikatif
| Phase | Durasi | Target Tag | Outcome |
| :--- | :--- | :--- | :--- |
| 0 — Stabilisasi | 12 minggu | `v0.9.0` | Bug kritikal fixed, metrics live |
| 1 — Aktivasi Klinis | 24 minggu | `v1.0.0` | Worker obat & radiologi production |
| 2 — Stateless Tracker | 23 minggu | `v1.1.0` | Multi-replica safe |
| 3 — Event-Driven | 46 minggu | `v1.2.0` | LISTEN/NOTIFY + Kafka |
| 4 — Worker Lengkap | rolling | `v1.x.y` | 13 resource FHIR aktif |
| 5 — Observability | 23 minggu | (paralel) | Grafana + alerting |
| 6 — UI Dashboard | terpisah | (FE repo) | Operator self-service |
| 7 — Hardening | berkelanjutan | — | Compliance & DR |
---
## Quick Wins Bulan Ini
1. **Fix BUG-001** (satusehatClient nil) — 1 jam kerja, dampak besar.
2. **Wire `/metrics`** — 30 menit, langsung kelihatan progress di Prometheus.
3. **Uncomment Worker `Medication`** + soak test 24 jam staging.
4. **Pindahkan password worker ke env** — 1 jam, tutup security finding.
---
*Disusun ulang: Tim Engineering GoPrint, 2026-05-13.*
+169
View File
@@ -0,0 +1,169 @@
# 📑 Product Requirements Document (PRD)
**Nama Produk:** GoPrint Service General — *SatuSehat Bridging & SIMRS Master Gateway*
**Repository:** `worker-satusehat`
**Versi Dokumen:** 1.0
**Tanggal:** 2026-05-13
**Status:** `DRAFT` (sinkron dengan kondisi kode terkini)
---
## 1. Latar Belakang
Fasilitas kesehatan (faskes) di Indonesia wajib melaporkan transaksi pelayanan medis ke platform nasional **SatuSehat** (Kemenkes RI) dalam format FHIR R4, dan tetap terhubung dengan layanan **BPJS Kesehatan** (VClaim / Antrol / Apotek / IHS) untuk klaim & antrean. Saat ini Sistem Informasi Manajemen Rumah Sakit (SIMRS) lokal tidak dirancang untuk menangani latensi, autentikasi (OAuth2 + HMAC + AES), dan rate limit dari API pemerintah secara langsung.
**Visi:** GoPrint Service General menjadi *bridging gateway* tunggal yang:
1. Memisahkan beban integrasi eksternal dari SIMRS inti.
2. Menjamin pengiriman data ke SatuSehat tahan terhadap *rate limit* dan kegagalan token.
3. Menyediakan abstraksi REST/gRPC sederhana bagi tim aplikasi internal.
4. Menampung modul administrasi (RBAC hierarkis, master data) yang dipakai bersama oleh seluruh aplikasi rumah sakit.
---
## 2. Pengguna & Persona
| Persona | Interaksi | Kebutuhan Utama |
| :--- | :--- | :--- |
| **SIMRS Backend / Aplikasi Klinis** | Konsumsi REST `/api/v1/...` & gRPC `:9090` | Latensi rendah, abstraksi FHIR, tidak menangani token SatuSehat sendiri |
| **Worker (Sub-system internal)** | Polling DB SIMRS → POST ke API internal `service-satusehat` | Pengiriman asinkron, recovery 401/429, tracker per resource |
| **Admin Faskes (IT)** | Web UI (eksternal) memanggil REST RBAC | CRUD role/pages/permission hirarkis, cache invalidasi otomatis |
| **DevOps / Operator** | Dashboard log + metrik | Log JSON terstruktur (WIB), Prometheus, graceful shutdown |
| **Auditor Kemenkes** | Tidak langsung — via histori sync | Jejak audit per dokumen (request, response, FHIR ID) |
---
## 3. Ruang Lingkup Fitur (Scope)
### 3.1. Modul Integrasi Eksternal
#### A. SatuSehat (Kemenkes)
- **Klien resmi `satusehat.SatuSehatClient`** ([client.go](internal/interfaces/satusehat/client.go)):
- OAuth2 *client-credentials* dengan caching token + safety margin 60 detik.
- Auto-retry pada HTTP 401 (refresh token, request ulang).
- Tiga endpoint terpisah: `DoRequest` (FHIR R4), `DoKFA` (master farmasi), `DoConsent`.
- **FHIRPayload Builder** (`types.go`): fluent interface (`Set/Append/ToJSON`) untuk membangun payload resource FHIR tanpa risiko *nil pointer*.
- **Resource yang DIDUKUNG** (struktur worker tersedia di [internal/worker/satusehat/](internal/worker/satusehat/)):
- Implementasi lengkap (ready-to-activate): `Encounter`, `ImagingStudy`, `ServiceRequest` (default + radiology), `Medication`, `MedicationRequest`, `MedicationDispense`.
- Kerangka stub (perlu pengisian logika sinkron): `AllergyIntolerance`, `CarePlan`, `ClinicalImpression`, `Composition`, `Condition`, `DiagnosticReport`, `EpisodeOfCare`, `Immunization`, `MedicationStatement`, `Observation`, `Procedure`, `QuestionnaireResponse`, `Specimen`.
#### B. BPJS Kesehatan
- **Klien resmi `bpjs.Client`** ([client.go](internal/interfaces/bpjs/client.go)):
- Header otomatis `X-cons-id`, `X-timestamp`, `X-signature` (HMAC-SHA256).
- Dekripsi respons V2 (AES-256-CBC, key = SHA256(ConsID+SecretKey+Timestamp), IV = 16 byte awal key, padding PKCS7).
- Auto-decompression berurutan: LZString → Gzip → plaintext JSON.
- **Service yang siap**: VClaim, AntreanRS, AntreanFKTP, Aplicare, Apotek, PCare, ICare, ERekamMedis. *Belum* terhubung sebagai worker aktif — disediakan sebagai SDK on-demand.
#### C. Master KFA (Kamus Farmasi & Alkes)
- **Worker `KFA Master Puller`** ([puller.go](internal/master/kfa/puller.go)):
- Polling halaman `/satusehat/reference/kfa/products?product_type=farmasi` via API internal `service-satusehat`.
- Fetch detail per `kfa_code`, UPSERT bundle 3 tabel: `kfa_product`, `kfa_product_active_ingredient`, `kfa_product_packaging`.
- Pencegahan rate-limit: 500ms antar item, 30s pada 429 list API, 60s pada 429 detail API, reset ke halaman 1 + idle 6 jam jika halaman kosong.
- **Status: SATU-SATUNYA WORKER YANG AKTIF** di runtime saat ini.
### 3.2. Modul Internal (System Management)
#### A. Multi-Database & CQRS
- **Database Manager** mendukung PostgreSQL, MySQL, SQL Server, SQLite, MongoDB melalui GORM + `sqlx` + driver native.
- Setiap modul bisnis (auth, role/pages, role/permission, role/master, kfa) mengikuti pola:
- `CommandRepository` (GORM, write → primary DB)
- `QueryRepository` (sqlx raw SQL, read → replica jika dikonfigurasi)
- `Service` (orkestrasi, validasi, cache-aside)
- Multi-koneksi sekaligus (config: `databases.postgres`, `databases.satudata`, `databases.simrs`).
#### B. RBAC Hierarkis
- Empat sub-modul ([internal/master/role/](internal/master/role/)):
- `master` — definisi role induk.
- `pages` — daftar halaman/menu dengan parent-child + level.
- `permission` — CRUD permission per role per page (Create/Read/Update/Delete/Disable).
- `accses` — endpoint agregasi `GetRoleAccess(userID)` untuk konsumsi UI.
- Caching tree menu via Redis dengan kunci hash SHA-256 dari parameter query; *invalidate by prefix* pada mutasi.
#### C. Transport
- **REST API** (Gin) — port 8080 default, registry-based service wiring di [http/servers](internal/infrastructure/transport/http/servers/), Swagger UI ([/docs/swagger](docs/swagger/)).
- **gRPC** (port 9090 default) — kerangka tersedia, registrasi handler belum diaktifkan ([main.go:200-204](cmd/api/main.go#L200-L204) — `permissionHandler` masih dikomentari).
- Middleware: Auth (JWT/Keycloak/Static/Hybrid), CORS, Request-ID, rate-limit Redis-backed.
#### D. Auth Pluggable
- Provider yang didukung di config: `jwt` (default), `keycloak`, `static`, `hybrid` dengan fallback. JWT signing key, Keycloak issuer/JWKS URL semuanya via env.
### 3.3. Object Storage
- **MinIO/S3** terkoneksi via [interfaces/minio](internal/interfaces/minio/minio.go) untuk penyimpanan dokumen klinis (PDF, hasil radiologi) — diinisialisasi di startup.
### 3.4. Observability
- Logger Logrus kustom (JSON, WIB timezone, rotasi harian `logs/YYYY/MM/YYYY-MM-DD.log`).
- Prometheus client (`prometheus/client_golang`) ter-import; metric endpoint perlu dipasang di routes.
- Field log standar: `service`, `environment`, `request_id`, `caller`, `function_name`.
---
## 4. Persyaratan Non-Fungsional
| Kategori | Target |
| :--- | :--- |
| **Latensi REST internal** | p95 ≤ 200 ms (cache hit), ≤ 800 ms (cache miss + DB query) |
| **Throughput Worker KFA** | ≥ 100 produk / menit (dibatasi rate limit Kemenkes) |
| **Ketahanan API gov't** | Retry otomatis pada 401 (refresh), 429 (sleep 30-60s), 5xx (retry dengan backoff) |
| **Availability** | 99.5% (single-instance), 99.9% (multi-instance — *butuh migrasi tracker DB*) |
| **Resource startup** | Token initial login, retry 6×, fail-fast Fatal jika tidak berhasil |
| **Graceful shutdown** | SIGTERM → `errgroup` cancel → wait-group worker → close DB & cache |
| **Keamanan** | TLS pada API gov't, parameter binding SQL, payload BPJS dekripsi sebelum log, password DB & secret via env |
| **Konfigurasi** | Viper YAML + overlay env var (`DATABASES_POSTGRES_HOST` dsb) |
---
## 5. Asumsi & Ketergantungan
- API internal **`service-satusehat`** (port 8096 default, `internal_fhir_server_url`) sudah running dan menyediakan endpoint:
- `/auth/login`, `/auth/refresh` — token JWT untuk worker.
- `/satusehat/reference/kfa/products` & `/products/{kfa_code}`.
- `/satusehat/medication`, `/satusehat/imaging-study`, `/satusehat/service-request`, dst. — proxy ke Kemenkes.
- Database SIMRS read-only (atau read-write via koneksi `simrs`/`satudata`) berisi tabel sumber: `m_pasien`, transaksi pesan obat, dispensing, imaging study order.
- Redis tersedia untuk cache + rate-limit; tanpa Redis sistem fallback ke NoOp cache (bukan failure).
- Akses outbound ke `https://api-satusehat.kemkes.go.id` dan `https://apijkn.bpjs-kesehatan.go.id`.
---
## 6. Kriteria Sukses (Acceptance Criteria)
### v1 (Saat Ini — Production untuk KFA saja)
- [x] Service start dengan REST + gRPC + KFA Worker tanpa error.
- [x] Master KFA tertarik secara periodik & ter-UPSERT lengkap ke 3 tabel.
- [x] RBAC CRUD via REST berfungsi, cache hit > 80% setelah warm-up.
- [x] Login worker SatuSehat berhasil retry 6× sebelum fatal.
### v1.x (Aktivasi Worker Klinis — *Target Q3 2026*)
- [ ] Worker `Medication`, `MedicationRequest`, `MedicationDispense` aktif → sync log tersimpan, mapping file harian terbentuk, FHIR ID kembali dan tersimpan.
- [ ] Worker `ServiceRequest (Radiology)` & `ImagingStudy` aktif end-to-end.
- [ ] Worker `Encounter` aktif sebagai prasyarat referensi resource lain.
- [ ] Bug `satusehatClient = nil` di [main.go:235](cmd/api/main.go#L235) diselesaikan.
### v2 (Stateless & Event-Driven — *Target Q4 2026 / Q1 2027*)
- [ ] State tracker (`*.txt`) dipindah ke tabel `sync_tracker` atau Redis hash → mendukung multi-replica.
- [ ] Kafka producer aktif (saat ini dikomentari di [main.go:111-113](cmd/api/main.go#L111-L113)).
- [ ] PostgreSQL `LISTEN/NOTIFY` menggantikan sebagian polling worker.
- [ ] Endpoint manual retry per `sync_log.id` untuk koreksi data.
---
## 7. Di Luar Cakupan (Out of Scope)
- UI dashboard (akan dibangun di repository terpisah, frontend).
- Sinkron pasien (`internal/worker/patient/migrator.go`) — kode tersedia tapi tidak dipanggil dari `main.go` / `worker.go`; dianggap **deprecated** atau tugas migrasi *one-shot* manual.
- 13 resource FHIR yang masih stub (lihat §3.1.A) — diaktifkan setelah 6 resource utama stabil.
- Integrasi BPJS aktif sebagai worker — disediakan sebagai SDK, *use case* belum dikonfirmasi.
---
## 8. Risiko & Mitigasi
| Risiko | Dampak | Mitigasi |
| :--- | :--- | :--- |
| Tracker `.txt` korup / race antara replica | Duplikasi atau data lost saat pengiriman | Pindah ke DB row-locked atau Redis `INCR` (Devplan Phase 2). |
| Token SatuSehat expired di tengah burst | Worker stuck 401 | Sudah ditangani: refresh dengan mutex + fallback re-login ([auth.go](internal/worker/auth.go)). |
| Rate limit Kemenkes berubah | Throughput turun | Setiap worker punya konstanta `rateLimitSleep` per resource — bisa di-tune via config (rekomendasi). |
| Password hardcoded di [patient/migrator.go:23-26](internal/worker/patient/migrator.go#L23-L26) | Keamanan jika diaktifkan | Hapus default atau wajibkan env var sebelum digunakan. |
| Bug `nil` SatuSehat client di main | Panic jika worker yang membutuhkan diaktifkan | Inisialisasi via `satusehat.NewClient(cfg)` sebelum `worker.NewManager`. |
---
*Disusun ulang: Tim Engineering GoPrint, 2026-05-13.*
+218
View File
@@ -0,0 +1,218 @@
# 🛡️ Quality Management Document (QMD)
**Project:** GoPrint Service General (`worker-satusehat`)
**Versi:** 1.0
**Tanggal:** 2026-05-13
Dokumen ini merangkum **standar mutu, pola tata-kelola kode, dan kontrol resiko** yang berlaku pada codebase. Setiap item disertai titik verifikasi (file/fungsi) supaya bisa diaudit ulang.
---
## 1. Standar Arsitektur
### 1.1 Clean Architecture + CQRS
Setiap modul bisnis (`auth`, `master/role/*`, `master/kfa`, `worker/satusehat/*`) **WAJIB** terdiri minimal dari:
- `entity.go` — definisi domain (GORM struct).
- `dto.go` — request/response & filter.
- `repository.go` — terpisah `CommandRepository` (GORM, primary) dan `QueryRepository` (sqlx, read replica).
- `service.go` — orkestrasi, validasi, integrasi cache.
- `mapper.go` — DTO ↔ Entity (anti-leak GORM tag).
> Generator boilerplate tersedia: lihat [scripts/](scripts/) dan `tools/generate.go`. Wajib digunakan untuk modul baru supaya struktur konsisten.
### 1.2 Pembagian Tanggung Jawab Worker
Setiap worker SatuSehat di `internal/worker/satusehat/<resource>/` menggunakan kontrak yang seragam:
- `Config{ DBManager, InternalBaseURL, OrganizationID }`
- `TokenManager` interface (`GetAccessToken`, `ForceRefreshAndGetToken`) — di-implement oleh `worker.Manager`.
- `Run(ctx context.Context)` — long-running loop dengan select-context untuk *graceful shutdown*.
- Tracker per modul (`last_*_id.txt` / `last_*_time.txt`) — *to be migrated to DB*.
---
## 2. Observability & Logging
### 2.1 Logger (`pkg/logger`)
- Wrapper **Logrus** dengan format JSON otomatis di mode `production`, text-formatted di `development`.
- **`localTimeHook`** memaksa timestamp = `Asia/Jakarta` (WIB). Tidak boleh ada log dengan timezone lain.
- Rotasi harian: `logs/YYYY/MM/YYYY-MM-DD.log` (writer ditutup & dibuka ulang per hari).
- Helper field: `logger.String/Int/Int64/Bool/Any/ErrorField` untuk konsistensi key.
- `caller` & `function_name` aktif otomatis ketika `Environment != "production"`.
### 2.2 Metrics
- `prometheus/client_golang` ter-import. Belum ada handler `/metrics` yang terpasang di routes — **TODO** sebelum production multi-instance.
### 2.3 Tracing
- Field `request_id` (UUID) wajib di-inject melalui middleware HTTP. Untuk worker, gunakan ID dokumen yang sedang diproses (`idxpesanobat`, `kfa_code`) sebagai correlation ID.
---
## 3. Database Resilience
### 3.1 Connection Pool
Setiap entry `databases.<name>` di `config.yaml` wajib mendefinisikan:
```yaml
max_open_conns: 25
max_idle_conns: 25
conn_max_lifetime: 5m
```
Jika `max_open_conns` tidak diatur, infrastructure menerapkan formula default `(CPU_Cores * 2) + 1`.
### 3.2 Multi-DB & Read Replica
- Database Manager mendukung **PostgreSQL, MySQL, SQL Server, SQLite, MongoDB** dalam satu service.
- `CommandRepository` selalu menulis ke primary; `QueryRepository` membaca dari replica (round-robin) jika dikonfigurasi.
- **SLOW QUERY THRESHOLD: 200 ms** (GORM logger adapter); query di atas ambang ini dilog warn level dengan SQL + duration.
### 3.3 Query Builder (`pkg/utils/query`)
- Whitelist kolom + dialect-aware (Postgres, MySQL, MSSQL).
- Pencegahan SQL injection wajib via parameter binding — *no string concat di repository*.
- Tests: [sql_builder_test.go](pkg/utils/query/sql_builder_test.go), [dialects_test.go](pkg/utils/query/dialects_test.go).
---
## 4. Keamanan
### 4.1 Secret Management
- Semua kredensial dilarang hardcoded. Akses via env var atau Viper dari `.env` / `.env.prod`.
- **Anomali yang diketahui** ([patient/migrator.go:23-26](internal/worker/patient/migrator.go#L23-L26)): `defaultUsername "adm_pendaftaran"` & `defaultPassword "1234"`. **WAJIB dihapus** atau dijadikan env-only sebelum file ini diaktifkan.
### 4.2 Auth
- Provider pluggable: `jwt`, `keycloak`, `static`, `hybrid` (config `auth.type` + `auth.fallback_to`).
- JWT signing key di env; Keycloak JWKS URL di env.
- Rate limit per IP dikelola Redis (`security.rate_limit.requests_per_minute`, default 60).
### 4.3 Crypto BPJS
- HMAC-SHA256 untuk signature (`X-cons-id` + `&` + `X-timestamp`).
- Dekripsi AES-256-CBC (`bpjs/crypto.go`):
- key = `SHA256(ConsID + SecretKey + Timestamp)`
- IV = 16 byte pertama dari key
- Padding PKCS7
- Auto-decompression `LZString → Gzip → plaintext` ([decompression.go](internal/interfaces/bpjs/decompression.go)).
### 4.4 SatuSehat OAuth2
- Client-Credentials flow; token cached in-memory dengan **safety margin 60 detik** sebelum exp.
- Pada 401, request retry otomatis setelah `ForceRefreshAndGetToken` ([satusehat/client.go](internal/interfaces/satusehat/client.go)).
- Mutex `tokenMutex` di [worker.Manager](internal/worker/worker.go#L25-L27) mencegah race antar worker.
### 4.5 RBAC Caching
- Kunci cache permission menggunakan SHA-256 dari (role_id, page_id, parameter query) — bukan plaintext.
- *Invalidate by prefix* otomatis pada `Create/Update/Delete`.
---
## 5. Resiliensi Worker
### 5.1 Initial Login Retry
- 6× percobaan dengan jeda 5 detik. Jika semua gagal → `Fatal` (service mati). Lihat [worker.go:42-55](internal/worker/worker.go#L42-L55).
### 5.2 Rate Limit Handling (per worker)
| Worker | Strategi |
| :--- | :--- |
| KFA Master Puller | 30s list API, 60s detail API, 500ms antar item, idle 6h saat halaman kosong |
| Medication / MedicationRequest / MedicationDispense | `rateLimitSleep = 60s` + retry dokumen yang sama (tidak skip ID) |
| ImagingStudy / ServiceRequest | Pola serupa, time-based tracker |
> Deteksi rate-limit fleksibel: cek status code 429 *atau* substring respons (`"too many requests"`, `"rate limit"`, `"limit exceeded"`, `"limit pengiriman"`, `"quota exceeded"`).
### 5.3 Token Recovery
- Tiap worker yang memanggil API internal `service-satusehat` *wajib*:
1. Sertakan `Authorization: Bearer <token>` dari `TokenManager.GetAccessToken()`.
2. Jika respons 401 → `ForceRefreshAndGetToken` lalu retry sekali.
3. Jika refresh gagal → fallback re-login ([auth.go:139-149](internal/worker/auth.go#L139-L149)).
### 5.4 Graceful Shutdown
- Root `context` dengan `errgroup` (`golang.org/x/sync/errgroup`).
- Sinyal `SIGINT`/`SIGTERM``cancel()` → semua goroutine select-context exit → `g.Wait()`.
- Tidak boleh ada `for { ... }` tanpa `select { case <-ctx.Done(): return; default: ... }` di kode worker.
### 5.5 Staggered Startup
- `baseDelay = 10s` antar tiap worker untuk mencegah *thundering herd* terhadap API gov't ([worker.go:268-303](internal/worker/worker.go#L268-L303)).
---
## 6. State Management Worker
### 6.1 Tracker File (Praktik Saat Ini)
- Lokasi: `internal/worker/satusehat/<resource>/last_*.txt` & `internal/master/kfa/kfa_tracker.txt`.
- Format: integer ID (medication, KFA, imagingstudy) atau ISO timestamp (servicerequest radiology).
- Mapping FHIR ID disimpan harian: `mapperdata/YYYY-MM/YYYY-MM-DD.txt` berformat `idxpesanobat|fhir_id`.
> **Batasan kritikal:** tidak aman untuk multi-replica. **Wajib** dipindah ke DB / Redis untuk deployment Kubernetes (lihat Devplan Phase 2).
### 6.2 Sync Log
- Setiap pengiriman → record `SyncLog` (request payload, response, status `SUCCESS`/`FAILED`/`RATE_LIMITED`, error message) tersimpan di tabel SIMRS untuk audit.
---
## 7. Standar Penulisan Kode
### 7.1 Linting & Format
- `go fmt` mandatory (CI hook disarankan).
- Komentar fungsi publik dalam Bahasa Indonesia diperbolehkan (codebase saat ini multilingual); konsistensi per file diutamakan.
### 7.2 Error Handling
- Gunakan `pkg/errors.AppError` untuk error berkode (HTTP/gRPC mapping otomatis).
- *Wrapping*: `fmt.Errorf("konteks: %w", err)` — selalu `%w` jika ingin di-unwrap.
- Worker **tidak boleh panic**; pakai `logger.Error` + retry/skip.
### 7.3 Testing
- File `*_test.go` wajib di package `pkg/utils/query`. Coverage modul bisnis saat ini rendah — **TODO** unit test layer service.
### 7.4 Generator Boilerplate
- Modul baru dibuat via `scripts/context.sh -s <file.sql>` atau `-j <file.json>`.
- Generator menghasilkan: entity, DTO, repository (Command + Query), service, mapper, gRPC proto draft.
---
## 8. Dependensi & Lisensi
- Go runtime: **1.25** (go.mod baris 3).
- Web: Gin 1.10, gRPC 1.78, Swagger via `swaggo/swag` 1.16.
- Persistence: GORM 1.30 (postgres/mysql/sqlserver/sqlite driver), sqlx 1.4, MongoDB driver 1.17.
- Cache & infra: redis/go-redis v9.17, MinIO Go v7.0.98, Prometheus client 1.23.
- Crypto: golang.org/x/crypto v0.49, JWT golang-jwt v5.2.
- Util: logrus 1.9, viper-style config via `karincake/apem`.
- License: Apache 2.0 (lihat header swagger).
---
## 9. Checklist Audit Periodik
- [ ] Semua tracker `.txt` masih dibutuhkan & sinkron dengan DB.
- [ ] Tidak ada secret di repository (`grep -r "password.*=.*\"" .` clean).
- [ ] `logger.Default()` digunakan; tidak ada `fmt.Println` di production path.
- [ ] Setiap worker punya rate-limit handler + token retry.
- [ ] Cache TTL & invalidation pattern terdokumentasi per modul.
- [ ] Migration script idempotent.
- [ ] Prometheus `/metrics` endpoint live & terkonsumsi monitoring stack.
---
*Disusun ulang: Tim Engineering GoPrint, 2026-05-13.*
+36 -2
View File
@@ -1,3 +1,37 @@
# Architecture
# 🏛️ System Architecture
TODO: Describe the project architecture.
Aplikasi **GoPrint Service General** dirancang menggunakan kombinasi prinsip **Clean Architecture**, **CQRS (Command Query Responsibility Segregation)**, dan **Event-Driven / Worker Patterns**.
## 1. High-Level Topologi
Aplikasi ini bertindak sebagai **Middleware / Bridging API** antara:
1. **Sistem Internal (SIMRS Lokal):** Berkomunikasi via REST API & gRPC.
2. **Sistem Eksternal (Nasional):** Berkomunikasi via HTTP Client ke BPJS (VClaim/Antrol) dan Kemenkes (SatuSehat FHIR & KFA).
## 2. Core Components
### A. Transport Layer (Dual-Protocol Server)
* **HTTP/REST (Gin Framework):** Menyediakan antarmuka standar bagi *frontend* dan sistem *legacy*. Implementasi *Service Registry* (`httpServer.ServiceRegistry`) menjamin manajemen rute yang modular.
* **gRPC:** Menggunakan Protobuf untuk komunikasi internal antar-*microservice* dengan latensi ultra-rendah dan *payload* terkompresi.
### B. Data & Infrastructure Layer
* **Multi-Database Manager (`database.go`):**
Mampu melayani multi-koneksi sekaligus (Postgres, MySQL, SQLServer, SQLite, MongoDB). Otomatis memisahkan beban *Read* (menggunakan *Read Replicas* dengan *round-robin balancer*) dan *Write* (ke *Primary DB*). Terdapat *auto-tuning* koneksi (MaxOpen, MaxIdle).
* **Cache Manager (`cache.go`):**
Redis digunakan secara ekstensif dengan pola **Cache-Aside**. Pada modul `permission`, pencarian dan *tree-hierarchy* di- *cache* (kunci menggunakan *hash* SHA-256 dari parameter *query*) dan diinvalidasi otomatis saat terjadi `Create`/`Update`/`Delete`.
* **Object Storage:** MinIO/S3 terintegrasi untuk penyimpanan aset (seperti hasil *ImagingStudy* atau dokumen medis).
### C. Business Logic Layer (CQRS Pattern)
Setiap modul (contoh: `role/permission`) dibagi dengan jelas:
* **Command Repository:** Khusus mutasi data (`Create`, `Update`, `Delete`) dengan GORM ke Primary DB.
* **Query Repository:** Khusus pengambilan data (`FindAll`, `Search`) menggunakan `sqlx` ke Read Replica DB agar sangat cepat dan tidak mengganggu transaksi utama.
* **Service:** Menangani aturan bisnis, validasi, dan integrasi ke *Cache Manager*.
### D. External Integration & Background Workers
* **BPJS Client (`bpjs/client.go`):** Meng- *handle* generasi *header* HMAC SHA256, *timestamp*, otomatis dekripsi *payload* VClaim V2, dan *error handling* standar BPJS.
* **SatuSehat Client (`satusehat/types.go`):** Menyediakan `FHIRPayload` *builder* (*fluent interface*) untuk JSON FHIR, serta manajemen *Access Token* OAuth2.
* **Background Workers:** Berjalan secara asinkron (`goroutine`) menggunakan `errgroup`. *Worker* seperti `migrator.go` menyinkronkan data pasien secara bertahap untuk menghindari *Rate Limit* Kemenkes, dicatat via `tracker file`.
## 3. Observability & Logging
Menggunakan sistem *logging* internal (berbasis `logrus`):
* Menambahkan **X-Request-ID** dan **Trace ID** di *Context*.
* Menjalankan *Hook* waktu **WIB (Asia/Jakarta)** secara ketat.
* Rotasi *log file* harian (`dailyFileWriter`) ke dalam folder `logs/YYYY/MM/YYYY-MM-DD.log`.
+48 -2
View File
@@ -1,3 +1,49 @@
# Deployment
# 🚀 Deployment Guide
TODO: Add deployment instructions.
Dokumen ini memuat panduan untuk men-*deploy* aplikasi **GoPrint Service General** di lingkungan *Staging* maupun *Production*.
## 1. Prerequisites
Pastikan infrastruktur/server telah memiliki:
* **Go** versi 1.21 atau lebih tinggi.
* **PostgreSQL / MySQL** (sebagai *Primary Database*).
* **Redis** (untuk mekanisme *Caching*).
* **MinIO / AWS S3** (Opsional, untuk penyimpanan berkas).
* Akses jaringan keluar (Koneksi ke `https://apijkn.bpjs-kesehatan.go.id` dan `https://api-satusehat.kemkes.go.id`).
## 2. Configuration (`config.yaml`)
Aplikasi menggunakan Viper untuk memuat konfigurasi. Siapkan `config.yaml` di *root* proyek. Beberapa nilai kunci yang harus disiapkan:
```yaml
server:
mode: "production" # development | production
rest:
enabled: true
port: 8080
grpc:
enabled: true
port: 9090
databases:
default:
type: "postgres"
host: "localhost"
port: 5432
database: "simrs_db"
username: "postgres"
password: "securepassword"
cache:
enabled: true
redis:
host: "localhost"
port: 6379
```
## 3. Build & Run (Bare-Metal)
```bash
# Build binary
go build -o service-general ./cmd/api/main.go
# Jalankan sebagai background process atau Service (systemd)
./service-general
```
@@ -48,6 +48,7 @@ type ServiceRequestJson struct {
Coding []struct {
Code string `json:"code"`
Display string `json:"display"`
System string `json:"system"`
} `json:"coding"`
} `json:"code"`
Requester struct {
@@ -50,9 +50,11 @@ func MapRadiologyToInternalAPI(dbData *ServiceRequestRadDB) map[string]interface
codeVal := ""
codeDisplay := ""
codeSystem := ""
if len(parsedData.Code.Coding) > 0 {
codeVal = parsedData.Code.Coding[0].Code
codeDisplay = parsedData.Code.Coding[0].Display
codeSystem = parsedData.Code.Coding[0].System
}
requesterID := dbData.RequesterID.String
@@ -86,6 +88,7 @@ func MapRadiologyToInternalAPI(dbData *ServiceRequestRadDB) map[string]interface
"category_display": categoryDisplay,
"code": codeVal,
"display": codeDisplay,
"system": codeSystem,
"requester_id": requesterID,
"performer_id": performerID,
"authored_on": parsedData.AuthoredOn,
+72 -68
View File
@@ -11,11 +11,6 @@ import (
"service/internal/infrastructure/database"
"service/internal/interfaces/satusehat"
"service/internal/master/kfa"
"service/internal/worker/satusehat/imagingstudy"
"service/internal/worker/satusehat/medication"
"service/internal/worker/satusehat/medicationdispense"
"service/internal/worker/satusehat/medicationrequest"
"service/internal/worker/satusehat/servicerequest"
"service/pkg/logger"
)
@@ -45,9 +40,18 @@ func (m *Manager) Start(ctx context.Context) {
logger.Default().Info("Starting background workers...")
// Lakukan login awal untuk mendapatkan token bagi semua worker
if err := m.login(ctx); err != nil {
logger.Default().Fatal("Initial login for workers failed, stopping.", logger.ErrorField(err))
return
maxRetries := 6
for i := 1; i <= maxRetries; i++ {
err := m.login(ctx)
if err == nil {
break
}
logger.Default().Warn(fmt.Sprintf("Initial login attempt %d/%d failed, retrying in 5 seconds...", i, maxRetries), logger.ErrorField(err))
if i == maxRetries {
logger.Default().Fatal("Initial login for workers failed after maximum retries, stopping.", logger.ErrorField(err))
return
}
time.Sleep(5 * time.Second)
}
var wg sync.WaitGroup
@@ -86,17 +90,17 @@ func (m *Manager) Start(ctx context.Context) {
// // condition.NewWorker(...).Run(c)
// },
// },
{
name: "Medication (Master)",
delay: 0, // Master data berjalan seketika
run: func(c context.Context) {
medication.NewWorker(medication.Config{
DBManager: m.db,
InternalBaseURL: internalBaseURL,
OrganizationID: orgID,
}, m).Run(c)
},
},
// {
// name: "Medication (Master)",
// delay: 2, // Master data berjalan seketika
// run: func(c context.Context) {
// medication.NewWorker(medication.Config{
// DBManager: m.db,
// InternalBaseURL: internalBaseURL,
// OrganizationID: orgID,
// }, m).Run(c)
// },
// },
// {
// name: "Observation",
// delay: 2 * time.Second,
@@ -148,17 +152,17 @@ func (m *Manager) Start(ctx context.Context) {
// }, m).Run(c)
// },
// },
{
name: "ServiceRequest (Radiology)",
delay: 2 * time.Second,
run: func(c context.Context) {
servicerequest.NewRadiologyWorker(servicerequest.Config{
DBManager: m.db,
InternalBaseURL: internalBaseURL,
OrganizationID: orgID,
}, m).Run(c)
},
},
// {
// name: "ServiceRequest (Radiology)",
// delay: 2 * time.Second,
// run: func(c context.Context) {
// servicerequest.NewRadiologyWorker(servicerequest.Config{
// DBManager: m.db,
// InternalBaseURL: internalBaseURL,
// OrganizationID: orgID,
// }, m).Run(c)
// },
// },
// {
// name: "Specimen",
// delay: 2 * time.Second,
@@ -175,16 +179,16 @@ func (m *Manager) Start(ctx context.Context) {
// // diagnosticreport.NewWorker(...).Run(c)
// },
// },
{
name: "ImagingStudy",
delay: 2 * time.Second,
run: func(c context.Context) {
// The Manager 'm' now acts as the TokenManager
imagingstudy.NewWorker(imagingstudy.Config{
DBManager: m.db, InternalBaseURL: internalBaseURL, OrganizationID: orgID,
}, m).Run(c)
},
},
// {
// name: "ImagingStudy",
// delay: 2 * time.Second,
// run: func(c context.Context) {
// // The Manager 'm' now acts as the TokenManager
// imagingstudy.NewWorker(imagingstudy.Config{
// DBManager: m.db, InternalBaseURL: internalBaseURL, OrganizationID: orgID,
// }, m).Run(c)
// },
// },
// {
// name: "Composition",
// delay: 2 * time.Second,
@@ -209,40 +213,40 @@ func (m *Manager) Start(ctx context.Context) {
// // episodeofcare.NewWorker(...).Run(c)
// },
// },
{
name: "MedicationRequest",
delay: 2 * time.Second,
run: func(c context.Context) {
medicationrequest.NewWorker(medicationrequest.Config{
DBManager: m.db,
InternalBaseURL: internalBaseURL,
OrganizationID: orgID,
}, m).Run(c)
},
},
{
name: "MedicationDispense",
delay: 2 * time.Second,
run: func(c context.Context) {
medicationdispense.NewWorker(medicationdispense.Config{
DBManager: m.db,
InternalBaseURL: internalBaseURL,
OrganizationID: orgID,
}, m).Run(c)
},
},
// {
// name: "KFA Master Puller",
// delay: 5 * time.Second,
// name: "MedicationRequest",
// delay: 2 * time.Second,
// run: func(c context.Context) {
// repo := kfa.NewCommandRepository(m.db, "default")
// kfa.NewWorker(kfa.Config{
// medicationrequest.NewWorker(medicationrequest.Config{
// DBManager: m.db,
// InternalBaseURL: internalBaseURL,
// PageSize: 100,
// }, repo, m).Run(c)
// OrganizationID: orgID,
// }, m).Run(c)
// },
// },
// {
// name: "MedicationDispense",
// delay: 2 * time.Second,
// run: func(c context.Context) {
// medicationdispense.NewWorker(medicationdispense.Config{
// DBManager: m.db,
// InternalBaseURL: internalBaseURL,
// OrganizationID: orgID,
// }, m).Run(c)
// },
// },
{
name: "KFA Master Puller",
delay: 5 * time.Second,
run: func(c context.Context) {
repo := kfa.NewCommandRepository(m.db, "default")
kfa.NewWorker(kfa.Config{
InternalBaseURL: internalBaseURL,
PageSize: 100,
}, repo, m).Run(c)
},
},
// {
// name: "MedicationStatement",
// delay: 2 * time.Second,
// run: func(c context.Context) {