diff --git a/DOCUMENT.md b/DOCUMENT.md index 0cd8681..a6bfbd6 100644 --- a/DOCUMENT.md +++ b/DOCUMENT.md @@ -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 diff --git a/Dockerfile b/Dockerfile index 9a7c76e..961941d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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"] \ No newline at end of file diff --git a/Dockerfile.dev b/Dockerfile.dev index 20c7d9c..20d6d5c 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -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/air@v1.51.0 diff --git a/cmd/api/main.go b/cmd/api/main.go index ab36543..f17e4cb 100644 --- a/cmd/api/main.go +++ b/cmd/api/main.go @@ -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 { diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 1b66bc9..e02625d 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -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= diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml index d82ee5b..cd9c806 100644 --- a/docker-compose.prod.yml +++ b/docker-compose.prod.yml @@ -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 diff --git a/docs/Devlog.md b/docs/Devlog.md new file mode 100644 index 0000000..31066f4 --- /dev/null +++ b/docs/Devlog.md @@ -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 (`admin@example.com` / `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 93โ€“117 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.* diff --git a/docs/Devplan.md b/docs/Devplan.md new file mode 100644 index 0000000..6cca646 --- /dev/null +++ b/docs/Devplan.md @@ -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 *(1โ€“2 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 *(2โ€“4 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) *(2โ€“3 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 *(4โ€“6 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 *(2โ€“3 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 | 1โ€“2 minggu | `v0.9.0` | Bug kritikal fixed, metrics live | +| 1 โ€” Aktivasi Klinis | 2โ€“4 minggu | `v1.0.0` | Worker obat & radiologi production | +| 2 โ€” Stateless Tracker | 2โ€“3 minggu | `v1.1.0` | Multi-replica safe | +| 3 โ€” Event-Driven | 4โ€“6 minggu | `v1.2.0` | LISTEN/NOTIFY + Kafka | +| 4 โ€” Worker Lengkap | rolling | `v1.x.y` | 13 resource FHIR aktif | +| 5 โ€” Observability | 2โ€“3 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.* diff --git a/docs/PRD.md b/docs/PRD.md new file mode 100644 index 0000000..e7cd79c --- /dev/null +++ b/docs/PRD.md @@ -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.* diff --git a/docs/QMD.md b/docs/QMD.md new file mode 100644 index 0000000..c921881 --- /dev/null +++ b/docs/QMD.md @@ -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//` 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.` 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 ` 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//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 ` atau `-j `. +- 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.* diff --git a/docs/architecture.md b/docs/architecture.md index bdff363..de5ad10 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -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`. diff --git a/docs/deployment.md b/docs/deployment.md index 86c0e76..c4f105d 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -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 +``` diff --git a/internal/worker/satusehat/servicerequest/dto.go b/internal/worker/satusehat/servicerequest/dto.go index 2e64625..dc0245b 100644 --- a/internal/worker/satusehat/servicerequest/dto.go +++ b/internal/worker/satusehat/servicerequest/dto.go @@ -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 { diff --git a/internal/worker/satusehat/servicerequest/mapper.go b/internal/worker/satusehat/servicerequest/mapper.go index 5a8d480..c848088 100644 --- a/internal/worker/satusehat/servicerequest/mapper.go +++ b/internal/worker/satusehat/servicerequest/mapper.go @@ -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, diff --git a/internal/worker/worker.go b/internal/worker/worker.go index 98cc9c7..16aa622 100644 --- a/internal/worker/worker.go +++ b/internal/worker/worker.go @@ -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) {