218 lines
5.7 KiB
Go
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()
|
|
}
|