cities
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
teguh nugroho 2026-08-18 08:26:22 +07:00
parent fc0d140c5c
commit b372a99465
9 changed files with 623 additions and 20 deletions

View File

@ -0,0 +1,36 @@
package dto
type CreateCityRequest struct {
Name string `json:"name" binding:"required,min=2"`
ProvinceID string `json:"province_id" binding:"required"`
}
type CreateCityResponse struct {
ID string `json:"id"`
Name *string `json:"name"`
ProvinceID *string `json:"province_id"`
MerchantID *string `json:"merchant_id"`
}
type CityFilterAllRequest struct {
ID string `json:"id" form:"id"`
Name string `json:"name" form:"name"`
ProvinceID string `json:"province_id" form:"province_id"`
MerchantID string `json:"merchant_id" form:"merchant_id"`
}
type UpdateCityRequest struct {
Name string `json:"name" binding:"required,min=2"`
ProvinceID string `json:"province_id" binding:"required"`
}
type ImportCityResponse struct {
JobID string `json:"job_id,omitempty"`
TotalRows int `json:"total_rows"`
Imported int `json:"imported"`
Skipped int `json:"skipped"`
Logs []ImportLog `json:"logs"`
Errors []string `json:"errors"`
IsAsync bool `json:"is_async"`
Message string `json:"message,omitempty"`
}

View File

@ -2,6 +2,7 @@ package handlers
import ( import (
"cargo-erp-backend/internal/handlers/router/auth" "cargo-erp-backend/internal/handlers/router/auth"
"cargo-erp-backend/internal/handlers/router/city"
"cargo-erp-backend/internal/handlers/router/province" "cargo-erp-backend/internal/handlers/router/province"
"cargo-erp-backend/internal/middleware" "cargo-erp-backend/internal/middleware"
"cargo-erp-backend/pkg/helpers" "cargo-erp-backend/pkg/helpers"
@ -105,9 +106,7 @@ func (h *Handlers) Run() {
//province //province
{ {
province.NewProvinceHandler(h.Helper, masterdata).Router() province.NewProvinceHandler(h.Helper, masterdata).Router()
city.NewCityHandler(h.Helper, masterdata).Router()
// regency / city (jika nanti ada)
// regency.NewRegencyHandler(h.Helper, masterdata).Router()
} }
h.Helper.Log().Info("Starting Apps") h.Helper.Log().Info("Starting Apps")

View File

@ -0,0 +1,256 @@
package city
import (
"cargo-erp-backend/internal/handlers/dto"
cityimporter "cargo-erp-backend/internal/importers/city"
"cargo-erp-backend/internal/usecases"
"cargo-erp-backend/pkg/helpers"
"cargo-erp-backend/pkg/importer"
"cargo-erp-backend/pkg/response"
"net/http"
"github.com/gin-gonic/gin"
)
type CityHandlerInterface interface {
Router()
}
type CityHandler struct {
Helper helpers.HelperInterface
Group *gin.RouterGroup
}
func NewCityHandler(helper helpers.HelperInterface, g *gin.RouterGroup) CityHandlerInterface {
return &CityHandler{Helper: helper, Group: g}
}
func (h *CityHandler) Router() {
r := h.Group.Group("cities")
{
r.GET("/", h.GetAll)
r.POST("/list", h.GetList)
r.GET("/:id", h.GetByID)
r.POST("/", h.Create)
r.PUT("/:id", h.Update)
r.DELETE("/:id", h.Delete)
r.GET("/template", h.Template)
r.POST("/import", h.Import)
r.GET("/import/status/:jobId", h.ImportStatus)
}
}
func (h *CityHandler) GetAll(c *gin.Context) {
userid, err := h.Helper.GetAuthInfo(c.GetString("auth"))
if err != nil {
response.Error(c, http.StatusUnauthorized, err.Error())
return
}
var req dto.CityFilterAllRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u := usecases.NewCityUsecase(h.Helper, *userid)
cities, _, err := u.GetAll(req)
if err != nil {
response.Error(c, http.StatusInternalServerError, err.Error())
return
}
var responseDTO = make([]dto.CreateCityResponse, 0)
for _, v := range cities {
var DTO dto.CreateCityResponse
h.Helper.MapStructToStruct(v, "json", &DTO, "json")
responseDTO = append(responseDTO, DTO)
}
response.Success(c, responseDTO)
}
func (h *CityHandler) GetList(c *gin.Context) {
userid, err := h.Helper.GetAuthInfo(c.GetString("auth"))
if err != nil {
response.Error(c, http.StatusUnauthorized, err.Error())
return
}
var req dto.DataTableRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, "Request Format request invalid")
return
}
u := usecases.NewCityUsecase(h.Helper, *userid)
res, err := u.GetList(req)
if err != nil {
response.Error(c, http.StatusInternalServerError, err.Error())
return
}
response.Success(c, res)
return
}
func (h *CityHandler) GetByID(c *gin.Context) {
userid, err := h.Helper.GetAuthInfo(c.GetString("auth"))
if err != nil {
response.Error(c, http.StatusUnauthorized, err.Error())
return
}
id := c.Param("id")
u := usecases.NewCityUsecase(h.Helper, *userid)
city, err := u.GetByID(id)
if err != nil {
response.Error(c, http.StatusNotFound, "city not found")
return
}
response.Success(c, city)
}
func (h *CityHandler) Create(c *gin.Context) {
var req dto.CreateCityRequest
if err := c.BindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
userid, err := h.Helper.GetAuthInfo(c.GetString("auth"))
if err != nil {
response.Error(c, http.StatusUnauthorized, err.Error())
return
}
var merchantid *string = nil
if userid.UsersRole_Relation.RoleKey != "SPM" {
merchantid = userid.MerchantID
}
createdBy := userid.ID
u := usecases.NewCityUsecase(h.Helper, *userid)
city, err := u.Create(req.Name, req.ProvinceID, createdBy, merchantid)
if err != nil {
response.Error(c, http.StatusInternalServerError, err.Error())
return
}
var responseDTO dto.CreateCityResponse
h.Helper.MapStructToStruct(city, "json", &responseDTO, "json")
response.Created(c, responseDTO)
}
func (h *CityHandler) Update(c *gin.Context) {
id := c.Param("id")
var req dto.UpdateCityRequest
if err := c.BindJSON(&req); err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
userid, err := h.Helper.GetAuthInfo(c.GetString("auth"))
if err != nil {
response.Error(c, http.StatusUnauthorized, err.Error())
return
}
updatedBy := userid.ID
u := usecases.NewCityUsecase(h.Helper, *userid)
city, err := u.Update(id, req.Name, req.ProvinceID, updatedBy)
if err != nil {
response.Error(c, http.StatusInternalServerError, err.Error())
return
}
var responseDTO dto.CreateCityResponse
h.Helper.MapStructToStruct(city, "json", &responseDTO, "json")
response.Success(c, responseDTO)
}
func (h *CityHandler) Delete(c *gin.Context) {
userid, err := h.Helper.GetAuthInfo(c.GetString("auth"))
if err != nil {
response.Error(c, http.StatusUnauthorized, err.Error())
return
}
id := c.Param("id")
u := usecases.NewCityUsecase(h.Helper, *userid)
if err := u.Delete(id); err != nil {
response.Error(c, http.StatusInternalServerError, err.Error())
return
}
response.Success(c, map[string]interface{}{"message": "city deleted"})
}
func (h *CityHandler) Template(c *gin.Context) {
userid, err := h.Helper.GetAuthInfo(c.GetString("auth"))
if err != nil {
response.Error(c, http.StatusUnauthorized, err.Error())
return
}
handler := cityimporter.NewCityImportHandler(*userid, h.Helper)
imp := importer.NewImporter(h.Helper, handler)
imp.ServeTemplate(c.Writer)
}
func (h *CityHandler) Import(c *gin.Context) {
userid, err := h.Helper.GetAuthInfo(c.GetString("auth"))
if err != nil {
response.Error(c, http.StatusUnauthorized, err.Error())
return
}
file, header, err := c.Request.FormFile("file")
if err != nil {
response.Error(c, http.StatusBadRequest, "File is required")
return
}
defer file.Close()
handler := cityimporter.NewCityImportHandler(*userid, h.Helper)
imp := importer.NewImporter(h.Helper, handler)
result, err := imp.Process(file, userid.ID, header.Filename)
if err != nil {
response.Error(c, http.StatusBadRequest, err.Error())
return
}
var importLogs []dto.ImportLog
for _, l := range result.Logs {
importLogs = append(importLogs, dto.ImportLog{
Row: l.Row,
Status: l.Status,
Data: l.Data,
Message: l.Message,
})
}
response.Success(c, dto.ImportCityResponse{
JobID: result.JobID,
TotalRows: result.TotalRows,
Imported: result.Imported,
Skipped: result.Skipped,
Logs: importLogs,
Errors: result.Errors,
IsAsync: result.IsAsync,
Message: result.Message,
})
}
func (h *CityHandler) ImportStatus(c *gin.Context) {
userid, err := h.Helper.GetAuthInfo(c.GetString("auth"))
if err != nil {
response.Error(c, http.StatusUnauthorized, err.Error())
return
}
jobId := c.Param("jobId")
handler := cityimporter.NewCityImportHandler(*userid, h.Helper)
imp := importer.NewImporter(h.Helper, handler)
job, logs, err := imp.GetJobStatus(jobId)
if err != nil {
response.Error(c, http.StatusNotFound, "Job not found")
return
}
response.Success(c, map[string]interface{}{
"job": job,
"logs": logs,
})
}

View File

@ -0,0 +1,108 @@
package city
import (
"cargo-erp-backend/internal/domain"
"cargo-erp-backend/pkg/helpers"
"cargo-erp-backend/pkg/importer"
"fmt"
"strings"
"time"
"gorm.io/gorm"
)
type CityImportHandler struct {
UserData domain.User
Helper helpers.HelperInterface
lastProvinceName string
}
func NewCityImportHandler(user_data domain.User, helper helpers.HelperInterface) importer.ImportHandler {
return &CityImportHandler{
UserData: user_data,
Helper: helper,
}
}
func (h *CityImportHandler) ValidateRow(row []string, rowNumber int) (importer.RowData, []error) {
var errors []error
if len(row) < 2 {
errors = append(errors, fmt.Errorf("row must have 2 columns: name, province_name"))
return nil, errors
}
name := strings.TrimSpace(row[0])
if name == "" {
errors = append(errors, fmt.Errorf("name is required"))
} else if len(name) < 2 {
errors = append(errors, fmt.Errorf("name must be at least 2 characters"))
} else if len(name) > 255 {
errors = append(errors, fmt.Errorf("name must be at most 255 characters"))
}
provinceName := strings.TrimSpace(row[1])
if provinceName == "" {
errors = append(errors, fmt.Errorf("province_name is required"))
}
if len(errors) > 0 {
return nil, errors
}
var province domain.Province
db := h.Helper.GetDB("slave")
if err := db.Where("UPPER(name) = ?", strings.ToUpper(provinceName)).First(&province).Error; err != nil {
errors = append(errors, fmt.Errorf("province '%s' not found", provinceName))
return nil, errors
}
h.lastProvinceName = provinceName
now := time.Now()
provinceID := province.ID
city := &domain.City{
Name: &name,
ProvinceID: &provinceID,
MerchantID: h.UserData.MerchantID,
CreatedBy: &h.UserData.ID,
CreatedOn: &now,
UpdatedBy: &h.UserData.ID,
UpdatedOn: &now,
}
return city, nil
}
func (h *CityImportHandler) GetDataString(data importer.RowData) string {
city := data.(*domain.City)
name := ""
if city.Name != nil {
name = *city.Name
}
return fmt.Sprintf("%s, %s", name, h.lastProvinceName)
}
func (h *CityImportHandler) IsDuplicate(db *gorm.DB, data importer.RowData) bool {
city := data.(*domain.City)
var count int64
db.Model(&domain.City{}).
Joins("JOIN province ON city.province_id = province.id").
Where("UPPER(city.name) = ? AND UPPER(province.name) = ?",
strings.ToUpper(*city.Name), strings.ToUpper(h.lastProvinceName)).
Count(&count)
return count > 0
}
func (h *CityImportHandler) GetTemplateHeaders() []string {
return []string{"name", "province_name"}
}
func (h *CityImportHandler) GetTemplateRows() [][]string {
return [][]string{
{"Jakarta Pusat", "DKI Jakarta"},
{"Bandung", "Jawa Barat"},
{"Surabaya", "Jawa Timur"},
{"Semarang", "Jawa Tengah"},
}
}

149
internal/usecases/city.go Normal file
View File

@ -0,0 +1,149 @@
package usecases
import (
"cargo-erp-backend/internal/domain"
"cargo-erp-backend/internal/handlers/dto"
"cargo-erp-backend/pkg/datatable"
"cargo-erp-backend/pkg/helpers"
"fmt"
"reflect"
"time"
)
type CityUsecaseInterface interface {
GetAll(filter dto.CityFilterAllRequest) ([]domain.City, int64, error)
GetList(dto.DataTableRequest) (dto.DataTableResponse, error)
GetByID(id string) (domain.City, error)
Create(name string, provinceID string, createdBy string, merchantid *string) (domain.City, error)
Update(id string, name string, provinceID string, updatedBy string) (domain.City, error)
Delete(id string) error
}
type CityUsecase struct {
Helper helpers.HelperInterface
AuthUser domain.User
}
func NewCityUsecase(helper helpers.HelperInterface, authUser domain.User) CityUsecaseInterface {
return &CityUsecase{Helper: helper, AuthUser: authUser}
}
func (u *CityUsecase) GetAll(filter dto.CityFilterAllRequest) ([]domain.City, int64, error) {
var cities []domain.City
var total int64
db := u.Helper.GetDB("slave")
tx := db.Model(&cities)
if u.AuthUser.UsersRole_Relation.RoleKey != "SPM" {
tx.Where(db.Where("merchant_id IS NULL").Or("merchant_id=?", u.AuthUser.MerchantID))
}
v := reflect.ValueOf(filter)
t := reflect.TypeOf(filter)
for i := 0; i < v.NumField(); i++ {
if v.Field(i).String() != "" {
tagKey := t.Field(i).Tag.Get("form")
tx.Where(fmt.Sprintf("%v=?", tagKey), v.Field(i))
}
}
tx.Model(&domain.City{}).Count(&total)
tx.Order("name ASC")
if err := tx.Find(&cities).Error; err != nil {
return nil, 0, err
}
return cities, total, nil
}
func (u *CityUsecase) GetList(req dto.DataTableRequest) (dto.DataTableResponse, error) {
var response dto.DataTableResponse
db := u.Helper.GetDB("slave")
tx := db.Table("city").
Joins("INNER JOIN province ON city.province_id=province.id").
Joins("LEFT JOIN merchant ON city.merchant_id=merchant.id")
dt := datatable.NewDatatable(tx, req)
coldef := make([]dto.DataTableColDef, 0)
coldef = append(coldef, dto.DataTableColDef{
Field: "city.name",
Alias: "name",
})
coldef = append(coldef, dto.DataTableColDef{
Field: "province.name",
Alias: "province_name",
})
coldef = append(coldef, dto.DataTableColDef{
Field: "city.province_id",
Alias: "province_id",
})
coldef = append(coldef, dto.DataTableColDef{
Field: "merchant.id",
Alias: "merchant_id",
})
coldef = append(coldef, dto.DataTableColDef{
Field: "city.id",
Alias: "id",
})
response = dt.Render(coldef)
return response, nil
}
func (u *CityUsecase) GetByID(id string) (domain.City, error) {
var city domain.City
db := u.Helper.GetDB("slave")
if err := db.Where("id = ?", id).First(&city).Error; err != nil {
return domain.City{}, err
}
return city, nil
}
func (u *CityUsecase) Create(name string, provinceID string, createdBy string, merchantid *string) (domain.City, error) {
now := time.Now()
city := domain.City{
Name: &name,
ProvinceID: &provinceID,
MerchantID: merchantid,
CreatedBy: &createdBy,
CreatedOn: &now,
UpdatedBy: &createdBy,
UpdatedOn: &now,
}
db := u.Helper.GetDB("master")
if err := db.Create(&city).Error; err != nil {
return domain.City{}, err
}
return city, nil
}
func (u *CityUsecase) Update(id string, name string, provinceID string, updatedBy string) (domain.City, error) {
var city domain.City
db := u.Helper.GetDB("master")
if u.AuthUser.UsersRole_Relation.RoleKey != "SPM" {
db.Where("merchant_id=?", u.AuthUser.MerchantID)
}
if err := db.Where("id = ?", id).First(&city).Error; err != nil {
return domain.City{}, err
}
now := time.Now()
city.Name = &name
city.ProvinceID = &provinceID
city.UpdatedBy = &updatedBy
city.UpdatedOn = &now
if err := db.Save(&city).Error; err != nil {
return domain.City{}, err
}
return city, nil
}
func (u *CityUsecase) Delete(id string) error {
db := u.Helper.GetDB("master")
if u.AuthUser.UsersRole_Relation.RoleKey != "SPM" {
db.Where("merchant_id=?", u.AuthUser.MerchantID)
}
if err := db.Delete(&domain.City{}, "id = ?", id).Error; err != nil {
return err
}
return nil
}

View File

@ -128,6 +128,7 @@ func (i *Importer) processSync(file io.Reader, userID string, totalRows int) (*I
var logs []ImportLog var logs []ImportLog
var errors []string var errors []string
var batch []RowData var batch []RowData
var batchRows []int
batchSize := 500 batchSize := 500
csvlib.ReadCSV(file, func(row []string, rowNumber int) error { csvlib.ReadCSV(file, func(row []string, rowNumber int) error {
@ -160,6 +161,7 @@ func (i *Importer) processSync(file io.Reader, userID string, totalRows int) (*I
} }
batch = append(batch, data) batch = append(batch, data)
batchRows = append(batchRows, rowNumber)
if len(batch) >= batchSize { if len(batch) >= batchSize {
if err := i.insertBatch(db, batch); err != nil { if err := i.insertBatch(db, batch); err != nil {
errorMsg := fmt.Sprintf("Batch insert error: %v", err) errorMsg := fmt.Sprintf("Batch insert error: %v", err)
@ -171,18 +173,19 @@ func (i *Importer) processSync(file io.Reader, userID string, totalRows int) (*I
Message: errorMsg, Message: errorMsg,
}) })
} else { } else {
for _, b := range batch { for idx, b := range batch {
dataStr := i.Handler.GetDataString(b) dataStr := i.Handler.GetDataString(b)
logs = append(logs, ImportLog{ logs = append(logs, ImportLog{
Row: rowNumber, Row: batchRows[idx],
Status: "inserted", Status: "inserted",
Data: dataStr, Data: dataStr,
Message: fmt.Sprintf("Row %d: '%s' inserted successfully", rowNumber, dataStr), Message: fmt.Sprintf("Row %d: '%s' inserted successfully", batchRows[idx], dataStr),
}) })
} }
imported += len(batch) imported += len(batch)
} }
batch = nil batch = nil
batchRows = nil
} }
return nil return nil
}) })
@ -192,19 +195,19 @@ func (i *Importer) processSync(file io.Reader, userID string, totalRows int) (*I
errorMsg := fmt.Sprintf("Batch insert error: %v", err) errorMsg := fmt.Sprintf("Batch insert error: %v", err)
errors = append(errors, errorMsg) errors = append(errors, errorMsg)
logs = append(logs, ImportLog{ logs = append(logs, ImportLog{
Row: totalRows + 1, Row: batchRows[0],
Status: "error", Status: "error",
Data: "", Data: "",
Message: errorMsg, Message: errorMsg,
}) })
} else { } else {
for _, b := range batch { for idx, b := range batch {
dataStr := i.Handler.GetDataString(b) dataStr := i.Handler.GetDataString(b)
logs = append(logs, ImportLog{ logs = append(logs, ImportLog{
Row: imported + 1, Row: batchRows[idx],
Status: "inserted", Status: "inserted",
Data: dataStr, Data: dataStr,
Message: fmt.Sprintf("'%s' inserted successfully", dataStr), Message: fmt.Sprintf("Row %d: '%s' inserted successfully", batchRows[idx], dataStr),
}) })
} }
imported += len(batch) imported += len(batch)
@ -252,6 +255,7 @@ func (i *Importer) processAsyncWorker(file io.Reader, jobID string, userID strin
var logs []ImportLog var logs []ImportLog
var errors []string var errors []string
var batch []RowData var batch []RowData
var batchRows []int
batchSize := 500 batchSize := 500
i.JobQ.UpdateProgress(ctx, jobID, 0, 0, nil) i.JobQ.UpdateProgress(ctx, jobID, 0, 0, nil)
@ -286,6 +290,7 @@ func (i *Importer) processAsyncWorker(file io.Reader, jobID string, userID strin
} }
batch = append(batch, data) batch = append(batch, data)
batchRows = append(batchRows, rowNumber)
if len(batch) >= batchSize { if len(batch) >= batchSize {
if err := i.insertBatch(db, batch); err != nil { if err := i.insertBatch(db, batch); err != nil {
errorMsg := fmt.Sprintf("Batch insert error: %v", err) errorMsg := fmt.Sprintf("Batch insert error: %v", err)
@ -297,18 +302,19 @@ func (i *Importer) processAsyncWorker(file io.Reader, jobID string, userID strin
Message: errorMsg, Message: errorMsg,
}) })
} else { } else {
for _, b := range batch { for idx, b := range batch {
dataStr := i.Handler.GetDataString(b) dataStr := i.Handler.GetDataString(b)
logs = append(logs, ImportLog{ logs = append(logs, ImportLog{
Row: rowNumber, Row: batchRows[idx],
Status: "inserted", Status: "inserted",
Data: dataStr, Data: dataStr,
Message: fmt.Sprintf("Row %d: '%s' inserted successfully", rowNumber, dataStr), Message: fmt.Sprintf("Row %d: '%s' inserted successfully", batchRows[idx], dataStr),
}) })
} }
imported += len(batch) imported += len(batch)
} }
batch = nil batch = nil
batchRows = nil
i.saveJobLogs(ctx, jobID, logs, imported, skipped, errors) i.saveJobLogs(ctx, jobID, logs, imported, skipped, errors)
} }
return nil return nil
@ -319,19 +325,19 @@ func (i *Importer) processAsyncWorker(file io.Reader, jobID string, userID strin
errorMsg := fmt.Sprintf("Batch insert error: %v", err) errorMsg := fmt.Sprintf("Batch insert error: %v", err)
errors = append(errors, errorMsg) errors = append(errors, errorMsg)
logs = append(logs, ImportLog{ logs = append(logs, ImportLog{
Row: totalRows + 1, Row: batchRows[0],
Status: "error", Status: "error",
Data: "", Data: "",
Message: errorMsg, Message: errorMsg,
}) })
} else { } else {
for _, b := range batch { for idx, b := range batch {
dataStr := i.Handler.GetDataString(b) dataStr := i.Handler.GetDataString(b)
logs = append(logs, ImportLog{ logs = append(logs, ImportLog{
Row: imported + 1, Row: batchRows[idx],
Status: "inserted", Status: "inserted",
Data: dataStr, Data: dataStr,
Message: fmt.Sprintf("'%s' inserted successfully", dataStr), Message: fmt.Sprintf("Row %d: '%s' inserted successfully", batchRows[idx], dataStr),
}) })
} }
imported += len(batch) imported += len(batch)

View File

@ -190,14 +190,15 @@ func joinErrors(errors []string) string {
func splitErrors(s string) []string { func splitErrors(s string) []string {
result := []string{} result := []string{}
current := "" current := ""
for _, c := range s { for i := 0; i < len(s); i++ {
if c == '|' { if i+1 < len(s) && s[i] == '|' && s[i+1] == '|' {
if current != "" { if current != "" {
result = append(result, current) result = append(result, current)
} }
current = "" current = ""
i++ // skip second |
} else { } else {
current += string(c) current += string(s[i])
} }
} }
if current != "" { if current != "" {

View File

@ -801,3 +801,27 @@
{"level":"info","ts":"2026-08-17T18:39:33.467+0700","caller":"database/database.go:76","msg":"Connect To Slave DB"} {"level":"info","ts":"2026-08-17T18:39:33.467+0700","caller":"database/database.go:76","msg":"Connect To Slave DB"}
{"level":"info","ts":"2026-08-17T18:39:33.514+0700","caller":"database/database.go:86","msg":"DB Connection Done"} {"level":"info","ts":"2026-08-17T18:39:33.514+0700","caller":"database/database.go:86","msg":"DB Connection Done"}
{"level":"info","ts":"2026-08-17T18:39:33.609+0700","caller":"handlers/handlers.go:113","msg":"Starting Apps"} {"level":"info","ts":"2026-08-17T18:39:33.609+0700","caller":"handlers/handlers.go:113","msg":"Starting Apps"}
{"level":"info","ts":"2026-08-18T06:54:36.198+0700","caller":"database/database.go:53","msg":"Connect To Master DB"}
{"level":"info","ts":"2026-08-18T06:54:36.328+0700","caller":"database/database.go:63","msg":"DB Master Connected"}
{"level":"info","ts":"2026-08-18T06:54:36.329+0700","caller":"database/database.go:76","msg":"Connect To Slave DB"}
{"level":"info","ts":"2026-08-18T06:54:36.387+0700","caller":"database/database.go:86","msg":"DB Connection Done"}
{"level":"info","ts":"2026-08-18T06:54:36.481+0700","caller":"handlers/handlers.go:112","msg":"Starting Apps"}
{"level":"warn","ts":"2026-08-18T06:54:38.609+0700","caller":"handlers/handlers.go:65","msg":"Client Error","status":401,"method":"POST","path":"/master-data/cities/list","query":"","ip":"127.0.0.1","latency":0.000351667,"user-agent":"Apidog/1.0.0 (https://apidog.com)"}
{"level":"info","ts":"2026-08-18T06:54:45.243+0700","caller":"handlers/handlers.go:67","msg":"Request Success","status":200,"method":"POST","path":"/login","query":"","ip":"127.0.0.1","latency":0.220723458,"user-agent":"Apidog/1.0.0 (https://apidog.com)"}
{"level":"info","ts":"2026-08-18T06:54:49.054+0700","caller":"middleware/auth.go:62","msg":"session_login : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIxOWZmMTA0YzE2NTIyYTIyNTJjYmFjYTI0ZCIsImlzcyI6ImNhcmdvLXBsYXRmb3JtIiwic3ViIjoiMTlmZjEwNGMxNjUyMmEyMjUyY2JhY2EyNGQiLCJleHAiOjE3ODcwMTQ0ODUsImlhdCI6MTc4NzAxMDg4NX0.--uTG5-sGG0BY1fOCZ5zcWhhRH9OSunYWMiSXNg8Sn8"}
{"level":"info","ts":"2026-08-18T06:54:49.078+0700","caller":"middleware/auth.go:90","msg":"Set Auth : {\"blocked\":false,\"blocked_notes\":null,\"citys_relation\":null,\"created_by\":null,\"created_on\":\"2026-08-11T20:30:48.554654Z\",\"districts_relation\":null,\"email\":\"gcx@gmail.com\",\"id\":\"19ff104c16522a2252cbaca24d\",\"is_active\":true,\"merchant_id\":\"19ff111bca6c2c382d9eb43b3c\",\"merchant_relation\":null,\"merchants_relation\":null,\"nick_name\":\"Administrator\",\"postal_codes_relation\":null,\"provinces_relation\":null,\"subdistricts_relation\":null,\"updated_by\":null,\"updated_on\":\"2026-08-11T20:30:48.554654Z\",\"users_role_id\":\"19fe814c5350d67814c3e6057c\",\"users_role_relation\":{\"created_by\":null,\"created_on\":\"2026-08-10T02:51:43.16495Z\",\"id\":\"19fe814c5350d67814c3e6057c\",\"merchant_id\":null,\"merchant_relation\":{\"active\":null,\"additional_information\":null,\"address\":null,\"city_id\":null,\"city_relation\":null,\"citys_relation\":null,\"created_by\":null,\"created_by_relation\":null,\"created_on\":null,\"district_id\":null,\"district_relation\":null,\"districts_relation\":null,\"email\":null,\"id\":\"\",\"modas_relation\":null,\"name\":null,\"package_types_relation\":null,\"postal_code\":null,\"postal_codes_relation\":null,\"province_id\":null,\"province_relation\":null,\"provinces_relation\":null,\"subdistrict_id\":null,\"subdistrict_relation\":null,\"subdistricts_relation\":null,\"telp\":null,\"updated_by\":null,\"updated_on\":null,\"users_roless_relation\":null,\"userss_relation\":null},\"name\":\"Administrator\",\"role_key\":\"ADM_MERCHANT\",\"updated_by\":null,\"updated_on\":\"2026-08-10T02:51:43.16495Z\",\"userss_relation\":null}}"}
{"level":"info","ts":"2026-08-18T06:54:49.178+0700","caller":"handlers/handlers.go:67","msg":"Request Success","status":200,"method":"POST","path":"/master-data/cities/list","query":"","ip":"127.0.0.1","latency":0.124817375,"user-agent":"Apidog/1.0.0 (https://apidog.com)"}
{"level":"info","ts":"2026-08-18T06:58:39.610+0700","caller":"database/database.go:53","msg":"Connect To Master DB"}
{"level":"info","ts":"2026-08-18T06:58:39.818+0700","caller":"database/database.go:63","msg":"DB Master Connected"}
{"level":"info","ts":"2026-08-18T06:58:39.818+0700","caller":"database/database.go:76","msg":"Connect To Slave DB"}
{"level":"info","ts":"2026-08-18T06:58:39.875+0700","caller":"database/database.go:86","msg":"DB Connection Done"}
{"level":"info","ts":"2026-08-18T06:58:39.970+0700","caller":"handlers/handlers.go:112","msg":"Starting Apps"}
{"level":"info","ts":"2026-08-18T06:58:46.340+0700","caller":"middleware/auth.go:62","msg":"session_login : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIxOWZmMTA0YzE2NTIyYTIyNTJjYmFjYTI0ZCIsImlzcyI6ImNhcmdvLXBsYXRmb3JtIiwic3ViIjoiMTlmZjEwNGMxNjUyMmEyMjUyY2JhY2EyNGQiLCJleHAiOjE3ODcwMTQ0ODUsImlhdCI6MTc4NzAxMDg4NX0.--uTG5-sGG0BY1fOCZ5zcWhhRH9OSunYWMiSXNg8Sn8"}
{"level":"info","ts":"2026-08-18T06:58:46.364+0700","caller":"middleware/auth.go:90","msg":"Set Auth : {\"blocked\":false,\"blocked_notes\":null,\"citys_relation\":null,\"created_by\":null,\"created_on\":\"2026-08-11T20:30:48.554654Z\",\"districts_relation\":null,\"email\":\"gcx@gmail.com\",\"id\":\"19ff104c16522a2252cbaca24d\",\"is_active\":true,\"merchant_id\":\"19ff111bca6c2c382d9eb43b3c\",\"merchant_relation\":null,\"merchants_relation\":null,\"nick_name\":\"Administrator\",\"postal_codes_relation\":null,\"provinces_relation\":null,\"subdistricts_relation\":null,\"updated_by\":null,\"updated_on\":\"2026-08-11T20:30:48.554654Z\",\"users_role_id\":\"19fe814c5350d67814c3e6057c\",\"users_role_relation\":{\"created_by\":null,\"created_on\":\"2026-08-10T02:51:43.16495Z\",\"id\":\"19fe814c5350d67814c3e6057c\",\"merchant_id\":null,\"merchant_relation\":{\"active\":null,\"additional_information\":null,\"address\":null,\"city_id\":null,\"city_relation\":null,\"citys_relation\":null,\"created_by\":null,\"created_by_relation\":null,\"created_on\":null,\"district_id\":null,\"district_relation\":null,\"districts_relation\":null,\"email\":null,\"id\":\"\",\"modas_relation\":null,\"name\":null,\"package_types_relation\":null,\"postal_code\":null,\"postal_codes_relation\":null,\"province_id\":null,\"province_relation\":null,\"provinces_relation\":null,\"subdistrict_id\":null,\"subdistrict_relation\":null,\"subdistricts_relation\":null,\"telp\":null,\"updated_by\":null,\"updated_on\":null,\"users_roless_relation\":null,\"userss_relation\":null},\"name\":\"Administrator\",\"role_key\":\"ADM_MERCHANT\",\"updated_by\":null,\"updated_on\":\"2026-08-10T02:51:43.16495Z\",\"userss_relation\":null}}"}
{"level":"info","ts":"2026-08-18T06:58:46.434+0700","caller":"handlers/handlers.go:67","msg":"Request Success","status":200,"method":"POST","path":"/master-data/cities/list","query":"","ip":"127.0.0.1","latency":0.098011125,"user-agent":"Apidog/1.0.0 (https://apidog.com)"}
{"level":"info","ts":"2026-08-18T07:00:26.298+0700","caller":"middleware/auth.go:62","msg":"session_login : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIxOWZmMTA0YzE2NTIyYTIyNTJjYmFjYTI0ZCIsImlzcyI6ImNhcmdvLXBsYXRmb3JtIiwic3ViIjoiMTlmZjEwNGMxNjUyMmEyMjUyY2JhY2EyNGQiLCJleHAiOjE3ODcwMTQ0ODUsImlhdCI6MTc4NzAxMDg4NX0.--uTG5-sGG0BY1fOCZ5zcWhhRH9OSunYWMiSXNg8Sn8"}
{"level":"info","ts":"2026-08-18T07:00:26.325+0700","caller":"middleware/auth.go:90","msg":"Set Auth : {\"blocked\":false,\"blocked_notes\":null,\"citys_relation\":null,\"created_by\":null,\"created_on\":\"2026-08-11T20:30:48.554654Z\",\"districts_relation\":null,\"email\":\"gcx@gmail.com\",\"id\":\"19ff104c16522a2252cbaca24d\",\"is_active\":true,\"merchant_id\":\"19ff111bca6c2c382d9eb43b3c\",\"merchant_relation\":null,\"merchants_relation\":null,\"nick_name\":\"Administrator\",\"postal_codes_relation\":null,\"provinces_relation\":null,\"subdistricts_relation\":null,\"updated_by\":null,\"updated_on\":\"2026-08-11T20:30:48.554654Z\",\"users_role_id\":\"19fe814c5350d67814c3e6057c\",\"users_role_relation\":{\"created_by\":null,\"created_on\":\"2026-08-10T02:51:43.16495Z\",\"id\":\"19fe814c5350d67814c3e6057c\",\"merchant_id\":null,\"merchant_relation\":{\"active\":null,\"additional_information\":null,\"address\":null,\"city_id\":null,\"city_relation\":null,\"citys_relation\":null,\"created_by\":null,\"created_by_relation\":null,\"created_on\":null,\"district_id\":null,\"district_relation\":null,\"districts_relation\":null,\"email\":null,\"id\":\"\",\"modas_relation\":null,\"name\":null,\"package_types_relation\":null,\"postal_code\":null,\"postal_codes_relation\":null,\"province_id\":null,\"province_relation\":null,\"provinces_relation\":null,\"subdistrict_id\":null,\"subdistrict_relation\":null,\"subdistricts_relation\":null,\"telp\":null,\"updated_by\":null,\"updated_on\":null,\"users_roless_relation\":null,\"userss_relation\":null},\"name\":\"Administrator\",\"role_key\":\"ADM_MERCHANT\",\"updated_by\":null,\"updated_on\":\"2026-08-10T02:51:43.16495Z\",\"userss_relation\":null}}"}
{"level":"info","ts":"2026-08-18T07:00:26.439+0700","caller":"handlers/handlers.go:67","msg":"Request Success","status":200,"method":"GET","path":"/master-data/cities/","query":"name=ACEH","ip":"127.0.0.1","latency":0.139473917,"user-agent":"Apidog/1.0.0 (https://apidog.com)"}
{"level":"info","ts":"2026-08-18T07:00:59.066+0700","caller":"middleware/auth.go:62","msg":"session_login : eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOiIxOWZmMTA0YzE2NTIyYTIyNTJjYmFjYTI0ZCIsImlzcyI6ImNhcmdvLXBsYXRmb3JtIiwic3ViIjoiMTlmZjEwNGMxNjUyMmEyMjUyY2JhY2EyNGQiLCJleHAiOjE3ODcwMTQ0ODUsImlhdCI6MTc4NzAxMDg4NX0.--uTG5-sGG0BY1fOCZ5zcWhhRH9OSunYWMiSXNg8Sn8"}
{"level":"info","ts":"2026-08-18T07:00:59.091+0700","caller":"middleware/auth.go:90","msg":"Set Auth : {\"blocked\":false,\"blocked_notes\":null,\"citys_relation\":null,\"created_by\":null,\"created_on\":\"2026-08-11T20:30:48.554654Z\",\"districts_relation\":null,\"email\":\"gcx@gmail.com\",\"id\":\"19ff104c16522a2252cbaca24d\",\"is_active\":true,\"merchant_id\":\"19ff111bca6c2c382d9eb43b3c\",\"merchant_relation\":null,\"merchants_relation\":null,\"nick_name\":\"Administrator\",\"postal_codes_relation\":null,\"provinces_relation\":null,\"subdistricts_relation\":null,\"updated_by\":null,\"updated_on\":\"2026-08-11T20:30:48.554654Z\",\"users_role_id\":\"19fe814c5350d67814c3e6057c\",\"users_role_relation\":{\"created_by\":null,\"created_on\":\"2026-08-10T02:51:43.16495Z\",\"id\":\"19fe814c5350d67814c3e6057c\",\"merchant_id\":null,\"merchant_relation\":{\"active\":null,\"additional_information\":null,\"address\":null,\"city_id\":null,\"city_relation\":null,\"citys_relation\":null,\"created_by\":null,\"created_by_relation\":null,\"created_on\":null,\"district_id\":null,\"district_relation\":null,\"districts_relation\":null,\"email\":null,\"id\":\"\",\"modas_relation\":null,\"name\":null,\"package_types_relation\":null,\"postal_code\":null,\"postal_codes_relation\":null,\"province_id\":null,\"province_relation\":null,\"provinces_relation\":null,\"subdistrict_id\":null,\"subdistrict_relation\":null,\"subdistricts_relation\":null,\"telp\":null,\"updated_by\":null,\"updated_on\":null,\"users_roless_relation\":null,\"userss_relation\":null},\"name\":\"Administrator\",\"role_key\":\"ADM_MERCHANT\",\"updated_by\":null,\"updated_on\":\"2026-08-10T02:51:43.16495Z\",\"userss_relation\":null}}"}
{"level":"info","ts":"2026-08-18T07:00:59.204+0700","caller":"handlers/handlers.go:67","msg":"Request Success","status":200,"method":"GET","path":"/master-data/cities/","query":"","ip":"127.0.0.1","latency":0.138777583,"user-agent":"Apidog/1.0.0 (https://apidog.com)"}

View File

@ -3463,3 +3463,27 @@
{"level":"info","ts":"2026-08-17T18:39:33.550+0700","caller":"database/redis.go:34","msg":"Parse config slave"} {"level":"info","ts":"2026-08-17T18:39:33.550+0700","caller":"database/redis.go:34","msg":"Parse config slave"}
{"level":"info","ts":"2026-08-17T18:39:33.551+0700","caller":"database/redis.go:49","msg":"Connecting to Redis slave"} {"level":"info","ts":"2026-08-17T18:39:33.551+0700","caller":"database/redis.go:49","msg":"Connecting to Redis slave"}
{"level":"info","ts":"2026-08-17T18:39:33.584+0700","caller":"database/redis.go:62","msg":"Connected"} {"level":"info","ts":"2026-08-17T18:39:33.584+0700","caller":"database/redis.go:62","msg":"Connected"}
{"level":"info","ts":"2026-08-18T06:54:36.388+0700","caller":"database/redis.go:34","msg":"Parse config slave"}
{"level":"info","ts":"2026-08-18T06:54:36.388+0700","caller":"database/redis.go:49","msg":"Connecting to Redis slave"}
{"level":"info","ts":"2026-08-18T06:54:36.438+0700","caller":"database/redis.go:62","msg":"Connected"}
{"level":"info","ts":"2026-08-18T06:54:36.438+0700","caller":"database/redis.go:34","msg":"Parse config master"}
{"level":"info","ts":"2026-08-18T06:54:36.438+0700","caller":"database/redis.go:49","msg":"Connecting to Redis master"}
{"level":"info","ts":"2026-08-18T06:54:36.481+0700","caller":"database/redis.go:62","msg":"Connected"}
{"level":"info","ts":"2026-08-18T06:54:45.113+0700","caller":"logger/database.go:90","msg":"[GORM] query execution","line":"/Users/teguh/Documents/project/cargo/cargo-erp-deploy/app/backend/internal/usecases/auth.go:29","elapsed":0.024513209,"rows":1,"sql":"SELECT * FROM \"users_roles\" WHERE \"users_roles\".\"id\" = '19fe814c5350d67814c3e6057c'"}
{"level":"info","ts":"2026-08-18T06:54:45.113+0700","caller":"logger/database.go:90","msg":"[GORM] query execution","line":"/Users/teguh/Documents/project/cargo/cargo-erp-deploy/app/backend/internal/usecases/auth.go:29","elapsed":0.087087208,"rows":1,"sql":"SELECT * FROM \"users\" WHERE email='gcx@gmail.com'"}
{"level":"info","ts":"2026-08-18T06:54:49.144+0700","caller":"logger/database.go:90","msg":"[GORM] query execution","line":"/Users/teguh/Documents/project/cargo/cargo-erp-deploy/app/backend/pkg/datatable/datatable.go:30","elapsed":0.063523875,"rows":1,"sql":"SELECT count(*) FROM \"city\" LEFT JOIN province ON city.province_id=province.id LEFT JOIN merchant ON city.merchant_id=merchant.id"}
{"level":"info","ts":"2026-08-18T06:54:49.153+0700","caller":"logger/database.go:90","msg":"[GORM] query execution","line":"/Users/teguh/Documents/project/cargo/cargo-erp-deploy/app/backend/pkg/datatable/datatable.go:65","elapsed":0.009042667,"rows":1,"sql":"SELECT count(*) FROM \"city\" LEFT JOIN province ON city.province_id=province.id LEFT JOIN merchant ON city.merchant_id=merchant.id"}
{"level":"info","ts":"2026-08-18T06:54:49.178+0700","caller":"logger/database.go:90","msg":"[GORM] query execution","line":"/Users/teguh/Documents/project/cargo/cargo-erp-deploy/app/backend/pkg/datatable/datatable.go:73","elapsed":0.024878667,"rows":10,"sql":"SELECT city.name AS name,province.name AS province_name,city.province_id AS province_id,merchant.id AS merchant_id,city.id AS id FROM \"city\" LEFT JOIN province ON city.province_id=province.id LEFT JOIN merchant ON city.merchant_id=merchant.id ORDER BY city.name ASC LIMIT 10"}
{"level":"info","ts":"2026-08-18T06:58:39.875+0700","caller":"database/redis.go:34","msg":"Parse config master"}
{"level":"info","ts":"2026-08-18T06:58:39.876+0700","caller":"database/redis.go:49","msg":"Connecting to Redis master"}
{"level":"info","ts":"2026-08-18T06:58:39.917+0700","caller":"database/redis.go:62","msg":"Connected"}
{"level":"info","ts":"2026-08-18T06:58:39.917+0700","caller":"database/redis.go:34","msg":"Parse config slave"}
{"level":"info","ts":"2026-08-18T06:58:39.917+0700","caller":"database/redis.go:49","msg":"Connecting to Redis slave"}
{"level":"info","ts":"2026-08-18T06:58:39.970+0700","caller":"database/redis.go:62","msg":"Connected"}
{"level":"info","ts":"2026-08-18T06:58:46.402+0700","caller":"logger/database.go:90","msg":"[GORM] query execution","line":"/Users/teguh/Documents/project/cargo/cargo-erp-deploy/app/backend/pkg/datatable/datatable.go:30","elapsed":0.027942041,"rows":1,"sql":"SELECT count(*) FROM \"city\" INNER JOIN province ON city.province_id=province.id LEFT JOIN merchant ON city.merchant_id=merchant.id"}
{"level":"info","ts":"2026-08-18T06:58:46.415+0700","caller":"logger/database.go:90","msg":"[GORM] query execution","line":"/Users/teguh/Documents/project/cargo/cargo-erp-deploy/app/backend/pkg/datatable/datatable.go:65","elapsed":0.011990583,"rows":1,"sql":"SELECT count(*) FROM \"city\" INNER JOIN province ON city.province_id=province.id LEFT JOIN merchant ON city.merchant_id=merchant.id"}
{"level":"info","ts":"2026-08-18T06:58:46.433+0700","caller":"logger/database.go:90","msg":"[GORM] query execution","line":"/Users/teguh/Documents/project/cargo/cargo-erp-deploy/app/backend/pkg/datatable/datatable.go:73","elapsed":0.018223125,"rows":10,"sql":"SELECT city.name AS name,province.name AS province_name,city.province_id AS province_id,merchant.id AS merchant_id,city.id AS id FROM \"city\" INNER JOIN province ON city.province_id=province.id LEFT JOIN merchant ON city.merchant_id=merchant.id ORDER BY city.name ASC LIMIT 10"}
{"level":"info","ts":"2026-08-18T07:00:26.392+0700","caller":"logger/database.go:90","msg":"[GORM] query execution","line":"/Users/teguh/Documents/project/cargo/cargo-erp-deploy/app/backend/internal/usecases/city.go:49","elapsed":0.058314917,"rows":1,"sql":"SELECT count(*) FROM \"city\" WHERE (merchant_id IS NULL OR merchant_id='19ff111bca6c2c382d9eb43b3c') AND name='ACEH'"}
{"level":"info","ts":"2026-08-18T07:00:26.432+0700","caller":"logger/database.go:90","msg":"[GORM] query execution","line":"/Users/teguh/Documents/project/cargo/cargo-erp-deploy/app/backend/internal/usecases/city.go:51","elapsed":0.03849825,"rows":0,"sql":"SELECT * FROM \"city\" WHERE (merchant_id IS NULL OR merchant_id='19ff111bca6c2c382d9eb43b3c') AND name='ACEH' ORDER BY name ASC"}
{"level":"info","ts":"2026-08-18T07:00:59.142+0700","caller":"logger/database.go:90","msg":"[GORM] query execution","line":"/Users/teguh/Documents/project/cargo/cargo-erp-deploy/app/backend/internal/usecases/city.go:49","elapsed":0.038294875,"rows":1,"sql":"SELECT count(*) FROM \"city\" WHERE merchant_id IS NULL OR merchant_id='19ff111bca6c2c382d9eb43b3c'"}
{"level":"info","ts":"2026-08-18T07:00:59.193+0700","caller":"logger/database.go:90","msg":"[GORM] query execution","line":"/Users/teguh/Documents/project/cargo/cargo-erp-deploy/app/backend/internal/usecases/city.go:51","elapsed":0.0479295,"rows":475,"sql":"SELECT * FROM \"city\" WHERE merchant_id IS NULL OR merchant_id='19ff111bca6c2c382d9eb43b3c' ORDER BY name ASC"}