Files
satusehat-worker/internal/master/kfa/repository.go
T

218 lines
5.7 KiB
Go

package kfa
import (
"context"
"encoding/json"
"reflect"
"strings"
"unicode"
"service/internal/infrastructure/database"
"service/pkg/utils/query"
"github.com/jmoiron/sqlx"
)
type CommandRepository interface {
UpsertProductBundle(ctx context.Context, product *KfaProduct, ingredients []KfaProductActiveIngredient, packagings []KfaProductPackaging) error
}
type commandRepository struct {
dbManager database.Service
connName string
}
func NewCommandRepository(dbManager database.Service, connName string) CommandRepository {
return &commandRepository{
dbManager: dbManager,
connName: connName,
}
}
func (r *commandRepository) UpsertProductBundle(ctx context.Context, product *KfaProduct, ingredients []KfaProductActiveIngredient, packagings []KfaProductPackaging) (err error) {
dbGorm, err := r.dbManager.GetGormDB(r.connName)
if err != nil {
return err
}
sqldb, err := dbGorm.DB()
if err != nil {
return err
}
dbx := sqlx.NewDb(sqldb, "postgres")
// Inisialisasi Query Builder (Nonaktifkan limit sekuritas untuk internal worker)
sqlBuilder := query.NewSQLQueryBuilder(query.DBTypePostgreSQL).
SetQueryLogging(false).
SetSecurityOptions(false, 0)
// Ekstrak semua kolom yang akan digunakan untuk mendaftarkannya ke whitelist Query Builder
productData, productUpdateCols := extractInsertData(product, "kfa_code")
var allowedCols []string
allowedCols = append(allowedCols, productData.Columns...)
if len(ingredients) > 0 {
ingData, _ := extractInsertData(ingredients[0], "")
allowedCols = append(allowedCols, ingData.Columns...)
}
if len(packagings) > 0 {
pkgData, _ := extractInsertData(packagings[0], "")
allowedCols = append(allowedCols, pkgData.Columns...)
}
allowedCols = append(allowedCols, "product_kfa_code", "kfa_code") // pastikan key relasi / conflict dimasukkan
// Daftarkan kolom agar terhindar dari error 'disallowed column'
sqlBuilder.SetAllowedColumns(allowedCols)
tx, err := dbx.BeginTxx(ctx, nil)
if err != nil {
return err
}
// Tangani Rollback jika terjadi kegagalan atau panic
defer func() {
if p := recover(); p != nil {
tx.Rollback()
panic(p)
} else if err != nil {
tx.Rollback()
}
}()
// 1. Upsert Induk Product (Manual Check & Update/Insert to avoid PG Constraint Error 42P10)
var count int
err = tx.GetContext(ctx, &count, "SELECT count(*) FROM master.kfa_products WHERE kfa_code = $1", product.KfaCode)
if err != nil {
return err
}
if count > 0 {
updateData := query.UpdateData{
Columns: productUpdateCols,
Values: make([]interface{}, len(productUpdateCols)),
}
for i, col := range productUpdateCols {
for j, insertCol := range productData.Columns {
if col == insertCol {
updateData.Values[i] = productData.Values[j]
break
}
}
}
_, err = sqlBuilder.ExecuteUpdate(ctx, tx, "master.kfa_products", updateData, []query.FilterGroup{{
Filters: []query.DynamicFilter{query.CreateEqualFilter("kfa_code", product.KfaCode)},
}})
} else {
_, err = sqlBuilder.ExecuteInsert(ctx, tx, "master.kfa_products", productData)
}
if err != nil {
return err
}
// 2. Hapus Active Ingredients Lama
delFilters := []query.FilterGroup{{
Filters: []query.DynamicFilter{
query.CreateEqualFilter("product_kfa_code", product.KfaCode),
},
}}
_, err = sqlBuilder.ExecuteDelete(ctx, tx, "master.kfa_product_active_ingredients", delFilters)
if err != nil {
return err
}
// 3. Insert Active Ingredients Baru
for _, ing := range ingredients {
ingData, _ := extractInsertData(ing, "")
_, err = sqlBuilder.ExecuteInsert(ctx, tx, "master.kfa_product_active_ingredients", ingData)
if err != nil {
return err
}
}
// 4. Hapus Packagings Lama
_, err = sqlBuilder.ExecuteDelete(ctx, tx, "master.kfa_product_packagings", delFilters)
if err != nil {
return err
}
// 5. Insert Packagings Baru
for _, pkg := range packagings {
pkgData, _ := extractInsertData(pkg, "")
_, err = sqlBuilder.ExecuteInsert(ctx, tx, "master.kfa_product_packagings", pkgData)
if err != nil {
return err
}
}
err = tx.Commit()
return err
}
// Helper: Ekstrak data model struct menjadi InsertData format untuk QueryBuilder
func extractInsertData(obj interface{}, conflictCol string) (query.InsertData, []string) {
val := reflect.ValueOf(obj)
if val.Kind() == reflect.Ptr {
val = val.Elem()
}
typ := val.Type()
var cols []string
var vals []interface{}
var updateCols []string
for i := 0; i < typ.NumField(); i++ {
field := typ.Field(i)
if field.PkgPath != "" { // Skip properti yang tidak diekspor
continue
}
colName := toSnakeCase(field.Name)
if colName == "id" { // ID selalu di-skip pada operasi insert karena dia auto increment
continue
}
fieldVal := val.Field(i).Interface()
cols = append(cols, colName)
if b, ok := fieldVal.(json.RawMessage); ok {
if len(b) == 0 || string(b) == "null" {
vals = append(vals, nil)
} else {
vals = append(vals, string(b))
}
} else {
vals = append(vals, fieldVal)
}
// Cek apa saja kolom yang valid di-update saat konflik (mengecualikan relasi/kode referensi)
if colName != conflictCol && colName != "created_at" && colName != "product_kfa_code" {
updateCols = append(updateCols, colName)
}
}
return query.InsertData{
Columns: cols,
Values: vals,
}, updateCols
}
// Helper: Ubah PascalCase field Go ke mode snake_case untuk tabel database
func toSnakeCase(s string) string {
if s == "ID" {
return "id"
}
var result strings.Builder
var prevUpper bool
for i, r := range s {
if unicode.IsUpper(r) {
if i > 0 && !prevUpper {
result.WriteByte('_')
}
result.WriteRune(unicode.ToLower(r))
prevUpper = true
} else {
result.WriteRune(r)
prevUpper = false
}
}
return result.String()
}