update worker
This commit is contained in:
No files matched your search
+34
-6
@@ -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
@@ -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"]
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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 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.*
|
||||
+250
@@ -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.*
|
||||
+169
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user