From b372a99465893cc68ba1d25a585126171d6347d3 Mon Sep 17 00:00:00 2001 From: teguh nugroho Date: Tue, 18 Aug 2026 08:26:22 +0700 Subject: [PATCH] cities --- internal/handlers/dto/city.go | 36 ++++ internal/handlers/handlers.go | 5 +- internal/handlers/router/city/city.go | 256 ++++++++++++++++++++++++++ internal/importers/city/importer.go | 108 +++++++++++ internal/usecases/city.go | 149 +++++++++++++++ pkg/importer/importer.go | 34 ++-- pkg/jobqueue/jobqueue.go | 7 +- storage/logs/app.log | 24 +++ storage/logs/db.log | 24 +++ 9 files changed, 623 insertions(+), 20 deletions(-) create mode 100644 internal/handlers/dto/city.go create mode 100644 internal/handlers/router/city/city.go create mode 100644 internal/importers/city/importer.go create mode 100644 internal/usecases/city.go diff --git a/internal/handlers/dto/city.go b/internal/handlers/dto/city.go new file mode 100644 index 0000000..cec9f70 --- /dev/null +++ b/internal/handlers/dto/city.go @@ -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"` +} diff --git a/internal/handlers/handlers.go b/internal/handlers/handlers.go index 3bb72e1..d373365 100644 --- a/internal/handlers/handlers.go +++ b/internal/handlers/handlers.go @@ -2,6 +2,7 @@ package handlers import ( "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/middleware" "cargo-erp-backend/pkg/helpers" @@ -105,9 +106,7 @@ func (h *Handlers) Run() { //province { province.NewProvinceHandler(h.Helper, masterdata).Router() - - // regency / city (jika nanti ada) - // regency.NewRegencyHandler(h.Helper, masterdata).Router() + city.NewCityHandler(h.Helper, masterdata).Router() } h.Helper.Log().Info("Starting Apps") diff --git a/internal/handlers/router/city/city.go b/internal/handlers/router/city/city.go new file mode 100644 index 0000000..8779ac4 --- /dev/null +++ b/internal/handlers/router/city/city.go @@ -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, + }) +} diff --git a/internal/importers/city/importer.go b/internal/importers/city/importer.go new file mode 100644 index 0000000..6aa2b70 --- /dev/null +++ b/internal/importers/city/importer.go @@ -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"}, + } +} diff --git a/internal/usecases/city.go b/internal/usecases/city.go new file mode 100644 index 0000000..83e3b79 --- /dev/null +++ b/internal/usecases/city.go @@ -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 +} diff --git a/pkg/importer/importer.go b/pkg/importer/importer.go index 7d7411b..4d724fd 100644 --- a/pkg/importer/importer.go +++ b/pkg/importer/importer.go @@ -128,6 +128,7 @@ func (i *Importer) processSync(file io.Reader, userID string, totalRows int) (*I var logs []ImportLog var errors []string var batch []RowData + var batchRows []int batchSize := 500 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) + batchRows = append(batchRows, rowNumber) if len(batch) >= batchSize { if err := i.insertBatch(db, batch); err != nil { 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, }) } else { - for _, b := range batch { + for idx, b := range batch { dataStr := i.Handler.GetDataString(b) logs = append(logs, ImportLog{ - Row: rowNumber, + Row: batchRows[idx], Status: "inserted", 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) } batch = nil + batchRows = 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) errors = append(errors, errorMsg) logs = append(logs, ImportLog{ - Row: totalRows + 1, + Row: batchRows[0], Status: "error", Data: "", Message: errorMsg, }) } else { - for _, b := range batch { + for idx, b := range batch { dataStr := i.Handler.GetDataString(b) logs = append(logs, ImportLog{ - Row: imported + 1, + Row: batchRows[idx], Status: "inserted", Data: dataStr, - Message: fmt.Sprintf("'%s' inserted successfully", dataStr), + Message: fmt.Sprintf("Row %d: '%s' inserted successfully", batchRows[idx], dataStr), }) } imported += len(batch) @@ -252,6 +255,7 @@ func (i *Importer) processAsyncWorker(file io.Reader, jobID string, userID strin var logs []ImportLog var errors []string var batch []RowData + var batchRows []int batchSize := 500 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) + batchRows = append(batchRows, rowNumber) if len(batch) >= batchSize { if err := i.insertBatch(db, batch); err != nil { 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, }) } else { - for _, b := range batch { + for idx, b := range batch { dataStr := i.Handler.GetDataString(b) logs = append(logs, ImportLog{ - Row: rowNumber, + Row: batchRows[idx], Status: "inserted", 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) } batch = nil + batchRows = nil i.saveJobLogs(ctx, jobID, logs, imported, skipped, errors) } 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) errors = append(errors, errorMsg) logs = append(logs, ImportLog{ - Row: totalRows + 1, + Row: batchRows[0], Status: "error", Data: "", Message: errorMsg, }) } else { - for _, b := range batch { + for idx, b := range batch { dataStr := i.Handler.GetDataString(b) logs = append(logs, ImportLog{ - Row: imported + 1, + Row: batchRows[idx], Status: "inserted", Data: dataStr, - Message: fmt.Sprintf("'%s' inserted successfully", dataStr), + Message: fmt.Sprintf("Row %d: '%s' inserted successfully", batchRows[idx], dataStr), }) } imported += len(batch) diff --git a/pkg/jobqueue/jobqueue.go b/pkg/jobqueue/jobqueue.go index 3b5cc28..161d1fd 100644 --- a/pkg/jobqueue/jobqueue.go +++ b/pkg/jobqueue/jobqueue.go @@ -190,14 +190,15 @@ func joinErrors(errors []string) string { func splitErrors(s string) []string { result := []string{} current := "" - for _, c := range s { - if c == '|' { + for i := 0; i < len(s); i++ { + if i+1 < len(s) && s[i] == '|' && s[i+1] == '|' { if current != "" { result = append(result, current) } current = "" + i++ // skip second | } else { - current += string(c) + current += string(s[i]) } } if current != "" { diff --git a/storage/logs/app.log b/storage/logs/app.log index 5e3ce8c..5962d03 100644 --- a/storage/logs/app.log +++ b/storage/logs/app.log @@ -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.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-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)"} diff --git a/storage/logs/db.log b/storage/logs/db.log index 595f785..85c01c6 100644 --- a/storage/logs/db.log +++ b/storage/logs/db.log @@ -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.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-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"}