update ignore
This commit is contained in:
No files matched your search
@@ -0,0 +1,20 @@
|
||||
package medication
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
)
|
||||
|
||||
// MedicationDB merepresentasikan tabel transaksi obat dari t_permintaan_apotek_rajal
|
||||
type MedicationDB struct {
|
||||
IdxPesanObat int64 `db:"idxpesanobat"`
|
||||
KfaCode sql.NullString `db:"kode_kfa"`
|
||||
}
|
||||
|
||||
type MedicationSyncLog struct {
|
||||
IdxPesanObat int64 `db:"idxpesanobat"`
|
||||
SatuSehatID string `db:"satusehat_id"`
|
||||
RequestPayload string `db:"request_payload"`
|
||||
ResponsePayload string `db:"response_payload"`
|
||||
Status string `db:"status"`
|
||||
ErrorMessage string `db:"error_message"`
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
3664014
|
||||
@@ -0,0 +1,46 @@
|
||||
package medication
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// MapToInternalAPI memetakan data DB ke payload API Internal Medication
|
||||
func MapToInternalAPI(dbData *MedicationDB, orgID string, kfaDisplay, formCode, formDisplay string) map[string]interface{} {
|
||||
payload := make(map[string]interface{})
|
||||
|
||||
payload["status_code"] = "active"
|
||||
|
||||
if dbData.IdxPesanObat != 0 {
|
||||
payload["medication_id"] = strconv.FormatInt(dbData.IdxPesanObat, 10)
|
||||
}
|
||||
if dbData.KfaCode.Valid && dbData.KfaCode.String != "" {
|
||||
payload["kfa_code"] = dbData.KfaCode.String
|
||||
}
|
||||
if orgID != "" {
|
||||
payload["manufacturer_id"] = orgID
|
||||
}
|
||||
|
||||
if kfaDisplay != "" {
|
||||
payload["kfa_display"] = kfaDisplay
|
||||
}
|
||||
if formCode != "" {
|
||||
payload["form_code"] = formCode
|
||||
}
|
||||
if formDisplay != "" {
|
||||
payload["form_display"] = formDisplay
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
func MapToSyncLog(idx int64, fhirID string, reqPayload interface{}, respBody []byte, status string, errMsg string) MedicationSyncLog {
|
||||
reqBytes, _ := json.Marshal(reqPayload)
|
||||
return MedicationSyncLog{
|
||||
IdxPesanObat: idx,
|
||||
SatuSehatID: fhirID,
|
||||
RequestPayload: string(reqBytes),
|
||||
ResponsePayload: string(respBody),
|
||||
Status: status,
|
||||
ErrorMessage: errMsg,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load diff
@@ -0,0 +1,92 @@
|
||||
package medication
|
||||
|
||||
import (
|
||||
"context"
|
||||
"service/internal/infrastructure/database"
|
||||
"service/pkg/utils/query"
|
||||
)
|
||||
|
||||
type Repository interface {
|
||||
GetNextMedication(ctx context.Context, lastID int64) (*MedicationDB, error)
|
||||
SaveSatuSehatID(ctx context.Context, idxPesanObat int64, fhirID string) error
|
||||
SaveSyncLog(ctx context.Context, logData MedicationSyncLog) error
|
||||
}
|
||||
|
||||
type repository struct {
|
||||
dbManager database.Service
|
||||
}
|
||||
|
||||
func NewRepository(dbManager database.Service) Repository {
|
||||
return &repository{dbManager: dbManager}
|
||||
}
|
||||
|
||||
func (r *repository) GetNextMedication(ctx context.Context, lastID int64) (*MedicationDB, error) {
|
||||
var med MedicationDB
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).
|
||||
SetSecurityOptions(false, 0).
|
||||
SetQueryLogging(false).
|
||||
SetAllowedColumns([]string{"idxpesanobat", "kode_kfa", "tgl_pesan"})
|
||||
|
||||
// startDate, _ := time.Parse("2006-01-02", "2026-01-02")
|
||||
|
||||
qSimrs := query.DynamicQuery{
|
||||
From: "public.t_permintaan_apotek_rajal",
|
||||
Fields: []query.SelectField{
|
||||
{Expression: "idxpesanobat"},
|
||||
{Expression: "kode_kfa"},
|
||||
},
|
||||
Filters: []query.FilterGroup{
|
||||
{
|
||||
Filters: []query.DynamicFilter{
|
||||
query.CreateFilter("idxpesanobat", query.OpGreaterThanEqual, lastID+1),
|
||||
// query.CreateFilter("tgl_pesan", query.OpGreaterThanEqual, startDate),
|
||||
},
|
||||
},
|
||||
},
|
||||
Sort: []query.SortField{
|
||||
query.CreateAscSort("idxpesanobat"),
|
||||
},
|
||||
Limit: 1,
|
||||
}
|
||||
|
||||
err = qb.ExecuteQueryRow(ctx, simrsDB, qSimrs, &med)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &med, nil
|
||||
}
|
||||
|
||||
func (r *repository) SaveSatuSehatID(ctx context.Context, idxPesanObat int64, fhirID string) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).
|
||||
SetSecurityOptions(false, 0).
|
||||
SetQueryLogging(false).
|
||||
SetAllowedColumns([]string{"id_satusehat", "idxpesanobat"})
|
||||
_, err = qb.ExecuteUpdate(ctx, simrsDB, "public.t_permintaan_apotek_rajal", query.UpdateData{Columns: []string{"id_satusehat"}, Values: []interface{}{fhirID}}, []query.FilterGroup{query.CreateAndFilterGroup([]query.DynamicFilter{query.CreateEqualFilter("idxpesanobat", idxPesanObat)})})
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *repository) SaveSyncLog(ctx context.Context, logData MedicationSyncLog) error {
|
||||
simrsDB, err := r.dbManager.GetSQLXDB("simrs")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
qb := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).
|
||||
SetSecurityOptions(false, 0).
|
||||
SetQueryLogging(false).
|
||||
SetAllowedColumns([]string{"idxpesanobat", "satusehat_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
insertData := query.InsertData{
|
||||
Columns: []string{"idxpesanobat", "satusehat_id", "request_payload", "response_payload", "status", "error_message"},
|
||||
Values: []interface{}{logData.IdxPesanObat, logData.SatuSehatID, logData.RequestPayload, logData.ResponsePayload, logData.Status, logData.ErrorMessage},
|
||||
}
|
||||
_, err = qb.ExecuteUpsert(ctx, simrsDB, "public.log_satusehat_medication", insertData, []string{"idxpesanobat"}, []string{"satusehat_id", "request_payload", "response_payload", "status", "error_message"})
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
package medication
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"service/internal/infrastructure/database"
|
||||
extapi "service/internal/worker/interface"
|
||||
"service/pkg/logger"
|
||||
)
|
||||
|
||||
const trackerFile = "internal/worker/satusehat/medication/last_medication_id.txt"
|
||||
const mappingBaseDir = "internal/worker/satusehat/medication/mapperdata"
|
||||
const rateLimitSleep = 60 * time.Second
|
||||
|
||||
type Config struct {
|
||||
DBManager database.Service
|
||||
InternalBaseURL string
|
||||
OrganizationID string
|
||||
}
|
||||
|
||||
type TokenManager interface {
|
||||
GetAccessToken() string
|
||||
ForceRefreshAndGetToken(ctx context.Context) (string, error)
|
||||
}
|
||||
|
||||
type WorkerService interface {
|
||||
Run(ctx context.Context)
|
||||
}
|
||||
|
||||
type worker struct {
|
||||
cfg Config
|
||||
repo Repository
|
||||
apiClient extapi.Client
|
||||
tokenManager TokenManager
|
||||
}
|
||||
|
||||
func NewWorker(cfg Config, tokenManager TokenManager) WorkerService {
|
||||
return &worker{
|
||||
cfg: cfg,
|
||||
repo: NewRepository(cfg.DBManager),
|
||||
apiClient: extapi.NewClient(60 * time.Second),
|
||||
tokenManager: tokenManager,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) Run(ctx context.Context) {
|
||||
logger.Default().Info("[MEDICATION WORKER] Memulai proses sinkronisasi API...")
|
||||
|
||||
w.ensureTrackerFileExists()
|
||||
lastID := w.readLastID()
|
||||
// if lastID == 0 {
|
||||
// lastID = 3384064 // Dikurangi 1 agar ID 3384065 ikut terambil
|
||||
// w.updateTracker(&lastID, lastID)
|
||||
// }
|
||||
logger.Default().Info("[MEDICATION WORKER] Melanjutkan proses dari", logger.Int64("last_id", lastID))
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logger.Default().Warn("[MEDICATION WORKER] Proses dihentikan oleh sistem (Graceful Shutdown)")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
med, err := w.repo.GetNextMedication(ctx, lastID)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
time.Sleep(5 * time.Second) // Tunggu data baru
|
||||
continue
|
||||
}
|
||||
logger.Default().Error("[MEDICATION WORKER] Gagal query ke database", logger.ErrorField(err))
|
||||
time.Sleep(5 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Default().Info("[MEDICATION WORKER] 🔄 Memproses Dokumen",
|
||||
logger.Int64("idxpesanobat", med.IdxPesanObat),
|
||||
logger.String("kode_kfa", med.KfaCode.String),
|
||||
)
|
||||
|
||||
if !med.KfaCode.Valid || med.KfaCode.String == "" {
|
||||
logger.Default().Warn("[MEDICATION WORKER] ⏭️ Skip Dokumen: KFA Code kosong",
|
||||
logger.Int64("idxpesanobat", med.IdxPesanObat),
|
||||
)
|
||||
w.updateTracker(&lastID, med.IdxPesanObat)
|
||||
continue
|
||||
}
|
||||
|
||||
kfaDisplay, formCode, formDisplay := w.fetchKFADetails(ctx, med.KfaCode.String)
|
||||
|
||||
internalPayload := MapToInternalAPI(med, w.cfg.OrganizationID, kfaDisplay, formCode, formDisplay)
|
||||
|
||||
fhirID, respBody, status, errMsg, shouldRetry, rateLimited := w.sendToInternalAPI(ctx, internalPayload, med.IdxPesanObat)
|
||||
|
||||
if rateLimited {
|
||||
logger.Default().Warn("[MEDICATION WORKER] ⏸️ Rate limit dari Satu Sehat - jeda dan akan retry dokumen yang sama",
|
||||
logger.Int64("idxpesanobat", med.IdxPesanObat),
|
||||
logger.String("response", truncate(string(respBody), 300)),
|
||||
logger.String("sleep", rateLimitSleep.String()),
|
||||
)
|
||||
time.Sleep(rateLimitSleep)
|
||||
continue
|
||||
}
|
||||
|
||||
// Menyimpan payload request dan response ke dalam log
|
||||
logger.Default().Info("[MEDICATION WORKER] Detail Transaksi",
|
||||
logger.Int64("idxpesanobat", med.IdxPesanObat),
|
||||
logger.Any("request_payload", internalPayload),
|
||||
logger.String("response_payload", string(respBody)),
|
||||
logger.String("status", status),
|
||||
)
|
||||
|
||||
syncLog := MapToSyncLog(med.IdxPesanObat, fhirID, internalPayload, respBody, status, errMsg)
|
||||
if errLog := w.repo.SaveSyncLog(ctx, syncLog); errLog != nil {
|
||||
logger.Default().Error("[MEDICATION WORKER] Gagal menyimpan histori log ke DB", logger.ErrorField(errLog))
|
||||
}
|
||||
|
||||
if status == "SUCCESS" && fhirID != "" {
|
||||
// if errUpdate := w.repo.SaveSatuSehatID(ctx, med.IdxPesanObat, fhirID); errUpdate != nil {
|
||||
// logger.Default().Error("[MEDICATION WORKER] Gagal update FHIR ID ke transaksi utama", logger.ErrorField(errUpdate))
|
||||
// }
|
||||
w.saveMapping(med.IdxPesanObat, fhirID)
|
||||
}
|
||||
|
||||
// SELALU update tracker agar lastID berjalan maju terus dan tidak mengulang data yang sama
|
||||
w.updateTracker(&lastID, med.IdxPesanObat)
|
||||
|
||||
if shouldRetry {
|
||||
logger.Default().Warn("[MEDICATION WORKER] ⚠️ Gagal memproses data (melewati dokumen)", logger.Int64("idxpesanobat", med.IdxPesanObat), logger.String("reason", errMsg))
|
||||
time.Sleep(5 * time.Second)
|
||||
} else {
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) sendToInternalAPI(ctx context.Context, payload map[string]interface{}, idxPesanObat int64) (fhirID string, respBody []byte, status string, errMsg string, shouldRetry bool, rateLimited bool) {
|
||||
reqURL := fmt.Sprintf("%s/satusehat/medication", strings.TrimRight(w.cfg.InternalBaseURL, "/"))
|
||||
|
||||
respBody, statusCode, err := w.apiClient.PostJSON(ctx, reqURL, w.tokenManager.GetAccessToken(), payload)
|
||||
if err != nil {
|
||||
logger.Default().Error("[MEDICATION WORKER] ❌ Gagal Koneksi ke API Part 3",
|
||||
logger.Int64("idxpesanobat", idxPesanObat),
|
||||
logger.Any("payload_sent", payload),
|
||||
logger.ErrorField(err),
|
||||
)
|
||||
return "", nil, "FAILED", err.Error(), true, false
|
||||
}
|
||||
|
||||
if statusCode == http.StatusUnauthorized {
|
||||
logger.Default().Warn("[MEDICATION WORKER] Menerima status 401 Unauthorized, mencoba refresh token...", logger.Int64("idxpesanobat", idxPesanObat))
|
||||
|
||||
newToken, refreshErr := w.tokenManager.ForceRefreshAndGetToken(ctx)
|
||||
if refreshErr != nil {
|
||||
logger.Default().Error("[MEDICATION WORKER] Gagal refresh token, akan mencoba lagi pada iterasi berikutnya.", logger.Int64("idxpesanobat", idxPesanObat), logger.ErrorField(refreshErr))
|
||||
return "", respBody, "FAILED", refreshErr.Error(), true, false
|
||||
}
|
||||
|
||||
logger.Default().Info("[MEDICATION WORKER] Token berhasil di-refresh, mencoba ulang request...", logger.Int64("idxpesanobat", idxPesanObat))
|
||||
respBody, statusCode, err = w.apiClient.PostJSON(ctx, reqURL, newToken, payload)
|
||||
if err != nil {
|
||||
logger.Default().Error("[MEDICATION WORKER] ❌ Gagal Koneksi ke API Part 3 (setelah retry)",
|
||||
logger.Int64("idxpesanobat", idxPesanObat),
|
||||
logger.ErrorField(err),
|
||||
)
|
||||
return "", nil, "FAILED", err.Error(), true, false
|
||||
}
|
||||
}
|
||||
|
||||
if isRateLimitResponse(statusCode, respBody) {
|
||||
errMsg = fmt.Sprintf("HTTP %d - rate limit Satu Sehat", statusCode)
|
||||
return "", respBody, "RATE_LIMITED", errMsg, true, true
|
||||
}
|
||||
|
||||
if statusCode != http.StatusOK && statusCode != http.StatusCreated {
|
||||
errMsg = fmt.Sprintf("HTTP %d", statusCode)
|
||||
logger.Default().Error("[MEDICATION WORKER] ❌ Respons Error dari API Part 3",
|
||||
logger.Int64("idxpesanobat", idxPesanObat),
|
||||
logger.Int("status_code", statusCode),
|
||||
logger.String("response", truncate(string(respBody), 200)),
|
||||
)
|
||||
|
||||
if statusCode >= 500 {
|
||||
return "", respBody, "FAILED", errMsg, true, false
|
||||
}
|
||||
return "", respBody, "FAILED", errMsg, false, false
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if errUnmarshal := json.Unmarshal(respBody, &result); errUnmarshal == nil {
|
||||
if data, ok := result["data"].(map[string]interface{}); ok {
|
||||
if id, ok := data["id"].(string); ok {
|
||||
fhirID = id
|
||||
}
|
||||
} else if id, ok := result["id"].(string); ok {
|
||||
fhirID = id
|
||||
}
|
||||
}
|
||||
logger.Default().Info("[MEDICATION WORKER] ✅ Sukses mengirim data Medication",
|
||||
logger.Int64("idxpesanobat", idxPesanObat),
|
||||
logger.String("fhir_id", fhirID),
|
||||
)
|
||||
|
||||
return fhirID, respBody, "SUCCESS", "", false, false
|
||||
}
|
||||
|
||||
// isRateLimitResponse mendeteksi respons rate-limit dari Satu Sehat
|
||||
// (HTTP 429 atau pesan body yang mengindikasikan limit pengiriman terlampaui).
|
||||
func isRateLimitResponse(statusCode int, respBody []byte) bool {
|
||||
if statusCode == http.StatusTooManyRequests {
|
||||
return true
|
||||
}
|
||||
body := strings.ToLower(string(respBody))
|
||||
return strings.Contains(body, "too many requests") ||
|
||||
strings.Contains(body, "rate limit") ||
|
||||
strings.Contains(body, "limit exceeded") ||
|
||||
strings.Contains(body, "limit pengiriman") ||
|
||||
strings.Contains(body, "quota exceeded")
|
||||
}
|
||||
|
||||
func (w *worker) fetchKFADetails(ctx context.Context, kfaCode string) (kfaDisplay, formCode, formDisplay string) {
|
||||
reqURL := fmt.Sprintf("%s/satusehat/reference/kfa/products/%s", strings.TrimRight(w.cfg.InternalBaseURL, "/"), kfaCode)
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqURL, nil)
|
||||
if err != nil {
|
||||
logger.Default().Error("[MEDICATION WORKER] Gagal membuat request KFA", logger.ErrorField(err))
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
req.Header.Set("Authorization", "Bearer "+w.tokenManager.GetAccessToken())
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
client := &http.Client{Timeout: 10 * time.Second}
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
logger.Default().Error("[MEDICATION WORKER] Gagal request data ke Part 3 KFA", logger.ErrorField(err))
|
||||
return "", "", ""
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
logger.Default().Warn("[MEDICATION WORKER] KFA tidak ditemukan / Error", logger.Int("status", resp.StatusCode), logger.String("kfa_code", kfaCode))
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
logger.Default().Error("[MEDICATION WORKER] Gagal decode response KFA", logger.ErrorField(err))
|
||||
return "", "", ""
|
||||
}
|
||||
|
||||
// Parsing respons bersarang dari KFA API (data -> result -> dosage_form)
|
||||
if dataMap, ok := result["data"].(map[string]interface{}); ok {
|
||||
if resultMap, ok := dataMap["result"].(map[string]interface{}); ok {
|
||||
// Ekstrak nama obat
|
||||
if val, ok := resultMap["name"].(string); ok {
|
||||
kfaDisplay = val
|
||||
}
|
||||
|
||||
// Ekstrak bentuk obat (dosage_form)
|
||||
if dosageForm, ok := resultMap["dosage_form"].(map[string]interface{}); ok {
|
||||
if val, ok := dosageForm["code"].(string); ok {
|
||||
formCode = val
|
||||
}
|
||||
if val, ok := dosageForm["name"].(string); ok {
|
||||
formDisplay = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback jika format respons langsung flat
|
||||
if kfaDisplay == "" {
|
||||
if val, ok := result["name"].(string); ok {
|
||||
kfaDisplay = val
|
||||
}
|
||||
}
|
||||
if formCode == "" {
|
||||
if val, ok := result["form_code"].(string); ok {
|
||||
formCode = val
|
||||
}
|
||||
}
|
||||
if formDisplay == "" {
|
||||
if val, ok := result["form_display"].(string); ok {
|
||||
formDisplay = val
|
||||
}
|
||||
}
|
||||
|
||||
return kfaDisplay, formCode, formDisplay
|
||||
}
|
||||
|
||||
func (w *worker) updateTracker(lastID *int64, currentID int64) {
|
||||
*lastID = currentID
|
||||
os.MkdirAll("internal/worker/satusehat/medication", 0755)
|
||||
os.WriteFile(trackerFile, []byte(strconv.FormatInt(currentID, 10)), 0644)
|
||||
}
|
||||
|
||||
func (w *worker) readLastID() int64 {
|
||||
data, err := os.ReadFile(trackerFile)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
idStr := strings.TrimSpace(string(data))
|
||||
id, err := strconv.ParseInt(idStr, 10, 64)
|
||||
if err != nil {
|
||||
logger.Default().Warn("[MEDICATION WORKER] Gagal parsing ID dari tracker file, memulai dari awal.", logger.String("content", idStr), logger.ErrorField(err))
|
||||
return 0
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
func (w *worker) ensureTrackerFileExists() {
|
||||
if _, err := os.Stat(trackerFile); os.IsNotExist(err) {
|
||||
dir := filepath.Dir(trackerFile)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
logger.Default().Error("[MEDICATION WORKER] Gagal membuat direktori tracker", logger.ErrorField(err))
|
||||
}
|
||||
if err := os.WriteFile(trackerFile, []byte("0"), 0644); err != nil {
|
||||
logger.Default().Error("[MEDICATION WORKER] Gagal membuat file tracker default", logger.ErrorField(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *worker) saveMapping(idxPesanObat int64, fhirID string) {
|
||||
now := logger.LocalNow()
|
||||
monthDir := filepath.Join(mappingBaseDir, now.Format("2006-01"))
|
||||
if err := os.MkdirAll(monthDir, 0755); err != nil {
|
||||
logger.Default().Error("[MEDICATION WORKER] Gagal membuat direktori mapping bulanan", logger.ErrorField(err))
|
||||
return
|
||||
}
|
||||
|
||||
mappingPath := filepath.Join(monthDir, now.Format("2006-01-02")+".txt")
|
||||
f, err := os.OpenFile(mappingPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
logger.Default().Error("[MEDICATION WORKER] Gagal membuka file mapping", logger.ErrorField(err))
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
line := fmt.Sprintf("%d|%s\n", idxPesanObat, fhirID)
|
||||
if _, err := f.WriteString(line); err != nil {
|
||||
logger.Default().Error("[MEDICATION WORKER] Gagal menulis ke file mapping", logger.ErrorField(err))
|
||||
}
|
||||
}
|
||||
|
||||
func truncate(s string, max int) string {
|
||||
if len(s) > max {
|
||||
return s[:max] + "..."
|
||||
}
|
||||
return s
|
||||
}
|
||||
Reference in New Issue
Block a user