From 1c99093ff8f7ecd29a501e449c64933ffd3d736f Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 10:31:35 +0700 Subject: [PATCH 01/29] feat[BE-127]; creating correct logic update and delete transfer laying --- .../services/transfer_laying.service.go | 172 ++++++++++++++++-- .../validations/transfer_laying.validation.go | 8 +- 2 files changed, 161 insertions(+), 19 deletions(-) diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 92965de0..06c297bc 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -361,7 +361,9 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return nil, err } - _, err := s.Repository.GetByID(c.Context(), id, nil) + existingTransfer, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + return db.Preload("Sources.ProductWarehouse").Preload("Targets") + }) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") @@ -382,26 +384,140 @@ func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, i } } - updateBody := make(map[string]any) - - if req.TransferDate != nil { - updateBody["transfer_date"] = *req.TransferDate + if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.SourceProjectFlockId, nil); err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Source project flock not found") } - if req.Reason != nil { - updateBody["notes"] = *req.Reason + if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.TargetProjectFlockId, nil); err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Target project flock not found") } - if len(updateBody) == 0 { - return s.GetOne(c, id) + transferDate, err := time.Parse("2006-01-02", req.TransferDate) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format") } - if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found") + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + projectFlockPopulationRepoTx := s.ProjectFlockPopulationRepo.WithTx(dbTransaction) + productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction) + + for _, oldSource := range existingTransfer.Sources { + if oldSource.ProductWarehouseId != nil && oldSource.Qty > 0 { + + if err := productWarehouseRepoTx.PatchOne(c.Context(), *oldSource.ProductWarehouseId, map[string]any{ + "quantity": gorm.Expr("quantity + ?", oldSource.Qty), + }, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to restore warehouse quantity") + } + + if err := s.restoreProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, oldSource.SourceProjectFlockKandangId, oldSource.Qty); err != nil { + return err + } + } } - s.Log.Errorf("Failed to update transferLaying: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer laying") + + for _, oldSource := range existingTransfer.Sources { + if err := dbTransaction.Delete(&oldSource).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete old source") + } + } + + for _, oldTarget := range existingTransfer.Targets { + if err := dbTransaction.Delete(&oldTarget).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete old target") + } + } + + totalSourceQty := 0.0 + for _, source := range req.SourceKandangs { + totalSourceQty += source.Quantity + } + + if err := s.Repository.WithTx(dbTransaction).PatchOne(c.Context(), id, map[string]any{ + "transfer_date": transferDate, + "notes": req.Reason, + "pending_usage_qty": &totalSourceQty, + }, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update transfer header") + } + + sourceWarehouseMap := make(map[uint]uint) + for _, sourceDetail := range req.SourceKandangs { + + populations, err := projectFlockPopulationRepoTx.GetByProjectFlockKandangID(c.Context(), sourceDetail.ProjectFlockKandangId) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get populations") + } + + if len(populations) == 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has no population available", sourceDetail.ProjectFlockKandangId)) + } + + var totalPopulation float64 + var productWarehouseId uint + + for _, pop := range populations { + totalPopulation += pop.TotalQty + if pop.ProductWarehouseId > 0 { + productWarehouseId = pop.ProductWarehouseId + } + } + + if totalPopulation < sourceDetail.Quantity { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Source kandang %d has insufficient quantity. Available: %.0f, Requested: %.0f", sourceDetail.ProjectFlockKandangId, totalPopulation, sourceDetail.Quantity)) + } + + sourceWarehouseMap[sourceDetail.ProjectFlockKandangId] = productWarehouseId + + source := entity.LayingTransferSource{ + LayingTransferId: id, + SourceProjectFlockKandangId: sourceDetail.ProjectFlockKandangId, + Qty: sourceDetail.Quantity, + ProductWarehouseId: &productWarehouseId, + } + if err := dbTransaction.Create(&source).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer source") + } + + if err := s.reduceProjectFlockPopulation(c.Context(), projectFlockPopulationRepoTx, sourceDetail.ProjectFlockKandangId, sourceDetail.Quantity); err != nil { + return err + } + + if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouseId, map[string]any{"quantity": gorm.Expr("quantity - ?", sourceDetail.Quantity)}, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update source warehouse quantity") + } + } + + for _, targetDetail := range req.TargetKandangs { + targetPFK, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), targetDetail.ProjectFlockKandangId) + if err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target project flock kandang") + } + + targetWarehouse, err := s.WarehouseRepo.GetLatestByKandangID(c.Context(), targetPFK.KandangId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No warehouse found for target kandang %d", targetDetail.ProjectFlockKandangId)) + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to get target warehouse") + } + + target := entity.LayingTransferTarget{ + LayingTransferId: id, + TargetProjectFlockKandangId: targetDetail.ProjectFlockKandangId, + Qty: targetDetail.Quantity, + ProductWarehouseId: &targetWarehouse.Id, + } + if err := dbTransaction.Create(&target).Error; err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create transfer target") + } + } + + return nil + }) + + if err != nil { + return nil, err } return s.GetOne(c, id) @@ -464,9 +580,8 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { for i := len(populations) - 1; i >= 0 && remainingToRestore > 0; i-- { pop := populations[i] restoreAmount := remainingToRestore - if remainingToRestore < pop.TotalQty { - - restoreAmount = remainingToRestore + if pop.TotalQty < remainingToRestore { + restoreAmount = pop.TotalQty } newQty := pop.TotalQty + restoreAmount @@ -725,3 +840,26 @@ func (s *transferLayingService) reduceProjectFlockPopulation(ctx context.Context return nil } + +func (s *transferLayingService) restoreProjectFlockPopulation(ctx context.Context, populationRepo ProjectFlockRepository.ProjectFlockPopulationRepository, projectFlockKandangID uint, quantityToRestore float64) error { + populations, err := populationRepo.GetByProjectFlockKandangID(ctx, projectFlockKandangID) + if err != nil { + return err + } + + if len(populations) == 0 { + return fiber.NewError(fiber.StatusBadRequest, "No populations found for restoration") + } + + // Restore in LIFO order (from newest to oldest) + // Add all quantity back to the last (newest) population + if len(populations) > 0 { + lastPop := populations[len(populations)-1] + newQty := lastPop.TotalQty + quantityToRestore + if err := populationRepo.PatchOne(ctx, lastPop.Id, map[string]any{"total_qty": newQty}, nil); err != nil { + return err + } + } + + return nil +} diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go index e35ba4f3..bd117bd9 100644 --- a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -20,8 +20,12 @@ type Create struct { } type Update struct { - TransferDate *string `json:"transfer_date,omitempty" validate:"omitempty,datetime=2006-01-02"` - Reason *string `json:"reason,omitempty" validate:"omitempty,max=1000"` + TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"` + SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"` + TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"` + SourceKandangs []SourceKandangDetail `json:"source_kandangs" validate:"required,min=1,dive,required"` + TargetKandangs []TargetKandangDetail `json:"target_kandangs" validate:"required,min=1,dive,required"` + Reason string `json:"reason" validate:"omitempty,max=1000"` } type Query struct { From 60fe553f636e8e0ce73cd1a14f8062422e719399 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 10:51:32 +0700 Subject: [PATCH 02/29] FIX[BE]: getting available qty from Flags instead of Product.Category --- .../repositories/product_warehouse.repository.go | 4 ++-- .../modules/production/chickins/services/chickin.service.go | 2 +- .../services/project_flock_kandang.service.go | 4 +--- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index d95c298b..fbda4718 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -165,7 +165,7 @@ func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Con var productWarehouses []entity.ProductWarehouse err := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}). Joins("JOIN products ON products.id = product_warehouses.product_id"). - Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products"). + Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'"). Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId). Order("product_warehouses.created_at DESC"). Preload("Product").Preload("Warehouse"). @@ -179,7 +179,7 @@ func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Con func (r *ProductWarehouseRepositoryImpl) GetFirstProductByFlag(ctx context.Context, flagName string) (*entity.Product, error) { var product entity.Product err := r.DB().WithContext(ctx). - Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products"). + Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'"). Where("flags.name = ?", flagName). First(&product).Error if err != nil { diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index d78a9a00..974b5c9f 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -136,7 +136,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category: %s", category)) } - productWarehouses, err = s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id) + productWarehouses, err = s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id) if err != nil || len(productWarehouses) == 0 { return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product for %s category in the Kandang's warehouse not found", strings.ToLower(category))) } diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index ec292d90..52b2570a 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -105,11 +105,10 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project } else if projectFlockKandang.ProjectFlock.Category == string(utils.ProjectFlockCategoryLaying) { productCategoryCode = "PULLET" } else { - return nil, nil } - products, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id) + products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id) if err != nil || len(products) == 0 { return nil, nil } @@ -121,7 +120,6 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err) } - // Only include product warehouse if available_qty > 0 if availableQty <= 0 { continue } From 028d5f6f913f109cc53a79d7a629d75a99dce5e9 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 11:13:56 +0700 Subject: [PATCH 03/29] FEAT[BE-127]: add flockName to getone projectflockkandang API --- .../project-flock-kandangs/dto/project_flock_kandang.dto.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 92db5b23..163de6a3 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -24,6 +24,7 @@ type ProjectFlockKandangBaseDTO struct { type ProjectFlockDTO struct { Id uint `json:"id"` + Name string `json:"name,omitempty"` Period int `json:"period"` Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` @@ -82,6 +83,7 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO return &ProjectFlockDTO{ Id: pf.Id, + Name: pf.FlockName, Period: pf.Period, Area: pf.Area, Category: pf.Category, From 5cfa97dd031561632f6317b253bb7e1fab556e83 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 13:41:16 +0700 Subject: [PATCH 04/29] Feat[BE]: adding project flock kandang id into project flock get one projecct flock api --- .../project_flocks/dto/projectflock.dto.go | 47 +++++++++++-------- .../repositories/projectflock.repository.go | 4 +- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index 977aeb40..ce9be3d3 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -23,25 +23,23 @@ type ProjectFlockBaseDTO struct { FlockName string `json:"flock_name"` } -type KandangWithProjectFlockIdDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - Status string `json:"status"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` -} - type ProjectFlockListDTO struct { ProjectFlockBaseDTO // Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` - Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` - Category string `json:"category"` - Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` - Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` - Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Approval approvalDTO.ApprovalBaseDTO `json:"approval"` + Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` + Category string `json:"category"` + Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` + Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` + Kandangs []KandangWithProjectFlockIdDTO `json:"kandangs,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval approvalDTO.ApprovalBaseDTO `json:"approval"` +} + +type KandangWithProjectFlockIdDTO struct { + kandangDTO.KandangBaseDTO + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` } type ProjectFlockDetailDTO struct { @@ -60,11 +58,22 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { createdUser = &mapped } - var kandangSummaries []kandangDTO.KandangBaseDTO + var kandangSummaries []KandangWithProjectFlockIdDTO if len(e.Kandangs) > 0 { - kandangSummaries = make([]kandangDTO.KandangBaseDTO, len(e.Kandangs)) + kandangSummaries = make([]KandangWithProjectFlockIdDTO, len(e.Kandangs)) for i, kandang := range e.Kandangs { - kandangSummaries[i] = kandangDTO.ToKandangBaseDTO(kandang) + // Find project_flock_kandang_id dari KandangHistory + var pfkId uint + for _, kh := range e.KandangHistory { + if kh.KandangId == kandang.Id { + pfkId = kh.Id + break + } + } + kandangSummaries[i] = KandangWithProjectFlockIdDTO{ + KandangBaseDTO: kandangDTO.ToKandangBaseDTO(kandang), + ProjectFlockKandangId: pfkId, + } } } diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index bb653fe9..a8fab919 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -117,7 +117,9 @@ func (r *ProjectflockRepositoryImpl) withDefaultRelations(db *gorm.DB) *gorm.DB Preload("Area"). Preload("Fcr"). Preload("Location"). - Preload("Kandangs") + Preload("Kandangs"). + Preload("KandangHistory"). + Preload("KandangHistory.Kandang") } func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *validation.Query) *gorm.DB { From 4b69afe4facf29ac2bd8eace1b97948b44979676 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 14:03:47 +0700 Subject: [PATCH 05/29] Feat[BE-127] create get all with param on project flock kandang --- .../project_flock_kandang.controller.go | 9 +- .../dto/project_flock_kandang.dto.go | 6 ++ .../services/project_flock_kandang.service.go | 92 +++++++++++++++++-- .../project_flock_kandang.validation.go | 12 ++- .../projectflock_kandang.repository.go | 39 ++++---- 5 files changed, 129 insertions(+), 29 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index 92743035..8e4c23a5 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -33,11 +33,16 @@ func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } - result, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query) + results, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query) if err != nil { return err } + var data []dto.ProjectFlockKandangListDTO + for _, result := range results { + data = append(data, dto.ToProjectFlockKandangListDTO(result.Entity)) + } + return c.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.ProjectFlockKandangListDTO]{ Code: fiber.StatusOK, @@ -49,7 +54,7 @@ func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: dto.ToProjectFlockKandangListDTOs(result), + Data: data, }) } diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 163de6a3..de74723f 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -68,6 +68,12 @@ type ProjectFlockKandangDetailDTO struct { ProjectFlockKandangListDTO } +// Wrapper untuk GetAll dengan available quantities sudah included +type ProjectFlockKandangWithAvailableQtysDTO struct { + Entity entity.ProjectFlockKandang + AvailableQtys []map[string]interface{} +} + // === Mapper Functions (ordered) === func ToProjectFlockKandangBaseDTO(e entity.ProjectFlockKandang) ProjectFlockKandangBaseDTO { diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 52b2570a..ee6687c0 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -7,6 +7,7 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" + dto "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/dto" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -18,7 +19,7 @@ import ( ) type ProjectFlockKandangService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ProjectFlockKandangWithAvailableQtysDTO, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) } @@ -44,19 +45,99 @@ func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository } } -func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) { +func (s projectFlockKandangService) withRelations(db *gorm.DB) *gorm.DB { + return db. + Preload("ProjectFlock"). + Preload("ProjectFlock.Fcr"). + Preload("ProjectFlock.Area"). + Preload("ProjectFlock.Location"). + Preload("ProjectFlock.CreatedUser"). + Preload("ProjectFlock.Kandangs"). + Preload("ProjectFlock.KandangHistory"). + Preload("Kandang"). + Preload("Chickins"). + Preload("Chickins.CreatedUser"). + Preload("Chickins.ProductWarehouse") + +} + +func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ProjectFlockKandangWithAvailableQtysDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } - projectFlockKandangs, err := s.Repository.GetAll(c.Context()) + if params.Page <= 0 { + params.Page = 1 + } + if params.Limit <= 0 { + params.Limit = 10 + } + + offset := (params.Page - 1) * params.Limit + + projectFlockKandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + + if params.Search != "" { + db = db.Where( + db.Where("LOWER(kandang.name) LIKE LOWER(?)", "%"+params.Search+"%"). + Or("LOWER(project_flock.flock_name) LIKE LOWER(?)", "%"+params.Search+"%"), + ) + } + + if params.ProjectFlockId > 0 { + db = db.Where("project_flock_kandangs.project_flock_id = ?", params.ProjectFlockId) + } + + if params.KandangId > 0 { + db = db.Where("project_flock_kandangs.kandang_id = ?", params.KandangId) + } + + if params.Category != "" { + db = db.Where("project_flock.category = ?", params.Category) + } + if params.AreaId > 0 { + db = db.Where("project_flock.area_id = ?", params.AreaId) + } + + sortBy := "project_flock_id ASC, created_at ASC" + if params.SortBy != "" { + sortOrder := "ASC" + if params.SortOrder == "DESC" { + sortOrder = "DESC" + } + + switch params.SortBy { + case "created_at": + sortBy = "project_flock_kandangs.created_at " + sortOrder + case "period": + sortBy = "project_flock.period " + sortOrder + } + } + + return db.Order(sortBy) + }) + if err != nil { s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err) return nil, 0, err } - total := int64(len(projectFlockKandangs)) - return projectFlockKandangs, total, nil + var results []dto.ProjectFlockKandangWithAvailableQtysDTO + for i := range projectFlockKandangs { + availableQtys, err := s.getAvailableQuantities(c, &projectFlockKandangs[i]) + if err != nil { + s.Log.Warnf("Failed to fetch available quantities for kandang %d: %+v", projectFlockKandangs[i].Kandang.Id, err) + availableQtys = nil + } + + results = append(results, dto.ProjectFlockKandangWithAvailableQtysDTO{ + Entity: projectFlockKandangs[i], + AvailableQtys: availableQtys, + }) + } + + return results, total, nil } func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) { @@ -65,7 +146,6 @@ func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Proje return nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") } if err != nil { - s.Log.Errorf("Failed get projectFlockKandang by id: %+v", err) return nil, nil, err } diff --git a/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go index 1e95fd2c..e635ff92 100644 --- a/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go +++ b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go @@ -11,7 +11,13 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=50"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=50"` + ProjectFlockId uint `query:"project_flock_id" validate:"omitempty"` + KandangId uint `query:"kandang_id" validate:"omitempty"` + Category string `query:"category" validate:"omitempty,oneof=Growing Laying"` + AreaId uint `query:"area_id" validate:"omitempty"` + SortBy string `query:"sort_by" validate:"omitempty,oneof=created_at period"` + SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"` } diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 101d01ab..6c39464b 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -14,7 +14,7 @@ type ProjectFlockKandangRepository interface { GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error) CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error - GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) + GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error) @@ -50,25 +50,28 @@ func (r *projectFlockKandangRepositoryImpl) DeleteMany(ctx context.Context, proj Delete(&entity.ProjectFlockKandang{}).Error } -func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) { +func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error) { var records []entity.ProjectFlockKandang - if err := r.db.WithContext(ctx). - Preload("ProjectFlock"). - Preload("ProjectFlock.Fcr"). - Preload("ProjectFlock.Area"). - Preload("ProjectFlock.Location"). - Preload("ProjectFlock.CreatedUser"). - Preload("ProjectFlock.Kandangs"). - Preload("ProjectFlock.KandangHistory"). - Preload("Kandang"). - Preload("Chickins"). - Preload("Chickins.CreatedUser"). - Preload("Chickins.ProductWarehouse"). - Order("project_flock_id ASC, created_at ASC"). - Find(&records).Error; err != nil { - return nil, err + var total int64 + + q := r.db.WithContext(ctx) + + // Apply modifier function + if modifier != nil { + q = modifier(q) } - return records, nil + + // Count total + if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + // Apply pagination and fetch + if err := q.Offset(offset).Limit(limit).Find(&records).Error; err != nil { + return nil, 0, err + } + + return records, total, nil } func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKandangRepository { From a587584156da887bf81b4876449393cbaaaac063 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 14:04:20 +0700 Subject: [PATCH 06/29] Feat[BE]: change response flock name on project flock kandang --- .../project-flock-kandangs/dto/project_flock_kandang.dto.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index de74723f..8e9d3672 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -24,7 +24,7 @@ type ProjectFlockKandangBaseDTO struct { type ProjectFlockDTO struct { Id uint `json:"id"` - Name string `json:"name,omitempty"` + Name string `json:"flock_name,omitempty"` Period int `json:"period"` Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` From 663d5129bb4532b2b9bb273e428c6e2acaf8456d Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 17:57:10 +0700 Subject: [PATCH 07/29] Feat[BE-127] Creating project flock kandang get all with soma query param --- .../project_flock_kandang.controller.go | 15 ++- .../services/project_flock_kandang.service.go | 80 ++++------------ .../project_flock_kandang.validation.go | 1 + .../projectflock_kandang.repository.go | 96 ++++++++++++++++++- 4 files changed, 126 insertions(+), 66 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index 8e4c23a5..383006bf 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -24,9 +24,16 @@ func NewProjectFlockKandangController(projectFlockKandangService service.Project func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - Search: c.Query("search", ""), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + ProjectFlockId: uint(c.QueryInt("project_flock_id", 0)), + KandangId: uint(c.QueryInt("kandang_id", 0)), + Category: c.Query("category", ""), + AreaId: uint(c.QueryInt("area_id", 0)), + SortBy: c.Query("sort_by", ""), + SortOrder: c.Query("sort_order", ""), + StepName: c.Query("step_name", ""), } if query.Page < 1 || query.Limit < 1 { @@ -38,7 +45,7 @@ func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { return err } - var data []dto.ProjectFlockKandangListDTO + data := make([]dto.ProjectFlockKandangListDTO, 0) for _, result := range results { data = append(data, dto.ToProjectFlockKandangListDTO(result.Entity)) } diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index ee6687c0..90e85882 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -45,22 +45,6 @@ func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository } } -func (s projectFlockKandangService) withRelations(db *gorm.DB) *gorm.DB { - return db. - Preload("ProjectFlock"). - Preload("ProjectFlock.Fcr"). - Preload("ProjectFlock.Area"). - Preload("ProjectFlock.Location"). - Preload("ProjectFlock.CreatedUser"). - Preload("ProjectFlock.Kandangs"). - Preload("ProjectFlock.KandangHistory"). - Preload("Kandang"). - Preload("Chickins"). - Preload("Chickins.CreatedUser"). - Preload("Chickins.ProductWarehouse") - -} - func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ProjectFlockKandangWithAvailableQtysDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err @@ -75,55 +59,33 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer offset := (params.Page - 1) * params.Limit - projectFlockKandangs, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - db = s.withRelations(db) - - if params.Search != "" { - db = db.Where( - db.Where("LOWER(kandang.name) LIKE LOWER(?)", "%"+params.Search+"%"). - Or("LOWER(project_flock.flock_name) LIKE LOWER(?)", "%"+params.Search+"%"), - ) - } - - if params.ProjectFlockId > 0 { - db = db.Where("project_flock_kandangs.project_flock_id = ?", params.ProjectFlockId) - } - - if params.KandangId > 0 { - db = db.Where("project_flock_kandangs.kandang_id = ?", params.KandangId) - } - - if params.Category != "" { - db = db.Where("project_flock.category = ?", params.Category) - } - if params.AreaId > 0 { - db = db.Where("project_flock.area_id = ?", params.AreaId) - } - - sortBy := "project_flock_id ASC, created_at ASC" - if params.SortBy != "" { - sortOrder := "ASC" - if params.SortOrder == "DESC" { - sortOrder = "DESC" - } - - switch params.SortBy { - case "created_at": - sortBy = "project_flock_kandangs.created_at " + sortOrder - case "period": - sortBy = "project_flock.period " + sortOrder - } - } - - return db.Order(sortBy) - }) + projectFlockKandangs, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) if err != nil { s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err) return nil, 0, err } - var results []dto.ProjectFlockKandangWithAvailableQtysDTO + if s.ApprovalSvc != nil { + projectFlockKandangIDs := make([]uint, len(projectFlockKandangs)) + for i, pfk := range projectFlockKandangs { + projectFlockKandangIDs[i] = pfk.Id + } + + approvalMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandangIDs, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Failed to fetch approvals for projectFlockKandangs: %+v", err) + } else { + for i := range projectFlockKandangs { + if approval, ok := approvalMap[projectFlockKandangs[i].Id]; ok { + projectFlockKandangs[i].LatestApproval = approval + } + } + } + } + results := make([]dto.ProjectFlockKandangWithAvailableQtysDTO, 0) for i := range projectFlockKandangs { availableQtys, err := s.getAvailableQuantities(c, &projectFlockKandangs[i]) if err != nil { diff --git a/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go index e635ff92..93e0256a 100644 --- a/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go +++ b/internal/modules/production/project-flock-kandangs/validations/project_flock_kandang.validation.go @@ -20,4 +20,5 @@ type Query struct { AreaId uint `query:"area_id" validate:"omitempty"` SortBy string `query:"sort_by" validate:"omitempty,oneof=created_at period"` SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"` + StepName string `query:"step_name" validate:"omitempty,max=50"` } diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 6c39464b..1a05acff 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -6,6 +6,7 @@ import ( "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" "gorm.io/gorm" ) @@ -15,6 +16,7 @@ type ProjectFlockKandangRepository interface { CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error) + GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error) FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error) @@ -56,17 +58,14 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context, offset i q := r.db.WithContext(ctx) - // Apply modifier function if modifier != nil { q = modifier(q) } - // Count total if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil { return nil, 0, err } - // Apply pagination and fetch if err := q.Offset(offset).Limit(limit).Find(&records).Error; err != nil { return nil, 0, err } @@ -74,6 +73,97 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context, offset i return records, total, nil } +func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) { + var records []entity.ProjectFlockKandang + var total int64 + + query, ok := params.(*validation.Query) + + q := r.db.WithContext(ctx). + Joins("JOIN \"kandangs\" ON \"project_flock_kandangs\".\"kandang_id\" = \"kandangs\".\"id\""). + Joins("JOIN \"project_flocks\" ON \"project_flock_kandangs\".\"project_flock_id\" = \"project_flocks\".\"id\""). + Preload("ProjectFlock"). + Preload("ProjectFlock.Fcr"). + Preload("ProjectFlock.Area"). + Preload("ProjectFlock.Location"). + Preload("ProjectFlock.CreatedUser"). + Preload("ProjectFlock.Kandangs"). + Preload("ProjectFlock.KandangHistory"). + Preload("Kandang"). + Preload("Chickins"). + Preload("Chickins.CreatedUser"). + Preload("Chickins.ProductWarehouse") + + if ok && query != nil && query.StepName != "" { + q = q.Where(` + EXISTS ( + SELECT 1 FROM "approvals" + WHERE "approvals"."approvable_id" = "project_flock_kandangs"."id" + AND "approvals"."approvable_type" = ? + AND LOWER("approvals"."step_name") = LOWER(?) + AND "approvals"."id" IN ( + SELECT "id" FROM "approvals" + WHERE "approvable_id" = "project_flock_kandangs"."id" + AND "approvable_type" = ? + ORDER BY "action_at" DESC + LIMIT 1 + ) + ) + `, "PROJECT_FLOCK_KANDANGS", query.StepName, "PROJECT_FLOCK_KANDANGS") + } + + if ok && query != nil { + if query.Search != "" { + escapedSearch := strings.NewReplacer("\\", "\\\\", "%", "\\%", "_", "\\_").Replace(query.Search) + q = q.Where( + r.db.Where("LOWER(\"kandangs\".\"name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"). + Or("LOWER(\"project_flocks\".\"flock_name\") LIKE LOWER(?) ESCAPE '\\'", "%"+escapedSearch+"%"), + ) + } + + if query.ProjectFlockId > 0 { + q = q.Where("\"project_flock_kandangs\".\"project_flock_id\" = ?", query.ProjectFlockId) + } + + if query.KandangId > 0 { + q = q.Where("\"project_flock_kandangs\".\"kandang_id\" = ?", query.KandangId) + } + + if query.Category != "" { + q = q.Where("\"project_flocks\".\"category\" = ?", query.Category) + } + + if query.AreaId > 0 { + q = q.Where("\"project_flocks\".\"area_id\" = ?", query.AreaId) + } + } + + if err := q.Model(&entity.ProjectFlockKandang{}).Count(&total).Error; err != nil { + return nil, 0, err + } + + sortBy := "\"project_flock_kandangs\".\"created_at\" DESC" + if ok && query != nil && query.SortBy != "" { + sortOrder := "DESC" + if query.SortOrder == "ASC" { + sortOrder = "ASC" + } + + switch query.SortBy { + case "created_at": + sortBy = "\"project_flock_kandangs\".\"created_at\" " + sortOrder + case "period": + sortBy = "\"project_flocks\".\"period\" " + sortOrder + } + } + + if err := q.Order(sortBy).Offset(offset).Limit(limit).Find(&records).Error; err != nil { + return nil, 0, err + } + + return records, total, nil +} + func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKandangRepository { return &projectFlockKandangRepositoryImpl{db: tx} } From 954cccd564231f4531454fa1e3e59577aae5f162 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 6 Nov 2025 21:25:15 +0700 Subject: [PATCH 08/29] Fix[BE]: make projectflock kandang API and dto clean --- .../production/chickins/dto/chickin.dto.go | 52 ++++- .../project_flock_kandang.controller.go | 4 +- .../dto/project_flock_kandang.dto.go | 200 +++++------------- .../services/project_flock_kandang.service.go | 69 ++---- .../projectflock_kandang.repository.go | 4 + 5 files changed, 115 insertions(+), 214 deletions(-) diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 7271c310..ad6d10f8 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -9,20 +9,29 @@ import ( flockBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" kandangBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" locationBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" + productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils" userBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) // === DTO Structs (ordered) === +type ProductWarehouseDTO struct { + Id uint `json:"id"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` +} + type ChickinBaseDTO struct { - Id uint `json:"id"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` - ChickInDate time.Time `json:"chick_in_date"` - ProductWarehouseId uint `json:"product_warehouse_id"` - UsageQty float64 `json:"usage_qty"` - PendingUsageQty float64 `json:"pending_usage_qty"` - Notes string `json:"notes"` + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + ChickInDate time.Time `json:"chick_in_date"` + ProductWarehouseId uint `json:"product_warehouse_id"` + ProductWarehouse *ProductWarehouseDTO `json:"product_warehouse,omitempty"` + UsageQty float64 `json:"usage_qty"` + PendingUsageQty float64 `json:"pending_usage_qty"` + Notes string `json:"notes"` } type ProjectFlockDTO struct { @@ -159,11 +168,18 @@ func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { // If relation is not loaded but ID is available, use the ID projectFlockKandangId = e.ProjectFlockKandangId } + + var productWarehouse *ProductWarehouseDTO + if e.ProductWarehouse != nil && e.ProductWarehouse.Id != 0 { + productWarehouse = toProductWarehouseDTO(e.ProductWarehouse) + } + return ChickinBaseDTO{ Id: e.Id, ProjectFlockKandangId: projectFlockKandangId, ChickInDate: e.ChickInDate, ProductWarehouseId: e.ProductWarehouseId, + ProductWarehouse: productWarehouse, UsageQty: e.UsageQty, PendingUsageQty: e.PendingUsageQty, Notes: e.Notes, @@ -242,3 +258,25 @@ func ToChickinDetailDTOs(e []entity.ProjectChickin) []ChickinDetailDTO { } return result } + +// === Helper Functions === + +// ToProductWarehouseDTO adalah exported helper untuk mapping ProductWarehouse (shared logic) +func ToProductWarehouseDTO(pw *entity.ProductWarehouse) *ProductWarehouseDTO { + if pw == nil { + return nil + } + + product := productDTO.ToProductBaseDTO(pw.Product) + warehouse := warehouseDTO.ToWarehouseBaseDTO(pw.Warehouse) + + return &ProductWarehouseDTO{ + Id: pw.Id, + Product: &product, + Warehouse: &warehouse, + } +} + +func toProductWarehouseDTO(pw *entity.ProductWarehouse) *ProductWarehouseDTO { + return ToProductWarehouseDTO(pw) +} diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index 383006bf..21701826 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -47,7 +47,7 @@ func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { data := make([]dto.ProjectFlockKandangListDTO, 0) for _, result := range results { - data = append(data, dto.ToProjectFlockKandangListDTO(result.Entity)) + data = append(data, dto.ToProjectFlockKandangListDTO(result)) } return c.Status(fiber.StatusOK). @@ -83,6 +83,6 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get projectFlockKandang successfully", - Data: dto.ToProjectFlockKandangListDTOWithAvailableQty(*result, availableQtys), + Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys), }) } diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 8e9d3672..e80e3220 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -19,7 +19,9 @@ import ( // === DTO Structs (ordered) === type ProjectFlockKandangBaseDTO struct { - Id uint `json:"id"` + Id uint `json:"id"` + KandangId uint `json:"kandang_id"` + ProjectFlockId uint `json:"project_flock_id"` } type ProjectFlockDTO struct { @@ -55,23 +57,17 @@ type AvailableQtyDTO struct { type ProjectFlockKandangListDTO struct { ProjectFlockKandangBaseDTO - ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"` - Kandang *KandangDTO `json:"kandang,omitempty"` - Chickins []chickinDTO.ChickinBaseDTO `json:"chickins,omitempty"` - AvailableQtys []AvailableQtyDTO `json:"available_qtys,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` + ProjectFlock *ProjectFlockDTO `json:"project_flock,omitempty"` + Kandang *KandangDTO `json:"kandang,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` } type ProjectFlockKandangDetailDTO struct { ProjectFlockKandangListDTO -} - -// Wrapper untuk GetAll dengan available quantities sudah included -type ProjectFlockKandangWithAvailableQtysDTO struct { - Entity entity.ProjectFlockKandang - AvailableQtys []map[string]interface{} + Chickins []chickinDTO.ChickinBaseDTO `json:"chickins,omitempty"` + AvailableQtys []AvailableQtyDTO `json:"available_qtys,omitempty"` } // === Mapper Functions (ordered) === @@ -101,23 +97,27 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO } } -func ToProjectFlockKandangListDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtysRaw []map[string]interface{}) ProjectFlockKandangListDTO { +func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtyMap map[uint]float64) ProjectFlockKandangDetailDTO { var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO if e.ProjectFlock.Id != 0 { mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) projectFlockSummary = &mapped } - return ProjectFlockKandangListDTO{ + listDTO := ProjectFlockKandangListDTO{ ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), ProjectFlock: toProjectFlockDTO(projectFlockSummary), Kandang: toKandangDTO(e.Kandang), - Chickins: toChickinDTOs(e.Chickins), - AvailableQtys: toAvailableQtyDTOsFromRaw(availableQtysRaw), CreatedAt: e.CreatedAt, CreatedUser: toCreatedUserDTO(e.ProjectFlock), Approval: toApprovalDTO(e), } + + return ProjectFlockKandangDetailDTO{ + ProjectFlockKandangListDTO: listDTO, + Chickins: toChickinDTOs(e.Chickins), + AvailableQtys: toAvailableQtyDTOsFromMap(e.Chickins, availableQtyMap), + } } func toKandangDTO(kandang entity.Kandang) *KandangDTO { @@ -151,8 +151,6 @@ func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKand ProjectFlockKandangBaseDTO: ToProjectFlockKandangBaseDTO(e), ProjectFlock: toProjectFlockDTO(projectFlockSummary), Kandang: toKandangDTO(e.Kandang), - Chickins: toChickinDTOs(e.Chickins), - AvailableQtys: toAvailableQtyDTOs(e.Chickins), CreatedAt: e.CreatedAt, CreatedUser: toCreatedUserDTO(e.ProjectFlock), Approval: toApprovalDTO(e), @@ -167,75 +165,6 @@ func ToProjectFlockKandangListDTOs(e []entity.ProjectFlockKandang) []ProjectFloc return result } -func ToProjectFlockKandangDetailDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDetailDTO { - return ProjectFlockKandangDetailDTO{ - ProjectFlockKandangListDTO: ToProjectFlockKandangListDTO(e), - } -} - -// === Helper Functions (ordered) === - -func toProductWarehouseDTO(pwData map[string]interface{}) *ProductWarehouseDTO { - if pwData == nil { - return nil - } - - dto := &ProductWarehouseDTO{} - - if id, ok := pwData["id"].(float64); ok { - dto.Id = uint(id) - } else if id, ok := pwData["id"].(uint); ok { - dto.Id = id - } - - if pData, ok := pwData["product"].(map[string]interface{}); ok { - dto.Product = toProductDTO(pData) - } - - if wData, ok := pwData["warehouse"].(map[string]interface{}); ok { - dto.Warehouse = toWarehouseDTO(wData) - } - - return dto -} - -func toProductDTO(pData map[string]interface{}) *productDTO.ProductBaseDTO { - if pData == nil { - return nil - } - - product := &productDTO.ProductBaseDTO{} - if id, ok := pData["id"].(float64); ok { - product.Id = uint(id) - } else if id, ok := pData["id"].(uint); ok { - product.Id = id - } - if name, ok := pData["name"].(string); ok { - product.Name = name - } - return product -} - -func toWarehouseDTO(wData map[string]interface{}) *warehouseDTO.WarehouseBaseDTO { - if wData == nil { - return nil - } - - warehouse := &warehouseDTO.WarehouseBaseDTO{} - if id, ok := wData["id"].(float64); ok { - warehouse.Id = uint(id) - } else if id, ok := wData["id"].(uint); ok { - warehouse.Id = id - } - if name, ok := wData["name"].(string); ok { - warehouse.Name = name - } - if wType, ok := wData["type"].(string); ok { - warehouse.Type = wType - } - return warehouse -} - func toCreatedUserDTO(pf entity.ProjectFlock) *userDTO.UserBaseDTO { if pf.CreatedUser.Id != 0 { mapped := userDTO.ToUserBaseDTO(pf.CreatedUser) @@ -261,78 +190,49 @@ func toChickinDTOs(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO return result } -func toAvailableQtyDTOs(chickins []entity.ProjectChickin) []AvailableQtyDTO { - if len(chickins) == 0 { - return nil - } - - availableQtyMap := make(map[uint]AvailableQtyDTO) - for _, ch := range chickins { - if ch.ProductWarehouse == nil || ch.ProductWarehouse.Quantity <= 0 { - continue - } - - if _, exists := availableQtyMap[ch.ProductWarehouseId]; exists { - continue - } - - pwDTO := &ProductWarehouseDTO{ - Id: ch.ProductWarehouse.Id, - } - - if ch.ProductWarehouse.Product.Id != 0 { - pwDTO.Product = &productDTO.ProductBaseDTO{ - Id: ch.ProductWarehouse.Product.Id, - Name: ch.ProductWarehouse.Product.Name, - } - } - - if ch.ProductWarehouse.Warehouse.Id != 0 { - pwDTO.Warehouse = &warehouseDTO.WarehouseBaseDTO{ - Id: ch.ProductWarehouse.Warehouse.Id, - Name: ch.ProductWarehouse.Warehouse.Name, - Type: ch.ProductWarehouse.Warehouse.Type, - } - } - - availableQtyMap[ch.ProductWarehouseId] = AvailableQtyDTO{ - ProductWarehouse: pwDTO, - } - } - +func toAvailableQtyDTOsFromMap(chickins []entity.ProjectChickin, availableQtyMap map[uint]float64) []AvailableQtyDTO { if len(availableQtyMap) == 0 { return nil } + pwMap := make(map[uint]*entity.ProductWarehouse) + for _, chickin := range chickins { + if chickin.ProductWarehouse != nil && chickin.ProductWarehouse.Id != 0 { + pwMap[chickin.ProductWarehouseId] = chickin.ProductWarehouse + } + } + result := make([]AvailableQtyDTO, 0, len(availableQtyMap)) - for _, v := range availableQtyMap { - result = append(result, v) - } - return result -} - -func toAvailableQtyDTOsFromRaw(availableQtysRaw []map[string]interface{}) []AvailableQtyDTO { - if len(availableQtysRaw) == 0 { - return nil - } - - result := make([]AvailableQtyDTO, len(availableQtysRaw)) - for i, v := range availableQtysRaw { - pwData, ok := v["product_warehouse"].(map[string]interface{}) - if !ok { + for pwId, availableQty := range availableQtyMap { + pw, exists := pwMap[pwId] + if !exists || pw == nil { continue } - pwDTO := toProductWarehouseDTO(pwData) - availableQty := 0.0 - if qty, ok := v["available_qty"].(float64); ok { - availableQty = qty - } + pwDTO := ToProductWarehouseDTO(pw) - result[i] = AvailableQtyDTO{ + result = append(result, AvailableQtyDTO{ AvailableQty: availableQty, ProductWarehouse: pwDTO, - } + }) } + return result } + +func ToProductWarehouseDTO(pw *entity.ProductWarehouse) *ProductWarehouseDTO { + if pw == nil { + return nil + } + + chickinPwDTO := chickinDTO.ToProductWarehouseDTO(pw) + if chickinPwDTO == nil { + return nil + } + + return &ProductWarehouseDTO{ + Id: chickinPwDTO.Id, + Product: chickinPwDTO.Product, + Warehouse: chickinPwDTO.Warehouse, + } +} diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 90e85882..fcdb31f4 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -7,7 +7,6 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" - dto "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/dto" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -19,10 +18,12 @@ import ( ) type ProjectFlockKandangService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ProjectFlockKandangWithAvailableQtysDTO, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, error) } +// Note: map[uint]float64 adalah mapping dari ProductWarehouse ID ke calculated available quantity + type projectFlockKandangService struct { Log *logrus.Logger Validate *validator.Validate @@ -45,18 +46,11 @@ func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository } } -func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ProjectFlockKandangWithAvailableQtysDTO, int64, error) { +func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } - if params.Page <= 0 { - params.Page = 1 - } - if params.Limit <= 0 { - params.Limit = 10 - } - offset := (params.Page - 1) * params.Limit projectFlockKandangs, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) @@ -85,24 +79,11 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer } } } - results := make([]dto.ProjectFlockKandangWithAvailableQtysDTO, 0) - for i := range projectFlockKandangs { - availableQtys, err := s.getAvailableQuantities(c, &projectFlockKandangs[i]) - if err != nil { - s.Log.Warnf("Failed to fetch available quantities for kandang %d: %+v", projectFlockKandangs[i].Kandang.Id, err) - availableQtys = nil - } - results = append(results, dto.ProjectFlockKandangWithAvailableQtysDTO{ - Entity: projectFlockKandangs[i], - AvailableQtys: availableQtys, - }) - } - - return results, total, nil + return projectFlockKandangs, total, nil } -func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, []map[string]interface{}, error) { +func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, error) { projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") @@ -122,16 +103,16 @@ func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Proje } } - availableQtys, err := s.getAvailableQuantities(c, projectFlockKandang) + availableQtyMap, err := s.getAvailableQuantities(c, projectFlockKandang) if err != nil { s.Log.Errorf("Failed to fetch available quantities for kandang %d: %+v", projectFlockKandang.Kandang.Id, err) - availableQtys = nil + availableQtyMap = nil } - return projectFlockKandang, availableQtys, nil + return projectFlockKandang, availableQtyMap, nil } -func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) ([]map[string]interface{}, error) { +func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) (map[uint]float64, error) { if projectFlockKandang.Kandang.Id == 0 || s.WarehouseRepo == nil || s.ProductWarehouseRepo == nil { return nil, nil } @@ -155,38 +136,16 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project return nil, nil } - var result []map[string]interface{} + result := make(map[uint]float64) for _, pw := range products { availableQty, err := s.calculateAvailableQuantityForProductWarehouse(c, projectFlockKandang, &pw) if err != nil { s.Log.Warnf("Failed to calculate available quantity for product warehouse %d: %v", pw.Id, err) } - if availableQty <= 0 { - continue + if availableQty > 0 { + result[pw.Id] = availableQty } - - productData := map[string]interface{}{ - "id": pw.Product.Id, - "name": pw.Product.Name, - } - - warehouseData := map[string]interface{}{ - "id": pw.Warehouse.Id, - "name": pw.Warehouse.Name, - "type": pw.Warehouse.Type, - } - - productWarehouseData := map[string]interface{}{ - "id": pw.Id, - "product": productData, - "warehouse": warehouseData, - } - - result = append(result, map[string]interface{}{ - "available_qty": availableQty, - "product_warehouse": productWarehouseData, - }) } return result, nil diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 1a05acff..a2ab8ebe 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -189,6 +189,8 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint Preload("Chickins"). Preload("Chickins.CreatedUser"). Preload("Chickins.ProductWarehouse"). + Preload("Chickins.ProductWarehouse.Product"). + Preload("Chickins.ProductWarehouse.Warehouse"). First(record, id).Error; err != nil { return nil, err } @@ -210,6 +212,8 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont Preload("Chickins"). Preload("Chickins.CreatedUser"). Preload("Chickins.ProductWarehouse"). + Preload("Chickins.ProductWarehouse.Product"). + Preload("Chickins.ProductWarehouse.Warehouse"). First(record).Error; err != nil { return nil, err } From d21aaead7babff8c0c91b7b5ed0f8fa13854b622 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 7 Nov 2025 07:58:00 +0700 Subject: [PATCH 09/29] Fix[BE]: use new request body for frontend requrement on chickin create API --- .../chickins/services/chickin.service.go | 49 +++++++++---------- .../validations/chickin.validation.go | 11 +++-- 2 files changed, 30 insertions(+), 30 deletions(-) diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 974b5c9f..a130740a 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -123,39 +123,34 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found") } - var productWarehouses []entity.ProductWarehouse category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category)) - var productCategoryCode string - switch category { - case string(utils.ProjectFlockCategoryGrowing): - productCategoryCode = "DOC" - case string(utils.ProjectFlockCategoryLaying): - productCategoryCode = "PULLET" - default: - return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Unknown category: %s", category)) - } - - productWarehouses, err = s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), productCategoryCode, warehouse.Id) - if err != nil || len(productWarehouses) == 0 { - return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product for %s category in the Kandang's warehouse not found", strings.ToLower(category))) - } - - chickinDate, err := utils.ParseDateString(req.ChickInDate) - if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format") - } - actorID := uint(1) // todo nanti ambil dari auth context newChikins := make([]*entity.ProjectChickin, 0) - for _, productWarehouse := range productWarehouses { - availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, &productWarehouse, category) + + for _, chickinReq := range req.ChickinRequests { + + productWarehouse, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickinReq.ProductWarehouseId, nil) if err != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", productWarehouse.Id)) + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", chickinReq.ProductWarehouseId)) + } + + if productWarehouse.WarehouseId != warehouse.Id { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d is not bound to kandang's warehouse", chickinReq.ProductWarehouseId)) + } + + chickinDate, err := utils.ParseDateString(chickinReq.ChickInDate) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid ChickInDate format for product warehouse %d", chickinReq.ProductWarehouseId)) + } + + availableQty, err := s.calculateAvailableQuantity(c, req.ProjectFlockKandangId, productWarehouse, category) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to calculate available quantity for product warehouse %d", chickinReq.ProductWarehouseId)) } if availableQty <= 0 { - continue + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No available quantity for product warehouse %d", chickinReq.ProductWarehouseId)) } newChickin := &entity.ProjectChickin{ @@ -163,8 +158,8 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti ChickInDate: chickinDate, UsageQty: 0, PendingUsageQty: availableQty, - ProductWarehouseId: productWarehouse.Id, - Notes: req.Note, + ProductWarehouseId: chickinReq.ProductWarehouseId, + Notes: chickinReq.Note, CreatedBy: actorID, } diff --git a/internal/modules/production/chickins/validations/chickin.validation.go b/internal/modules/production/chickins/validations/chickin.validation.go index c2676130..ebc6487f 100644 --- a/internal/modules/production/chickins/validations/chickin.validation.go +++ b/internal/modules/production/chickins/validations/chickin.validation.go @@ -1,9 +1,14 @@ package validation type Create struct { - ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` - ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"` - Note string `json:"note" validate:"omitempty"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` + ChickinRequests []ChickinRequestItem `json:"chickin_requests" validate:"required,min=1,dive"` +} + +type ChickinRequestItem struct { + ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"` + ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,number,min=1"` + Note string `json:"note" validate:"omitempty"` } type Update struct { From ba12320d12b86c4d7aa06c261781945e6b001b80 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 7 Nov 2025 09:01:37 +0700 Subject: [PATCH 10/29] Feat[BE-221]: create So DO migration --- .../20251107013921_create_marketings.down.sql | 5 +++ .../20251107013921_create_marketings.up.sql | 44 +++++++++++++++++++ ...7015122_create_marketing_products.down.sql | 1 + ...107015122_create_marketing_products.up.sql | 34 ++++++++++++++ ...eate_marketing_product_deliveries.down.sql | 2 + ...create_marketing_product_deliveries.up.sql | 29 ++++++++++++ 6 files changed, 115 insertions(+) create mode 100644 internal/database/migrations/20251107013921_create_marketings.down.sql create mode 100644 internal/database/migrations/20251107013921_create_marketings.up.sql create mode 100644 internal/database/migrations/20251107015122_create_marketing_products.down.sql create mode 100644 internal/database/migrations/20251107015122_create_marketing_products.up.sql create mode 100644 internal/database/migrations/20251107015128_create_marketing_product_deliveries.down.sql create mode 100644 internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql diff --git a/internal/database/migrations/20251107013921_create_marketings.down.sql b/internal/database/migrations/20251107013921_create_marketings.down.sql new file mode 100644 index 00000000..df4a0db9 --- /dev/null +++ b/internal/database/migrations/20251107013921_create_marketings.down.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS marketing_delivery_products CASCADE; + +DROP TABLE IF EXISTS marketing_products CASCADE; + +DROP TABLE IF EXISTS marketings CASCADE; \ No newline at end of file diff --git a/internal/database/migrations/20251107013921_create_marketings.up.sql b/internal/database/migrations/20251107013921_create_marketings.up.sql new file mode 100644 index 00000000..d2a3e24f --- /dev/null +++ b/internal/database/migrations/20251107013921_create_marketings.up.sql @@ -0,0 +1,44 @@ +CREATE TABLE marketings ( + id BIGSERIAL PRIMARY KEY, + so_number VARCHAR(255) UNIQUE NOT NULL, + customer_id BIGINT NOT NULL, + so_docs VARCHAR(20), + so_date DATE NOT NULL, + sales_person_id BIGINT NOT NULL, + notes TEXT, + created_by BIGINT NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'customers') THEN + ALTER TABLE marketings + ADD CONSTRAINT fk_marketings_customer_id + FOREIGN KEY (customer_id) REFERENCES customers(id) ON DELETE RESTRICT; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE marketings + ADD CONSTRAINT fk_marketings_sales_person_id + FOREIGN KEY (sales_person_id) REFERENCES users(id) ON DELETE RESTRICT; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE marketings + ADD CONSTRAINT fk_marketings_created_by + FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE RESTRICT; + END IF; +END $$; + +CREATE INDEX idx_marketings_customer_id ON marketings (customer_id); + +CREATE INDEX idx_marketings_sales_person_id ON marketings (sales_person_id); + +CREATE INDEX idx_marketings_created_by ON marketings (created_by); + +CREATE INDEX idx_marketings_so_date ON marketings (so_date); + +CREATE INDEX idx_marketings_deleted_at ON marketings (deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251107015122_create_marketing_products.down.sql b/internal/database/migrations/20251107015122_create_marketing_products.down.sql new file mode 100644 index 00000000..c40efdb3 --- /dev/null +++ b/internal/database/migrations/20251107015122_create_marketing_products.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS marketing_products CASCADE; \ No newline at end of file diff --git a/internal/database/migrations/20251107015122_create_marketing_products.up.sql b/internal/database/migrations/20251107015122_create_marketing_products.up.sql new file mode 100644 index 00000000..5490f931 --- /dev/null +++ b/internal/database/migrations/20251107015122_create_marketing_products.up.sql @@ -0,0 +1,34 @@ +CREATE TABLE marketing_products ( + id BIGSERIAL PRIMARY KEY, + marketing_id BIGINT NOT NULL, + product_warehouse_id BIGINT NOT NULL, + qty NUMERIC(15, 3) NOT NULL, + unit_price NUMERIC(15, 3) NOT NULL, + avg_weight NUMERIC(15, 3) NOT NULL, + total_weight NUMERIC(15, 3) NOT NULL, + total_price NUMERIC(15, 3) NOT NULL, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'marketings') THEN + ALTER TABLE marketing_products + ADD CONSTRAINT fk_marketing_products_marketing_id + FOREIGN KEY (marketing_id) REFERENCES marketings(id) ON DELETE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN + ALTER TABLE marketing_products + ADD CONSTRAINT fk_marketing_products_product_warehouse_id + FOREIGN KEY (product_warehouse_id) REFERENCES product_warehouses(id) ON DELETE RESTRICT; + END IF; +END $$; + +CREATE INDEX idx_marketing_products_marketing_id ON marketing_products (marketing_id); + +CREATE INDEX idx_marketing_products_product_warehouse_id ON marketing_products (product_warehouse_id); + +CREATE INDEX idx_marketing_products_deleted_at ON marketing_products (deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251107015128_create_marketing_product_deliveries.down.sql b/internal/database/migrations/20251107015128_create_marketing_product_deliveries.down.sql new file mode 100644 index 00000000..20da9516 --- /dev/null +++ b/internal/database/migrations/20251107015128_create_marketing_product_deliveries.down.sql @@ -0,0 +1,2 @@ + +DROP TABLE IF EXISTS marketing_delivery_products CASCADE; \ No newline at end of file diff --git a/internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql b/internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql new file mode 100644 index 00000000..09625c16 --- /dev/null +++ b/internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql @@ -0,0 +1,29 @@ +CREATE TABLE marketing_delivery_products ( + id BIGSERIAL PRIMARY KEY, + marketing_product_id BIGINT UNIQUE NOT NULL, + qty NUMERIC(15, 3) NOT NULL, + unit_price NUMERIC(15, 3) NOT NULL, + total_weight NUMERIC(15, 3) NOT NULL, + avg_weight NUMERIC(15, 3) NOT NULL, + total_price NUMERIC(15, 3) NOT NULL, + delivery_date TIMESTAMPTZ, + vehicle_number VARCHAR(50), + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), + deleted_at TIMESTAMPTZ +); + +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'marketing_products') THEN + ALTER TABLE marketing_delivery_products + ADD CONSTRAINT fk_marketing_delivery_products_marketing_product_id + FOREIGN KEY (marketing_product_id) REFERENCES marketing_products(id) ON DELETE CASCADE; + END IF; +END $$; + +CREATE INDEX idx_marketing_delivery_products_marketing_product_id ON marketing_delivery_products (marketing_product_id); + +CREATE INDEX idx_marketing_delivery_products_delivery_date ON marketing_delivery_products (delivery_date); + +CREATE INDEX idx_marketing_delivery_products_deleted_at ON marketing_delivery_products (deleted_at); \ No newline at end of file From 6e69e97d269f857abc19e86615715914df5cdcb5 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 7 Nov 2025 13:24:48 +0700 Subject: [PATCH 11/29] feat[BE-127]: create available qty API and inisiate sales order and delivery order --- internal/entities/marketing.go | 26 ++++ .../entities/marketing_delivery_product.go | 24 +++ internal/entities/marketing_product.go | 24 +++ internal/entities/sales-orders.go | 18 +++ internal/modules/marketing/module.go | 13 ++ internal/modules/marketing/route.go | 25 +++ .../controllers/sales-orders.controller.go | 144 ++++++++++++++++++ .../sales-orders/dto/sales-orders.dto.go | 64 ++++++++ .../modules/marketing/sales-orders/module.go | 26 ++++ .../repositories/sales-orders.repository.go | 21 +++ .../modules/marketing/sales-orders/route.go | 28 ++++ .../services/sales-orders.service.go | 129 ++++++++++++++++ .../validations/sales-orders.validation.go | 15 ++ .../controllers/projectflock.controller.go | 4 + .../project_flock_population_repository.go | 15 ++ .../repositories/projectflock.repository.go | 3 + .../production/project_flocks/route.go | 2 +- .../validations/projectflock.validation.go | 1 + .../controllers/transfer_laying.controller.go | 63 ++++++-- .../dto/transfer_laying.dto.go | 15 +- .../production/transfer_layings/route.go | 1 + .../services/transfer_laying.service.go | 88 ++++++----- .../validations/transfer_laying.validation.go | 12 +- internal/route/route.go | 2 + 24 files changed, 695 insertions(+), 68 deletions(-) create mode 100644 internal/entities/marketing.go create mode 100644 internal/entities/marketing_delivery_product.go create mode 100644 internal/entities/marketing_product.go create mode 100644 internal/entities/sales-orders.go create mode 100644 internal/modules/marketing/module.go create mode 100644 internal/modules/marketing/route.go create mode 100644 internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go create mode 100644 internal/modules/marketing/sales-orders/dto/sales-orders.dto.go create mode 100644 internal/modules/marketing/sales-orders/module.go create mode 100644 internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go create mode 100644 internal/modules/marketing/sales-orders/route.go create mode 100644 internal/modules/marketing/sales-orders/services/sales-orders.service.go create mode 100644 internal/modules/marketing/sales-orders/validations/sales-orders.validation.go diff --git a/internal/entities/marketing.go b/internal/entities/marketing.go new file mode 100644 index 00000000..1ae4d8c3 --- /dev/null +++ b/internal/entities/marketing.go @@ -0,0 +1,26 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type Marketing struct { + Id uint `gorm:"primaryKey;autoIncrement"` + SoNumber string `gorm:"uniqueIndex;not null"` + CustomerId uint `gorm:"not null"` + SoDocs string `gorm:"type:varchar(20)"` + SoDate time.Time `gorm:"type:date;not null"` + SalesPersonId uint `gorm:"not null"` + Notes string `gorm:"type:text"` + CreatedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Customer Customer `gorm:"foreignKey:CustomerId;references:Id"` + SalesPerson User `gorm:"foreignKey:SalesPersonId;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + Products []MarketingProduct `gorm:"foreignKey:MarketingId;references:Id"` +} diff --git a/internal/entities/marketing_delivery_product.go b/internal/entities/marketing_delivery_product.go new file mode 100644 index 00000000..85b4591a --- /dev/null +++ b/internal/entities/marketing_delivery_product.go @@ -0,0 +1,24 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type MarketingDeliveryProduct struct { + Id uint `gorm:"primaryKey;autoIncrement"` + MarketingProductId uint `gorm:"uniqueIndex;not null"` + Qty float64 `gorm:"type:numeric(15,3);not null"` + UnitPrice float64 `gorm:"type:numeric(15,3);not null"` + TotalWeight float64 `gorm:"type:numeric(15,3);not null"` + AvgWeight float64 `gorm:"type:numeric(15,3);not null"` + TotalPrice float64 `gorm:"type:numeric(15,3);not null"` + DeliveryDate *time.Time `gorm:"type:timestamptz"` + VehicleNumber string `gorm:"type:varchar(50)"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"` +} diff --git a/internal/entities/marketing_product.go b/internal/entities/marketing_product.go new file mode 100644 index 00000000..2e6aef58 --- /dev/null +++ b/internal/entities/marketing_product.go @@ -0,0 +1,24 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type MarketingProduct struct { + Id uint `gorm:"primaryKey;autoIncrement"` + MarketingId uint `gorm:"not null"` + ProductWarehouseId uint `gorm:"not null"` + Qty float64 `gorm:"type:numeric(15,3);not null"` + UnitPrice float64 `gorm:"type:numeric(15,3);not null"` + AvgWeight float64 `gorm:"type:numeric(15,3);not null"` + TotalWeight float64 `gorm:"type:numeric(15,3);not null"` + TotalPrice float64 `gorm:"type:numeric(15,3);not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` + ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` +} diff --git a/internal/entities/sales-orders.go b/internal/entities/sales-orders.go new file mode 100644 index 00000000..48615607 --- /dev/null +++ b/internal/entities/sales-orders.go @@ -0,0 +1,18 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type SalesOrders struct { + Id uint `gorm:"primaryKey"` + Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"` + CreatedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/modules/marketing/module.go b/internal/modules/marketing/module.go new file mode 100644 index 00000000..9bf4f018 --- /dev/null +++ b/internal/modules/marketing/module.go @@ -0,0 +1,13 @@ +package marketing + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" +) + +type MarketingModule struct{} + +func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + RegisterRoutes(router, db, validate) +} diff --git a/internal/modules/marketing/route.go b/internal/modules/marketing/route.go new file mode 100644 index 00000000..1b72b8cb --- /dev/null +++ b/internal/modules/marketing/route.go @@ -0,0 +1,25 @@ +package marketing + +import ( + "gitlab.com/mbugroup/lti-api.git/internal/modules" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + salesOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders" + // MODULE IMPORTS +) + +func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + group := router.Group("/marketing") + + allModules := []modules.Module{ + salesOrderss.SalesOrdersModule{}, + // MODULE REGISTRY + } + + for _, m := range allModules { + m.RegisterRoutes(group, db, validate) + } +} diff --git a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go new file mode 100644 index 00000000..837a8ee0 --- /dev/null +++ b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go @@ -0,0 +1,144 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type SalesOrdersController struct { + SalesOrdersService service.SalesOrdersService +} + +func NewSalesOrdersController(salesOrdersService service.SalesOrdersService) *SalesOrdersController { + return &SalesOrdersController{ + SalesOrdersService: salesOrdersService, + } +} + +func (u *SalesOrdersController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := u.SalesOrdersService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.SalesOrdersListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all salesOrderss successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToSalesOrdersListDTOs(result), + }) +} + +func (u *SalesOrdersController) GetOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + result, err := u.SalesOrdersService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get salesOrders successfully", + Data: dto.ToSalesOrdersListDTO(*result), + }) +} + +func (u *SalesOrdersController) CreateOne(c *fiber.Ctx) error { + req := new(validation.Create) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + // result, err := u.SalesOrdersService.CreateOne(c, req) + // if err != nil { + // return err + // } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create salesOrders successfully", + Data: req, + }) +} + +func (u *SalesOrdersController) UpdateOne(c *fiber.Ctx) error { + req := new(validation.Update) + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.SalesOrdersService.UpdateOne(c, req, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Update salesOrders successfully", + Data: dto.ToSalesOrdersListDTO(*result), + }) +} + +func (u *SalesOrdersController) DeleteOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + if err := u.SalesOrdersService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete salesOrders successfully", + }) +} diff --git a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go new file mode 100644 index 00000000..6dd7c8e3 --- /dev/null +++ b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go @@ -0,0 +1,64 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +// === DTO Structs === + +type SalesOrdersBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type SalesOrdersListDTO struct { + SalesOrdersBaseDTO + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type SalesOrdersDetailDTO struct { + SalesOrdersListDTO +} + +// === Mapper Functions === + +func ToSalesOrdersBaseDTO(e entity.SalesOrders) SalesOrdersBaseDTO { + return SalesOrdersBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToSalesOrdersListDTO(e entity.SalesOrders) SalesOrdersListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + createdUser = &mapped + } + + return SalesOrdersListDTO{ + SalesOrdersBaseDTO: ToSalesOrdersBaseDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToSalesOrdersListDTOs(e []entity.SalesOrders) []SalesOrdersListDTO { + result := make([]SalesOrdersListDTO, len(e)) + for i, r := range e { + result[i] = ToSalesOrdersListDTO(r) + } + return result +} + +func ToSalesOrdersDetailDTO(e entity.SalesOrders) SalesOrdersDetailDTO { + return SalesOrdersDetailDTO{ + SalesOrdersListDTO: ToSalesOrdersListDTO(e), + } +} diff --git a/internal/modules/marketing/sales-orders/module.go b/internal/modules/marketing/sales-orders/module.go new file mode 100644 index 00000000..12310354 --- /dev/null +++ b/internal/modules/marketing/sales-orders/module.go @@ -0,0 +1,26 @@ +package sales_orders + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" + sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type SalesOrdersModule struct{} + +func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + salesOrdersRepo := rSalesOrders.NewSalesOrdersRepository(db) + userRepo := rUser.NewUserRepository(db) + + salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + SalesOrdersRoutes(router, userService, salesOrdersService) +} + diff --git a/internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go b/internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go new file mode 100644 index 00000000..5f8cfe79 --- /dev/null +++ b/internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + "gorm.io/gorm" +) + +type SalesOrdersRepository interface { + repository.BaseRepository[entity.SalesOrders] +} + +type SalesOrdersRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.SalesOrders] +} + +func NewSalesOrdersRepository(db *gorm.DB) SalesOrdersRepository { + return &SalesOrdersRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.SalesOrders](db), + } +} diff --git a/internal/modules/marketing/sales-orders/route.go b/internal/modules/marketing/sales-orders/route.go new file mode 100644 index 00000000..455977fb --- /dev/null +++ b/internal/modules/marketing/sales-orders/route.go @@ -0,0 +1,28 @@ +package sales_orders + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/controllers" + salesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesOrdersService) { + ctrl := controller.NewSalesOrdersController(s) + + route := v1.Group("/sales-orders") + + // route.Get("/", m.Auth(u), ctrl.GetAll) + // route.Post("/", m.Auth(u), ctrl.CreateOne) + // route.Get("/:id", m.Auth(u), ctrl.GetOne) + // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) + // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) + + route.Get("/", ctrl.GetAll) + route.Post("/", ctrl.CreateOne) + route.Get("/:id", ctrl.GetOne) + route.Patch("/:id", ctrl.UpdateOne) + route.Delete("/:id", ctrl.DeleteOne) +} diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go new file mode 100644 index 00000000..33c93f6d --- /dev/null +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -0,0 +1,129 @@ +package service + +import ( + "errors" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type SalesOrdersService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.SalesOrders, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.SalesOrders, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.SalesOrders, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.SalesOrders, error) + DeleteOne(ctx *fiber.Ctx, id uint) error +} + +type salesOrdersService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.SalesOrdersRepository +} + +func NewSalesOrdersService(repo repository.SalesOrdersRepository, validate *validator.Validate) SalesOrdersService { + return &salesOrdersService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + } +} + +func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser") +} + +func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.SalesOrders, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + salesOrderss, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + if params.Search != "" { + return db.Where("name LIKE ?", "%"+params.Search+"%") + } + return db.Order("created_at DESC").Order("updated_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get salesOrderss: %+v", err) + return nil, 0, err + } + return salesOrderss, total, nil +} + +func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.SalesOrders, error) { + salesOrders, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") + } + if err != nil { + s.Log.Errorf("Failed get salesOrders by id: %+v", err) + return nil, err + } + return salesOrders, nil +} + +func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.SalesOrders, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + createBody := &entity.SalesOrders{ + Name: req.Name, + } + + if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create salesOrders: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.SalesOrders, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + updateBody := make(map[string]any) + + if req.Name != nil { + updateBody["name"] = *req.Name + } + + if len(updateBody) == 0 { + return s.GetOne(c, id) + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") + } + s.Log.Errorf("Failed to update salesOrders: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") + } + s.Log.Errorf("Failed to delete salesOrders: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go new file mode 100644 index 00000000..7d16d3ee --- /dev/null +++ b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go @@ -0,0 +1,15 @@ +package validation + +type Create struct { + Name string `json:"name" validate:"required_strict,min=3"` +} + +type Update struct { + Name *string `json:"name,omitempty" validate:"omitempty"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=50"` +} diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index d3b0061c..cf2dff05 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -71,6 +71,10 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error { if period := c.QueryInt("period", 0); period > 0 { query.Period = period } + if category := c.Query("category", ""); category != "" { + query.Category = category + + } if kandangRaw := c.Query("kandang_id", c.Query("kandang_ids", "")); kandangRaw != "" { ids, err := parseUintList(kandangRaw) diff --git a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go index d914b128..a2b56dce 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -14,6 +14,7 @@ type ProjectFlockPopulationRepository interface { ExistsByProjectChickinID(ctx context.Context, projectChickinID uint) (bool, error) GetByProjectChickinIDAndProductWarehouseID(ctx context.Context, projectChickinID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) GetByProjectFlockKandangIDAndProductWarehouseID(ctx context.Context, projectFlockKandangID uint, productWarehouseID uint) ([]entity.ProjectFlockPopulation, error) + GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) // subset of base repository methods used by services CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error @@ -91,3 +92,17 @@ func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangIDAndProd } return records, nil } + +func (r *projectFlockPopulationRepositoryImpl) GetTotalQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error) { + var total float64 + err := r.DB().WithContext(ctx). + Table("project_flock_populations"). + Select("COALESCE(SUM(total_qty), 0) AS total_qty"). + Joins("JOIN project_chickins ON project_chickins.id = project_flock_populations.project_chickin_id"). + Where("project_chickins.project_flock_kandang_id = ?", projectFlockKandangID). + Scan(&total).Error + if err != nil { + return 0, err + } + return total, nil +} diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index a8fab919..41cbde12 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -127,6 +127,9 @@ func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *vali return db } + if params.Category != "" { + db = db.Where("project_flocks.category = ?", params.Category) + } if params.AreaId > 0 { db = db.Where("project_flocks.area_id = ?", params.AreaId) } diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index 70a22bad..a8d4ca6d 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -28,5 +28,5 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Post("/approvals", ctrl.Approval) route.Get("/kandangs/:project-flock_kandang-id/periods", ctrl.GetFlockPeriodSummary) - + } diff --git a/internal/modules/production/project_flocks/validations/projectflock.validation.go b/internal/modules/production/project_flocks/validations/projectflock.validation.go index 7932e07e..33f20725 100644 --- a/internal/modules/production/project_flocks/validations/projectflock.validation.go +++ b/internal/modules/production/project_flocks/validations/projectflock.validation.go @@ -27,6 +27,7 @@ type Query struct { AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"` LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"` Period int `query:"period" validate:"omitempty,number,gt=0"` + Category string `query:"category" validate:"omitempty"` KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"` } diff --git a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go index 62d64127..c69f4ff5 100644 --- a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go +++ b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go @@ -1,6 +1,7 @@ package controller import ( + "fmt" "math" "strconv" @@ -24,16 +25,8 @@ func NewTransferLayingController(transferLayingService service.TransferLayingSer func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - SourceProjectFlockId: uint(c.QueryInt("source_project_flock_id", 0)), - TargetProjectFlockId: uint(c.QueryInt("target_project_flock_id", 0)), - TransferDateFrom: c.Query("transfer_date_from", ""), - TransferDateTo: c.Query("transfer_date_to", ""), - ApprovalStatus: c.Query("approval_status", ""), - TransferNumber: c.Query("transfer_number", ""), - Sort: c.Query("sort", "created_at"), - Order: c.Query("order", "desc"), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), } if query.Page < 1 || query.Limit < 1 { @@ -45,8 +38,13 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { return err } + data := make([]dto.TransferLayingDetailDTO, len(result)) + for i, transfer := range result { + data[i] = dto.ToTransferLayingDetailDTOWithSingleApproval(transfer, transfer.LatestApproval) + } + return c.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.TransferLayingListDTO]{ + JSON(response.SuccessWithPaginate[dto.TransferLayingDetailDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Get all transferLayings successfully", @@ -56,7 +54,7 @@ func (u *TransferLayingController) GetAll(c *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: dto.ToTransferLayingListDTOs(result), + Data: data, }) } @@ -113,7 +111,7 @@ func (u *TransferLayingController) UpdateOne(c *fiber.Ctx) error { } if err := c.BodyParser(req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid request body: %s", err.Error())) } result, err := u.TransferLayingService.UpdateOne(c, req, uint(id)) @@ -126,7 +124,7 @@ func (u *TransferLayingController) UpdateOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Update transferLaying successfully", - Data: dto.ToTransferLayingListDTO(*result), + Data: dto.ToTransferLayingDetailDTOWithSingleApproval(*result, result.LatestApproval), }) } @@ -180,3 +178,40 @@ func (u *TransferLayingController) Approval(c *fiber.Ctx) error { Data: data, }) } + + +func (u *TransferLayingController) GetAvailableQtyPerKandang(c *fiber.Ctx) error { + projectFlockID, err := strconv.ParseUint(c.Params("project_flock_id"), 10, 32) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") + } + + pf, kandangQtyMap, err := u.TransferLayingService.GetAvailableQtyPerKandang(c, uint(projectFlockID)) + if err != nil { + return err + } + + // Build kandang list response + kandangs := make([]dto.KandangAvailableQtyDTO, 0, len(kandangQtyMap)) + for kandangPFID, qty := range kandangQtyMap { + kandangs = append(kandangs, dto.KandangAvailableQtyDTO{ + ProjectFlockKandangId: kandangPFID, + AvailableQty: qty, + }) + } + + resp := dto.AvailableQtyForTransferDTO{ + ProjectFlockId: pf.Id, + ProjectFlockCode: pf.FlockName, + Category: pf.Category, + Kandangs: kandangs, + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get available quantity successfully", + Data: resp, + }) +} diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index 719e458a..087f146a 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -77,6 +77,20 @@ type TransferLayingDetailDTO struct { Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` } +// === Available Quantity DTOs === + +type KandangAvailableQtyDTO struct { + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + AvailableQty float64 `json:"available_qty"` +} + +type AvailableQtyForTransferDTO struct { + ProjectFlockId uint `json:"project_flock_id"` + ProjectFlockCode string `json:"project_flock_code"` + Category string `json:"category"` + Kandangs []KandangAvailableQtyDTO `json:"kandangs"` +} + // === Mapper Functions === func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO { @@ -207,7 +221,6 @@ func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO { func ToTransferLayingDetailDTO(e entity.LayingTransfer, approvals []entity.Approval) TransferLayingDetailDTO { var latestApproval *approvalDTO.ApprovalBaseDTO - // Use LatestApproval from entity if available if e.LatestApproval != nil { mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) latestApproval = &mapped diff --git a/internal/modules/production/transfer_layings/route.go b/internal/modules/production/transfer_layings/route.go index 13a0bc8f..ad0cb9e1 100644 --- a/internal/modules/production/transfer_layings/route.go +++ b/internal/modules/production/transfer_layings/route.go @@ -27,4 +27,5 @@ func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying. route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) route.Post("/approvals", ctrl.Approval) + route.Get("/project-flocks/:project_flock_id/available-qty", ctrl.GetAvailableQtyPerKandang) } diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index 06c297bc..e7591aef 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -31,6 +31,7 @@ type TransferLayingService interface { UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) DeleteOne(ctx *fiber.Ctx, id uint) error Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.LayingTransfer, error) + GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) } type transferLayingService struct { @@ -92,32 +93,7 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) - if params.SourceProjectFlockId != 0 { - db = db.Where("from_project_flock_id = ?", params.SourceProjectFlockId) - } - if params.TargetProjectFlockId != 0 { - db = db.Where("to_project_flock_id = ?", params.TargetProjectFlockId) - } - if params.TransferDateFrom != "" { - db = db.Where("transfer_date >= ?", params.TransferDateFrom) - } - if params.TransferDateTo != "" { - db = db.Where("transfer_date <= ?", params.TransferDateTo) - } - if params.TransferNumber != "" { - db = db.Where("transfer_number ILIKE ?", "%"+params.TransferNumber+"%") - } - - sortField := "created_at" - if params.Sort != "" { - sortField = params.Sort - } - sortOrder := "DESC" - if params.Order == "asc" { - sortOrder = "ASC" - } - db = db.Order(fmt.Sprintf("%s %s", sortField, sortOrder)) - + db = db.Order("created_at DESC") return db }) @@ -126,19 +102,14 @@ func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([ return nil, 0, err } - if params.ApprovalStatus != "" { - var filtered []entity.LayingTransfer - approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) - - for _, transfer := range transferLayings { - latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, nil) - if err == nil && latestApproval != nil && latestApproval.Action != nil { - if string(*latestApproval.Action) == params.ApprovalStatus { - filtered = append(filtered, transfer) - } - } + approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) + for i, transfer := range transferLayings { + latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transfer.Id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err == nil && latestApproval != nil { + transferLayings[i].LatestApproval = latestApproval } - transferLayings = filtered } return transferLayings, total, nil @@ -155,7 +126,9 @@ func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTran } approvalRepo := commonRepo.NewApprovalRepository(s.Repository.DB()) - latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, nil) + latestApproval, err := approvalRepo.LatestByTarget(c.Context(), string(utils.ApprovalWorkflowTransferToLaying), transferLaying.Id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) if err == nil && latestApproval != nil { transferLaying.LatestApproval = latestApproval } @@ -547,7 +520,6 @@ func (s transferLayingService) DeleteOne(c *fiber.Ctx, id uint) error { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete transfer laying with status %s", action)) } } - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(dbTransaction) @@ -851,8 +823,6 @@ func (s *transferLayingService) restoreProjectFlockPopulation(ctx context.Contex return fiber.NewError(fiber.StatusBadRequest, "No populations found for restoration") } - // Restore in LIFO order (from newest to oldest) - // Add all quantity back to the last (newest) population if len(populations) > 0 { lastPop := populations[len(populations)-1] newQty := lastPop.TotalQty + quantityToRestore @@ -863,3 +833,37 @@ func (s *transferLayingService) restoreProjectFlockPopulation(ctx context.Contex return nil } + +func (s transferLayingService) GetAvailableQtyPerKandang(ctx *fiber.Ctx, projectFlockID uint) (*entity.ProjectFlock, map[uint]float64, error) { + + pf, err := s.ProjectFlockRepo.GetByID(ctx.Context(), projectFlockID, func(db *gorm.DB) *gorm.DB { + return db + }) + if err != nil { + s.Log.Errorf("Failed to get project flock %d: %+v", projectFlockID, err) + return nil, nil, fiber.NewError(fiber.StatusNotFound, "Project flock not found") + } + + kandangs, _, err := s.ProjectFlockKandangRepo.GetAll(ctx.Context(), 0, 1000, func(db *gorm.DB) *gorm.DB { + return db.Where("project_flock_id = ?", projectFlockID).Order("kandang_id ASC") + }) + if err != nil { + s.Log.Errorf("Failed to get kandangs for project flock %d: %+v", projectFlockID, err) + return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs") + } + + kandangAvailableQty := make(map[uint]float64) + for _, kandang := range kandangs { + + totalQty, err := s.ProjectFlockPopulationRepo.GetTotalQtyByProjectFlockKandangID(ctx.Context(), kandang.Id) + if err != nil { + s.Log.Warnf("Failed to get total qty for kandang %d: %+v", kandang.Id, err) + kandangAvailableQty[kandang.Id] = 0 + continue + } + + kandangAvailableQty[kandang.Id] = totalQty + } + + return pf, kandangAvailableQty, nil +} diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go index bd117bd9..45a73e48 100644 --- a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -29,16 +29,8 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` - SourceProjectFlockId uint `query:"source_project_flock_id" validate:"omitempty"` - TargetProjectFlockId uint `query:"target_project_flock_id" validate:"omitempty"` - TransferDateFrom string `query:"transfer_date_from" validate:"omitempty,datetime=2006-01-02"` - TransferDateTo string `query:"transfer_date_to" validate:"omitempty,datetime=2006-01-02"` - ApprovalStatus string `query:"approval_status" validate:"omitempty,oneof=PENDING APPROVED REJECTED"` // Filter by latest approval status - TransferNumber string `query:"transfer_number" validate:"omitempty"` // Search by transfer number - Sort string `query:"sort" validate:"omitempty,oneof=created_at transfer_date"` // Sort by field - Order string `query:"order" validate:"omitempty,oneof=asc desc"` // Sort order + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` } type Approve struct { diff --git a/internal/route/route.go b/internal/route/route.go index d6277549..d66828c6 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -15,6 +15,7 @@ import ( production "gitlab.com/mbugroup/lti-api.git/internal/modules/production" purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases" users "gitlab.com/mbugroup/lti-api.git/internal/modules/users" + marketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing" // MODULE IMPORTS ) @@ -32,6 +33,7 @@ func Routes(app *fiber.App, db *gorm.DB) { production.ProductionModule{}, approvals.ApprovalModule{}, purchases.PurchaseModule{}, + marketing.MarketingModule{}, // MODULE REGISTRY } From 4c279baad785c8f9ae5257785edcfd7f43d9e573 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 7 Nov 2025 13:32:03 +0700 Subject: [PATCH 12/29] FIX[BE} change json response on avaibility transfer laying --- .../production/transfer_layings/dto/transfer_laying.dto.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index 087f146a..98c1a2e7 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -86,7 +86,7 @@ type KandangAvailableQtyDTO struct { type AvailableQtyForTransferDTO struct { ProjectFlockId uint `json:"project_flock_id"` - ProjectFlockCode string `json:"project_flock_code"` + ProjectFlockCode string `json:"flock_name"` Category string `json:"category"` Kandangs []KandangAvailableQtyDTO `json:"kandangs"` } From 3785d529257c3d6bc9585e92b6ab855bab8b5ac6 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 7 Nov 2025 15:26:32 +0700 Subject: [PATCH 13/29] FIX[BE]: fix get projectflock kandang periods not found --- internal/modules/production/project_flocks/route.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index a8d4ca6d..9ec64799 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -25,8 +25,8 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) + route.Get("/kandangs/:project_flock_kandang_id/periods", ctrl.GetFlockPeriodSummary) route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang) route.Post("/approvals", ctrl.Approval) - route.Get("/kandangs/:project-flock_kandang-id/periods", ctrl.GetFlockPeriodSummary) } From b2ed58c7342f16be2c1b54525b9fb7f91e8e1f30 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 7 Nov 2025 16:41:52 +0700 Subject: [PATCH 14/29] Fix[BE]: availableqty not appeared --- .../project_flock_kandang.controller.go | 4 +-- .../dto/project_flock_kandang.dto.go | 14 ++++++++--- .../services/project_flock_kandang.service.go | 25 +++++++++++++++---- .../dto/projectflock_kandang.dto.go | 25 ++++++++----------- 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index 21701826..94316dce 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -73,7 +73,7 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } - result, availableQtys, err := u.ProjectFlockKandangService.GetOne(c, uint(id)) + result, availableQtys, productWarehouses, err := u.ProjectFlockKandangService.GetOne(c, uint(id)) if err != nil { return err } @@ -83,6 +83,6 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get projectFlockKandang successfully", - Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys), + Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys, productWarehouses), }) } diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index e80e3220..35bf615d 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -97,7 +97,7 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO } } -func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtyMap map[uint]float64) ProjectFlockKandangDetailDTO { +func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtyMap map[uint]float64, productWarehouses []entity.ProductWarehouse) ProjectFlockKandangDetailDTO { var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO if e.ProjectFlock.Id != 0 { mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) @@ -116,7 +116,7 @@ func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang return ProjectFlockKandangDetailDTO{ ProjectFlockKandangListDTO: listDTO, Chickins: toChickinDTOs(e.Chickins), - AvailableQtys: toAvailableQtyDTOsFromMap(e.Chickins, availableQtyMap), + AvailableQtys: toAvailableQtyDTOsFromMap(e.Chickins, availableQtyMap, productWarehouses), } } @@ -190,11 +190,12 @@ func toChickinDTOs(chickins []entity.ProjectChickin) []chickinDTO.ChickinBaseDTO return result } -func toAvailableQtyDTOsFromMap(chickins []entity.ProjectChickin, availableQtyMap map[uint]float64) []AvailableQtyDTO { +func toAvailableQtyDTOsFromMap(chickins []entity.ProjectChickin, availableQtyMap map[uint]float64, productWarehouses []entity.ProductWarehouse) []AvailableQtyDTO { if len(availableQtyMap) == 0 { return nil } + // First, build map from chickins pwMap := make(map[uint]*entity.ProductWarehouse) for _, chickin := range chickins { if chickin.ProductWarehouse != nil && chickin.ProductWarehouse.Id != 0 { @@ -202,6 +203,13 @@ func toAvailableQtyDTOsFromMap(chickins []entity.ProjectChickin, availableQtyMap } } + // Then, add productWarehouses that are not in chickins yet + for i := range productWarehouses { + if _, exists := pwMap[productWarehouses[i].Id]; !exists { + pwMap[productWarehouses[i].Id] = &productWarehouses[i] + } + } + result := make([]AvailableQtyDTO, 0, len(availableQtyMap)) for pwId, availableQty := range availableQtyMap { pw, exists := pwMap[pwId] diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index fcdb31f4..11e8b0d5 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -19,7 +19,7 @@ import ( type ProjectFlockKandangService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) } // Note: map[uint]float64 adalah mapping dari ProductWarehouse ID ke calculated available quantity @@ -83,13 +83,13 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer return projectFlockKandangs, total, nil } -func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, error) { +func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) { projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") + return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") } if err != nil { - return nil, nil, err + return nil, nil, nil, err } if len(projectFlockKandang.Chickins) > 0 && s.ApprovalSvc != nil { @@ -109,7 +109,22 @@ func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Proje availableQtyMap = nil } - return projectFlockKandang, availableQtyMap, nil + var productWarehouses []entity.ProductWarehouse + if len(availableQtyMap) > 0 { + pwIds := make([]uint, 0, len(availableQtyMap)) + for pwId := range availableQtyMap { + pwIds = append(pwIds, pwId) + } + productWarehouses, err = s.ProductWarehouseRepo.GetByIDs(c.Context(), pwIds, func(db *gorm.DB) *gorm.DB { + return db.Preload("Product").Preload("Warehouse") + }) + if err != nil { + s.Log.Warnf("Failed to fetch product warehouses: %+v", err) + productWarehouses = []entity.ProductWarehouse{} + } + } + + return projectFlockKandang, availableQtyMap, productWarehouses, nil } func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) (map[uint]float64, error) { diff --git a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go index 24e53d28..233f655b 100644 --- a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go @@ -28,13 +28,12 @@ type ProjectFlockWithPivotDTO struct { } type ProjectFlockKandangDTO struct { - Id uint `json:"id"` - ProjectFlockKandangId uint `json:"project_flock_kandang_id"` - ProjectFlockId uint `json:"project_flock_id"` - KandangId uint `json:"kandang_id"` - Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"` - ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` - AvailableQuantity float64 `json:"available_quantity"` + Id uint `json:"id"` + + KandangId uint `json:"kandang_id"` + Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"` + ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` + AvailableQuantity float64 `json:"available_quantity"` } func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { @@ -89,12 +88,10 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD } return ProjectFlockKandangDTO{ - Id: e.Id, - ProjectFlockKandangId: e.Id, - ProjectFlockId: e.ProjectFlockId, - KandangId: e.KandangId, - Kandang: kandang, - ProjectFlock: pf, - AvailableQuantity: 0, + Id: e.Id, + KandangId: e.KandangId, + Kandang: kandang, + ProjectFlock: pf, + AvailableQuantity: 0, } } From fd0943dfafc85e388c029cb0b19d9233e9adab48 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 10 Nov 2025 14:49:46 +0700 Subject: [PATCH 15/29] feat[BE-222]: create migration create template for SO API and kandang id param on product warehouse --- .../product_warehouse.controller.go | 1 + .../inventory/product-warehouses/module.go | 5 +- .../product_warehouse.repository.go | 5 ++ .../services/product_warehouse.service.go | 32 +++++++--- .../product_warehouse.validation.go | 1 + .../controllers/sales-orders.controller.go | 12 ++-- .../modules/marketing/sales-orders/module.go | 10 ++- .../repositories/marketings.repository.go | 21 +++++++ .../services/sales-orders.service.go | 63 +++++++++++++++---- .../validations/sales-orders.validation.go | 18 +++++- .../repositories/customer.repository.go | 5 ++ .../repositories/kandang.repository.go | 5 ++ 12 files changed, 147 insertions(+), 31 deletions(-) create mode 100644 internal/modules/marketing/sales-orders/repositories/marketings.repository.go diff --git a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go index 26f23278..671d964b 100644 --- a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go +++ b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go @@ -29,6 +29,7 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error { ProductId: uint(c.QueryInt("product_id", 0)), WarehouseId: uint(c.QueryInt("warehouse_id", 0)), Flags: c.Query("flags", ""), + KandangId: uint(c.QueryInt("kandang_id", 0)), } if query.Page < 1 || query.Limit < 1 { diff --git a/internal/modules/inventory/product-warehouses/module.go b/internal/modules/inventory/product-warehouses/module.go index dfb72e8f..0dd8047e 100644 --- a/internal/modules/inventory/product-warehouses/module.go +++ b/internal/modules/inventory/product-warehouses/module.go @@ -7,6 +7,7 @@ import ( rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" sProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/services" + rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -17,10 +18,10 @@ type ProductWarehouseModule struct{} func (ProductWarehouseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) userRepo := rUser.NewUserRepository(db) + kandangRepo := rKandang.NewKandangRepository(db) - productWarehouseService := sProductWarehouse.NewProductWarehouseService(productWarehouseRepo, validate) + productWarehouseService := sProductWarehouse.NewProductWarehouseService(productWarehouseRepo, validate, kandangRepo) userService := sUser.NewUserService(userRepo, validate) ProductWarehouseRoutes(router, userService, productWarehouseService) } - diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index 151a3697..80c551eb 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -24,6 +24,7 @@ type ProductWarehouseRepository interface { ApplyFlagsFilter(db *gorm.DB, flags []string) *gorm.DB AdjustQuantities(ctx context.Context, deltas map[uint]float64, modifier func(*gorm.DB) *gorm.DB) error GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) + IdExists(ctx context.Context, id uint) (bool, error) } type ProductWarehouseRepositoryImpl struct { @@ -47,6 +48,10 @@ func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id) } +func (r *ProductWarehouseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.ProductWarehouse](ctx, r.DB(), id) +} + func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) { var count int64 query := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}). diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go index cc925970..cc7d5b85 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -6,6 +6,7 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/validations" + kandangrepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/go-playground/validator/v10" @@ -20,16 +21,18 @@ type ProductWarehouseService interface { } type productWarehouseService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.ProductWarehouseRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ProductWarehouseRepository + KandangRepo kandangrepo.KandangRepository } -func NewProductWarehouseService(repo repository.ProductWarehouseRepository, validate *validator.Validate) ProductWarehouseService { +func NewProductWarehouseService(repo repository.ProductWarehouseRepository, validate *validator.Validate, kandangRepo kandangrepo.KandangRepository) ProductWarehouseService { return &productWarehouseService{ - Log: utils.Log, - Validate: validate, - Repository: repo, + Log: utils.Log, + Validate: validate, + Repository: repo, + KandangRepo: kandangRepo, } } @@ -69,6 +72,16 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) } } + if params.KandangId > 0 { + isKandangExist, err := s.KandangRepo.IdExists(c.Context(), params.KandangId) + if err != nil { + return nil, 0, err + } + if !isKandangExist { + return nil, 0, fiber.NewError(fiber.StatusNotFound, "Kandang not found") + } + } + offset := (params.Page - 1) * params.Limit cleanFlags := utils.ParseFlags(params.Flags) @@ -80,6 +93,11 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) db = db.Where("product_id = ?", params.ProductId) } + if params.KandangId != 0 { + db = db.Joins("JOIN warehouses ON product_warehouses.warehouse_id = warehouses.id"). + Where("warehouses.kandang_id = ?", params.KandangId) + } + if params.WarehouseId != 0 { db = db.Where("warehouse_id = ?", params.WarehouseId) } diff --git a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go index 3a3acb28..322d0a00 100644 --- a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go +++ b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go @@ -18,4 +18,5 @@ type Query struct { ProductId uint `query:"product_id" validate:"omitempty,number,min=1"` WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"` Flags string `query:"flags" validate:"omitempty"` + KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"` } diff --git a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go index 837a8ee0..be578e15 100644 --- a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go +++ b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go @@ -29,7 +29,7 @@ func (u *SalesOrdersController) GetAll(c *fiber.Ctx) error { Search: c.Query("search", ""), } - if query.Page < 1 || query.Limit < 1 { + if query.Page < 1 || query.Limit < 1 { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } @@ -82,17 +82,17 @@ func (u *SalesOrdersController) CreateOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - // result, err := u.SalesOrdersService.CreateOne(c, req) - // if err != nil { - // return err - // } + result, err := u.SalesOrdersService.CreateOne(c, req) + if err != nil { + return err + } return c.Status(fiber.StatusCreated). JSON(response.Success{ Code: fiber.StatusCreated, Status: "success", Message: "Create salesOrders successfully", - Data: req, + Data: result, }) } diff --git a/internal/modules/marketing/sales-orders/module.go b/internal/modules/marketing/sales-orders/module.go index 12310354..21926f93 100644 --- a/internal/modules/marketing/sales-orders/module.go +++ b/internal/modules/marketing/sales-orders/module.go @@ -5,8 +5,11 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" + rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" + rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -17,10 +20,13 @@ type SalesOrdersModule struct{} func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { salesOrdersRepo := rSalesOrders.NewSalesOrdersRepository(db) userRepo := rUser.NewUserRepository(db) + customerRepo := rCustomer.NewCustomerRepository(db) + kandangRepo := rKandang.NewKandangRepository(db) + productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + marketingRepo := rSalesOrders.NewMarketingRepository(db) - salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, validate) + salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, kandangRepo, productWarehouseRepo, marketingRepo, validate) userService := sUser.NewUserService(userRepo, validate) SalesOrdersRoutes(router, userService, salesOrdersService) } - diff --git a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go new file mode 100644 index 00000000..bb4896cb --- /dev/null +++ b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type MarketingRepository interface { + repository.BaseRepository[entity.Marketing] +} + +type MarketingRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.Marketing] +} + +func NewMarketingRepository(db *gorm.DB) MarketingRepository { + return &MarketingRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.Marketing](db), + } +} diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 33c93f6d..847305c1 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -3,9 +3,13 @@ package service import ( "errors" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations" + customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" + kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/go-playground/validator/v10" @@ -23,16 +27,24 @@ type SalesOrdersService interface { } type salesOrdersService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.SalesOrdersRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.SalesOrdersRepository + CustomerRepo customerRepo.CustomerRepository + KandangRepo kandangRepo.KandangRepository + ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository + MarketingRepo repository.MarketingRepository } -func NewSalesOrdersService(repo repository.SalesOrdersRepository, validate *validator.Validate) SalesOrdersService { +func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, kandangRepo kandangRepo.KandangRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, validate *validator.Validate) SalesOrdersService { return &salesOrdersService{ - Log: utils.Log, - Validate: validate, - Repository: repo, + Log: utils.Log, + Validate: validate, + Repository: repo, + CustomerRepo: customerRepo, + KandangRepo: kandangRepo, + ProductWarehouseRepo: productWarehouseRepo, + MarketingRepo: marketingRepo, } } @@ -79,13 +91,40 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, err } - createBody := &entity.SalesOrders{ - Name: req.Name, + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists}, + ); err != nil { + return nil, err } - if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { - s.Log.Errorf("Failed to create salesOrders: %+v", err) - return nil, err + for _, item := range req.MarketingProducts { + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Kandang", ID: &item.KandangId, Exists: s.KandangRepo.IdExists}, + commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, + ); err != nil { + return nil, err + } + } + + createBody := &entity.Marketing{ + CustomerId: req.CustomerId, + } + + err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + + if err := marketingRepoTx.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create marketing: %+v", err) + return err + } + + return nil + }) + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders") } return s.GetOne(c, createBody.Id) diff --git a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go index 7d16d3ee..b09129bc 100644 --- a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go +++ b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go @@ -1,11 +1,25 @@ package validation type Create struct { - Name string `json:"name" validate:"required_strict,min=3"` + CustomerId uint `json:"customer_id" validate:"required,gt=0"` + Date string `json:"date" validate:"required,datetime=2006-01-02"` + Notes string `json:"notes" validate:"omitempty,max=500"` + MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"required,min=1,dive"` +} + +type CreateMarketingProduct struct { + VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"` + KandangId uint `json:"kandang_id" validate:"required,gt=0"` + ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"` + UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` + TotalWeight float64 `json:"total_weight" validate:"required,gt=0"` + Qty float64 `json:"qty" validate:"required,gt=0"` + AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` + TotalPrice float64 `json:"total_price" validate:"required,gt=0"` } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + Name *string `json:"name,omitempty" validate:"omitempty"` } type Query struct { diff --git a/internal/modules/master/customers/repositories/customer.repository.go b/internal/modules/master/customers/repositories/customer.repository.go index 13a08211..68877548 100644 --- a/internal/modules/master/customers/repositories/customer.repository.go +++ b/internal/modules/master/customers/repositories/customer.repository.go @@ -12,6 +12,7 @@ type CustomerRepository interface { repository.BaseRepository[entity.Customer] PicExists(ctx context.Context, areaId uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) + IdExists(ctx context.Context, id uint) (bool, error) } type CustomerRepositoryImpl struct { @@ -33,3 +34,7 @@ func (r *CustomerRepositoryImpl) PicExists(ctx context.Context, picId uint) (boo func (r *CustomerRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) { return repository.ExistsByName[entity.Customer](ctx, r.db, name, excludeID) } + +func (r *CustomerRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.Customer](ctx, r.db, id) +} diff --git a/internal/modules/master/kandangs/repositories/kandang.repository.go b/internal/modules/master/kandangs/repositories/kandang.repository.go index 8f32a7b2..04b6a914 100644 --- a/internal/modules/master/kandangs/repositories/kandang.repository.go +++ b/internal/modules/master/kandangs/repositories/kandang.repository.go @@ -21,6 +21,7 @@ type KandangRepository interface { UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error UpsertProjectFlockKandang(ctx context.Context, projectFlockID, kandangID uint) error UpdateStatusByIDs(ctx context.Context, kandangIDs []uint, status utils.KandangStatus) error + IdExists(ctx context.Context, id uint) (bool, error) } type KandangRepositoryImpl struct { @@ -59,6 +60,10 @@ func (r *KandangRepositoryImpl) ProjectFlockExists(ctx context.Context, projectF return count > 0, nil } +func (r *KandangRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.Kandang](ctx, r.db, id) +} + func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) { var count int64 q := r.db.WithContext(ctx). From 6b5d27ae8e992e7eae86ffd414d6330b117d67c6 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 11 Nov 2025 12:16:39 +0700 Subject: [PATCH 16/29] feat[BE]: add flock response to project flock and projectflockkandang getone and getall API --- .../project_flock_kandang.controller.go | 13 ++- .../dto/project_flock_kandang.dto.go | 24 ++++- .../project-flock-kandangs/module.go | 4 +- .../services/project_flock_kandang.service.go | 62 ++++++++++--- .../controllers/projectflock.controller.go | 24 +++-- .../project_flocks/dto/projectflock.dto.go | 64 +++++++++----- .../services/projectflock.service.go | 87 ++++++++++++++++--- 7 files changed, 220 insertions(+), 58 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go index 94316dce..d65cedfd 100644 --- a/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go +++ b/internal/modules/production/project-flock-kandangs/controllers/project_flock_kandang.controller.go @@ -4,6 +4,7 @@ import ( "math" "strconv" + flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" @@ -40,14 +41,18 @@ func (u *ProjectFlockKandangController) GetAll(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") } - results, totalResults, err := u.ProjectFlockKandangService.GetAll(c, query) + results, totalResults, flockMap, err := u.ProjectFlockKandangService.GetAll(c, query) if err != nil { return err } data := make([]dto.ProjectFlockKandangListDTO, 0) for _, result := range results { - data = append(data, dto.ToProjectFlockKandangListDTO(result)) + var flock *flockDTO.FlockBaseDTO + if flockMap != nil { + flock = flockMap[result.ProjectFlock.Id] + } + data = append(data, dto.ToProjectFlockKandangListDTOWithFlock(result, flock)) } return c.Status(fiber.StatusOK). @@ -73,7 +78,7 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } - result, availableQtys, productWarehouses, err := u.ProjectFlockKandangService.GetOne(c, uint(id)) + result, availableQtys, productWarehouses, flock, err := u.ProjectFlockKandangService.GetOne(c, uint(id)) if err != nil { return err } @@ -83,6 +88,6 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get projectFlockKandang successfully", - Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys, productWarehouses), + Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQtyAndFlock(*result, availableQtys, productWarehouses, flock), }) } diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 35bf615d..5d8e6628 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -87,6 +87,7 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO Id: pf.Id, Name: pf.FlockName, Period: pf.Period, + Flock: pf.Flock, Area: pf.Area, Category: pf.Category, Fcr: pf.Fcr, @@ -98,9 +99,13 @@ func toProjectFlockDTO(pf *projectFlockDTO.ProjectFlockListDTO) *ProjectFlockDTO } func ToProjectFlockKandangDetailDTOWithAvailableQty(e entity.ProjectFlockKandang, availableQtyMap map[uint]float64, productWarehouses []entity.ProductWarehouse) ProjectFlockKandangDetailDTO { + return ToProjectFlockKandangDetailDTOWithAvailableQtyAndFlock(e, availableQtyMap, productWarehouses, nil) +} + +func ToProjectFlockKandangDetailDTOWithAvailableQtyAndFlock(e entity.ProjectFlockKandang, availableQtyMap map[uint]float64, productWarehouses []entity.ProductWarehouse, flock *flockDTO.FlockBaseDTO) ProjectFlockKandangDetailDTO { var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO if e.ProjectFlock.Id != 0 { - mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) + mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock, flock) projectFlockSummary = &mapped } @@ -132,6 +137,17 @@ func toKandangDTO(kandang entity.Kandang) *KandangDTO { } } +func toFlockDTO(flock *entity.Flock) *flockDTO.FlockBaseDTO { + if flock == nil || flock.Id == 0 { + return nil + } + + return &flockDTO.FlockBaseDTO{ + Id: flock.Id, + Name: flock.Name, + } +} + func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO { if e.LatestApproval != nil { mapped := approvalDTO.ToApprovalDTO(*e.LatestApproval) @@ -141,9 +157,13 @@ func toApprovalDTO(e entity.ProjectFlockKandang) *approvalDTO.ApprovalBaseDTO { } func ToProjectFlockKandangListDTO(e entity.ProjectFlockKandang) ProjectFlockKandangListDTO { + return ToProjectFlockKandangListDTOWithFlock(e, nil) +} + +func ToProjectFlockKandangListDTOWithFlock(e entity.ProjectFlockKandang, flock *flockDTO.FlockBaseDTO) ProjectFlockKandangListDTO { var projectFlockSummary *projectFlockDTO.ProjectFlockListDTO if e.ProjectFlock.Id != 0 { - mapped := projectFlockDTO.ToProjectFlockListDTO(e.ProjectFlock) + mapped := projectFlockDTO.ToProjectFlockListDTOWithFlock(e.ProjectFlock, flock) projectFlockSummary = &mapped } diff --git a/internal/modules/production/project-flock-kandangs/module.go b/internal/modules/production/project-flock-kandangs/module.go index 160cec5e..3e7e8fa0 100644 --- a/internal/modules/production/project-flock-kandangs/module.go +++ b/internal/modules/production/project-flock-kandangs/module.go @@ -13,6 +13,7 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -28,6 +29,7 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB userRepo := rUser.NewUserRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + flockRepo := rFlock.NewFlockRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalService := commonSvc.NewApprovalService(approvalRepo) @@ -36,7 +38,7 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err)) } - projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, validate) + projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, flockRepo, validate) userService := sUser.NewUserService(userRepo, validate) ProjectFlockKandangRoutes(router, userService, projectFlockKandangService) diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 11e8b0d5..349eca4d 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -6,9 +6,12 @@ import ( commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" + flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/go-playground/validator/v10" @@ -18,8 +21,8 @@ import ( ) type ProjectFlockKandangService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, map[uint]*flockDTO.FlockBaseDTO, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, *flockDTO.FlockBaseDTO, error) } // Note: map[uint]float64 adalah mapping dari ProductWarehouse ID ke calculated available quantity @@ -32,9 +35,10 @@ type projectFlockKandangService struct { WarehouseRepo rWarehouse.WarehouseRepository ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository PopulationRepo repository.ProjectFlockPopulationRepository + FlockRepo flockRepository.FlockRepository } -func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService { +func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, flockRepo flockRepository.FlockRepository, validate *validator.Validate) ProjectFlockKandangService { return &projectFlockKandangService{ Log: utils.Log, Validate: validate, @@ -43,12 +47,13 @@ func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository WarehouseRepo: warehouseRepo, ProductWarehouseRepo: productWarehouseRepo, PopulationRepo: populationRepo, + FlockRepo: flockRepo, } } -func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error) { +func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, map[uint]*flockDTO.FlockBaseDTO, error) { if err := s.Validate.Struct(params); err != nil { - return nil, 0, err + return nil, 0, nil, err } offset := (params.Page - 1) * params.Limit @@ -57,7 +62,7 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer if err != nil { s.Log.Errorf("Failed to get projectFlockKandangs: %+v", err) - return nil, 0, err + return nil, 0, nil, err } if s.ApprovalSvc != nil { @@ -80,16 +85,36 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer } } - return projectFlockKandangs, total, nil + // Fetch Flock master data for each ProjectFlockKandang + flockMap := make(map[uint]*flockDTO.FlockBaseDTO) + for i := range projectFlockKandangs { + if projectFlockKandangs[i].ProjectFlock.Id != 0 { + baseName := pfutils.DeriveBaseName(projectFlockKandangs[i].ProjectFlock.FlockName) + if baseName != "" { + flock, err := s.FlockRepo.GetByName(c.Context(), baseName) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) + } else if flock != nil { + // Convert to DTO and store in map with ProjectFlockId as key + flockMap[projectFlockKandangs[i].ProjectFlock.Id] = &flockDTO.FlockBaseDTO{ + Id: flock.Id, + Name: flock.Name, + } + } + } + } + } + + return projectFlockKandangs, total, flockMap, nil } -func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error) { +func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, *flockDTO.FlockBaseDTO, error) { projectFlockKandang, err := s.Repository.GetByID(c.Context(), id) if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") + return nil, nil, nil, nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found") } if err != nil { - return nil, nil, nil, err + return nil, nil, nil, nil, err } if len(projectFlockKandang.Chickins) > 0 && s.ApprovalSvc != nil { @@ -123,8 +148,23 @@ func (s projectFlockKandangService) GetOne(c *fiber.Ctx, id uint) (*entity.Proje productWarehouses = []entity.ProductWarehouse{} } } + var flockResult *flockDTO.FlockBaseDTO + if projectFlockKandang.ProjectFlock.Id != 0 { + baseName := pfutils.DeriveBaseName(projectFlockKandang.ProjectFlock.FlockName) + if baseName != "" { + flock, err := s.FlockRepo.GetByName(c.Context(), baseName) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) + } else if flock != nil { + flockResult = &flockDTO.FlockBaseDTO{ + Id: flock.Id, + Name: flock.Name, + } + } + } + } - return projectFlockKandang, availableQtyMap, productWarehouses, nil + return projectFlockKandang, availableQtyMap, productWarehouses, flockResult, nil } func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang) (map[uint]float64, error) { diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index cf2dff05..67b04f99 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -7,6 +7,7 @@ import ( "strconv" "strings" + flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" @@ -84,11 +85,20 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error { query.KandangIds = ids } - result, totalResults, err := u.ProjectflockService.GetAll(c, query) + result, totalResults, flockMap, err := u.ProjectflockService.GetAll(c, query) if err != nil { return err } + data := make([]dto.ProjectFlockListDTO, 0) + for _, projectFlock := range result { + var flock *flockDTO.FlockBaseDTO + if flockMap != nil { + flock = flockMap[projectFlock.Id] + } + data = append(data, dto.ToProjectFlockListDTO(projectFlock, flock)) + } + return c.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.ProjectFlockListDTO]{ Code: fiber.StatusOK, @@ -100,7 +110,7 @@ func (u *ProjectflockController) GetAll(c *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: dto.ToProjectFlockListDTOs(result), + Data: data, }) } @@ -112,7 +122,7 @@ func (u *ProjectflockController) GetOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") } - result, err := u.ProjectflockService.GetOne(c, uint(id)) + result, flock, err := u.ProjectflockService.GetOne(c, uint(id)) if err != nil { return err } @@ -122,7 +132,7 @@ func (u *ProjectflockController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get projectflock successfully", - Data: dto.ToProjectFlockListDTO(*result), + Data: dto.ToProjectFlockListDTO(*result, flock), }) } @@ -143,7 +153,7 @@ func (u *ProjectflockController) CreateOne(c *fiber.Ctx) error { Code: fiber.StatusCreated, Status: "success", Message: "Create projectflock successfully", - Data: dto.ToProjectFlockListDTO(*result), + Data: dto.ToProjectFlockListDTO(*result, nil), }) } @@ -170,7 +180,7 @@ func (u *ProjectflockController) UpdateOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Update projectflock successfully", - Data: dto.ToProjectFlockListDTO(*result), + Data: dto.ToProjectFlockListDTO(*result, nil), }) } @@ -210,7 +220,7 @@ func (u *ProjectflockController) Approval(c *fiber.Ctx) error { message = "Submit projectflock approval successfully" ) if len(results) == 1 { - data = dto.ToProjectFlockListDTO(results[0]) + data = dto.ToProjectFlockListDTO(results[0], nil) } else { message = "Submit projectflock approvals successfully" data = dto.ToProjectFlockListDTOs(results) diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index ce9be3d3..2230d865 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -25,7 +25,7 @@ type ProjectFlockBaseDTO struct { type ProjectFlockListDTO struct { ProjectFlockBaseDTO - // Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` + Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` Category string `json:"category"` Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` @@ -51,7 +51,7 @@ type FlockPeriodDTO struct { NextPeriod int `json:"next_period"` } -func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { +func ToProjectFlockListDTO(e entity.ProjectFlock, flock *flockDTO.FlockBaseDTO) ProjectFlockListDTO { var createdUser *userDTO.UserBaseDTO if e.CreatedUser.Id != 0 { mapped := userDTO.ToUserBaseDTO(e.CreatedUser) @@ -62,7 +62,7 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { if len(e.Kandangs) > 0 { kandangSummaries = make([]KandangWithProjectFlockIdDTO, len(e.Kandangs)) for i, kandang := range e.Kandangs { - // Find project_flock_kandang_id dari KandangHistory + var pfkId uint for _, kh := range e.KandangHistory { if kh.KandangId == kandang.Id { @@ -77,12 +77,6 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { } } - // var flockSummary *flockDTO.FlockBaseDTO - // if baseName := pfutils.DeriveBaseName(e.FlockName); baseName != "" { - // summary := flockDTO.FlockBaseDTO{Id: 0, Name: baseName} - // flockSummary = &summary - // } - var areaSummary *areaDTO.AreaBaseDTO if e.Area.Id != 0 { mapped := areaDTO.ToAreaBaseDTO(e.Area) @@ -101,6 +95,11 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { locationSummary = &mapped } + var flockSummary *flockDTO.FlockBaseDTO + if flock != nil && flock.Id != 0 { + flockSummary = flock + } + latestApproval := defaultProjectFlockLatestApproval(e) if e.LatestApproval != nil { snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval) @@ -109,30 +108,51 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { return ProjectFlockListDTO{ ProjectFlockBaseDTO: createProjectFlockBaseDTO(e), - // Flock: flockSummary, - Area: areaSummary, - Kandangs: kandangSummaries, - Category: e.Category, - Fcr: fcrSummary, - Location: locationSummary, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, - Approval: latestApproval, + Flock: flockSummary, + Area: areaSummary, + Kandangs: kandangSummaries, + Category: e.Category, + Fcr: fcrSummary, + Location: locationSummary, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + Approval: latestApproval, } } +func ToProjectFlockListDTOWithFlock(e entity.ProjectFlock, flock *flockDTO.FlockBaseDTO) ProjectFlockListDTO { + return ToProjectFlockListDTO(e, flock) +} + func ToProjectFlockListDTOs(items []entity.ProjectFlock) []ProjectFlockListDTO { result := make([]ProjectFlockListDTO, len(items)) for i, item := range items { - result[i] = ToProjectFlockListDTO(item) + result[i] = ToProjectFlockListDTO(item, nil) } return result } -func ToProjectFlockDetailDTO(e entity.ProjectFlock) ProjectFlockDetailDTO { +func ToProjectFlockListDTOsWithFlocks(items []entity.ProjectFlock, flocks map[uint]*entity.Flock) []ProjectFlockListDTO { + result := make([]ProjectFlockListDTO, len(items)) + for i, item := range items { + var flock *flockDTO.FlockBaseDTO + if flocks != nil { + if f := flocks[item.Id]; f != nil { + flock = &flockDTO.FlockBaseDTO{ + Id: f.Id, + Name: f.Name, + } + } + } + result[i] = ToProjectFlockListDTOWithFlock(item, flock) + } + return result +} + +func ToProjectFlockDetailDTO(e entity.ProjectFlock, flock *flockDTO.FlockBaseDTO) ProjectFlockDetailDTO { return ProjectFlockDetailDTO{ - ProjectFlockListDTO: ToProjectFlockListDTO(e), + ProjectFlockListDTO: ToProjectFlockListDTO(e, flock), } } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 9b5b4750..715fb1fb 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -11,6 +11,7 @@ import ( commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" @@ -27,8 +28,8 @@ import ( ) type ProjectflockService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockBaseDTO, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockBaseDTO, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) @@ -92,9 +93,9 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB { Preload("KandangHistory.Kandang") } -func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) { +func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockBaseDTO, error) { if err := s.Validate.Struct(params); err != nil { - return nil, 0, err + return nil, 0, nil, err } if params.Page <= 0 { @@ -110,7 +111,7 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e if err != nil { s.Log.Errorf("Failed to get projectflocks: %+v", err) - return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flocks") + return nil, 0, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flocks") } if s.ApprovalSvc != nil && len(projectflocks) > 0 { @@ -133,10 +134,28 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e } } - return projectflocks, total, nil + flockMap := make(map[uint]*flockDTO.FlockBaseDTO) + for i := range projectflocks { + if projectflocks[i].FlockName != "" { + baseName := pfutils.DeriveBaseName(projectflocks[i].FlockName) + if baseName != "" { + flock, err := s.FlockRepo.GetByName(c.Context(), baseName) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) + } else if flock != nil { + flockMap[projectflocks[i].Id] = &flockDTO.FlockBaseDTO{ + Id: flock.Id, + Name: flock.Name, + } + } + } + } + } + + return projectflocks, total, flockMap, nil } -func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) { +func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) { projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") @@ -165,6 +184,52 @@ func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock return projectflock, nil } +func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockBaseDTO, error) { + projectflock, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations()) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") + } + if err != nil { + s.Log.Errorf("Failed get projectflock by id: %+v", err) + return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") + } + + if s.ApprovalSvc != nil { + approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err) + } else if len(approvals) > 0 { + if projectflock.LatestApproval == nil { + latest := approvals[len(approvals)-1] + projectflock.LatestApproval = &latest + } + } else { + projectflock.LatestApproval = nil + } + } + + // Fetch Flock master data for this ProjectFlock + var flockResult *flockDTO.FlockBaseDTO + if projectflock.FlockName != "" { + baseName := pfutils.DeriveBaseName(projectflock.FlockName) + if baseName != "" { + flock, err := s.FlockRepo.GetByName(c.Context(), baseName) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) + } else if flock != nil { + flockResult = &flockDTO.FlockBaseDTO{ + Id: flock.Id, + Name: flock.Name, + } + } + } + } + + return projectflock, flockResult, nil +} + func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) { if err := s.Validate.Struct(req); err != nil { return nil, err @@ -275,7 +340,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create project flock") } - return s.GetOne(c, createBody.Id) + return s.getOneEntityOnly(c, createBody.Id) } func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) { @@ -387,7 +452,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id hasChanges := hasBodyChanges || hasKandangChanges if !hasChanges { - return s.GetOne(c, id) + return s.getOneEntityOnly(c, id) } err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { @@ -510,7 +575,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock") } - return s.GetOne(c, id) + return s.getOneEntityOnly(c, id) } func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) { @@ -600,7 +665,7 @@ func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([] updated := make([]entity.ProjectFlock, 0, len(approvableIDs)) for _, approvableID := range approvableIDs { - project, err := s.GetOne(c, approvableID) + project, err := s.getOneEntityOnly(c, approvableID) if err != nil { return nil, err } From 762dfa9fb9e7cdde0090b8a8e768aca8a014ef5f Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 11 Nov 2025 14:32:55 +0700 Subject: [PATCH 17/29] feat[BE-127] add source and target project flock to transfer laying API --- .../services/sales-orders.service.go | 72 +++++++++++++++++-- .../services/project_flock_kandang.service.go | 3 +- .../dto/transfer_laying.dto.go | 33 ++++++--- .../services/transfer_laying.service.go | 4 ++ 4 files changed, 94 insertions(+), 18 deletions(-) diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 847305c1..0a1e5898 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -2,6 +2,7 @@ package service import ( "errors" + "time" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -106,18 +107,68 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e } } - createBody := &entity.Marketing{ - CustomerId: req.CustomerId, + // parse date + soDate, err := utils.ParseDateString(req.Date) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format") } - err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { - marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + // generate SO number + soNumber := "SO-" + time.Now().Format("20060102150405") - if err := marketingRepoTx.CreateOne(c.Context(), createBody, nil); err != nil { + var marketing *entity.Marketing + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + // create marketing directly (tanpa create SalesOrders) + marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + marketing = &entity.Marketing{ + CustomerId: req.CustomerId, + SoNumber: soNumber, + SoDate: soDate, + SalesPersonId: 1, + Notes: req.Notes, + CreatedBy: 1, + } + if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil { s.Log.Errorf("Failed to create marketing: %+v", err) return err } + // Create MarketingProducts and MarketingDeliveryProducts + if len(req.MarketingProducts) > 0 { + for _, product := range req.MarketingProducts { + marketingProduct := &entity.MarketingProduct{ + MarketingId: marketing.Id, + ProductWarehouseId: product.ProductWarehouseId, + Qty: product.Qty, + UnitPrice: product.UnitPrice, + AvgWeight: product.AvgWeight, + TotalWeight: product.TotalWeight, + TotalPrice: product.TotalPrice, + } + if err := dbTransaction.Create(marketingProduct).Error; err != nil { + s.Log.Errorf("Failed to create marketing product: %+v", err) + return err + } + + // create delivery product with zeroed numeric fields and nil delivery date + marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ + MarketingProductId: marketingProduct.Id, + Qty: 0, + UnitPrice: 0, + TotalWeight: 0, + AvgWeight: 0, + TotalPrice: 0, + DeliveryDate: nil, + VehicleNumber: product.VehicleNumber, + } + if err := dbTransaction.Create(marketingDeliveryProduct).Error; err != nil { + s.Log.Errorf("Failed to create marketing delivery product: %+v", err) + return err + } + } + } + return nil }) if err != nil { @@ -127,7 +178,16 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders") } - return s.GetOne(c, createBody.Id) + createdMarketing, err := s.MarketingRepo.GetByID(c.Context(), marketing.Id, nil) + if err != nil { + s.Log.Errorf("Failed to fetch created marketing: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created marketing") + } + + return &entity.SalesOrders{ + Id: createdMarketing.Id, + Name: createdMarketing.SoNumber, + }, nil } func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.SalesOrders, error) { diff --git a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go index 349eca4d..ec61a7e4 100644 --- a/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go +++ b/internal/modules/production/project-flock-kandangs/services/project_flock_kandang.service.go @@ -85,7 +85,6 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer } } - // Fetch Flock master data for each ProjectFlockKandang flockMap := make(map[uint]*flockDTO.FlockBaseDTO) for i := range projectFlockKandangs { if projectFlockKandangs[i].ProjectFlock.Id != 0 { @@ -95,7 +94,7 @@ func (s projectFlockKandangService) GetAll(c *fiber.Ctx, params *validation.Quer if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { s.Log.Warnf("Failed to fetch flock %q: %+v", baseName, err) } else if flock != nil { - // Convert to DTO and store in map with ProjectFlockId as key + flockMap[projectFlockKandangs[i].ProjectFlock.Id] = &flockDTO.FlockBaseDTO{ Id: flock.Id, Name: flock.Name, diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go index 98c1a2e7..ef537e19 100644 --- a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -18,9 +18,9 @@ type TransferLayingBaseDTO struct { } type ProjectFlockSummaryDTO struct { - Id uint `json:"id"` - Period int `json:"period"` - Category string `json:"category"` + Id uint `json:"id"` + FlockName string `json:"flock_name"` + Category string `json:"category"` } type ProductSummaryDTO struct { @@ -40,8 +40,13 @@ type ProductWarehouseSummaryDTO struct { } type ProjectFlockKandangSummaryDTO struct { - Id uint `json:"id"` - KandangId uint `json:"kandang_id"` + Id uint `json:"id"` + Kandang *KandangSummaryDTO `json:"kandang,omitempty"` +} + +type KandangSummaryDTO struct { + Id uint `json:"id"` + Name string `json:"name"` } type LayingTransferSourceDTO struct { @@ -99,9 +104,9 @@ func ToProjectFlockSummaryDTO(pf *entity.ProjectFlock) *ProjectFlockSummaryDTO { } return &ProjectFlockSummaryDTO{ - Id: pf.Id, - Period: pf.Period, - Category: pf.Category, + Id: pf.Id, + FlockName: pf.FlockName, + Category: pf.Category, } } @@ -110,9 +115,17 @@ func ToProjectFlockKandangSummaryDTO(pfk *entity.ProjectFlockKandang) *ProjectFl return nil } + var kandang *KandangSummaryDTO + if pfk.Kandang.Id != 0 { + kandang = &KandangSummaryDTO{ + Id: pfk.Kandang.Id, + Name: pfk.Kandang.Name, + } + } + return &ProjectFlockKandangSummaryDTO{ - Id: pfk.Id, - KandangId: pfk.KandangId, + Id: pfk.Id, + Kandang: kandang, } } diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go index e7591aef..2aa7129c 100644 --- a/internal/modules/production/transfer_layings/services/transfer_laying.service.go +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -72,13 +72,17 @@ func NewTransferLayingService( func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). + Preload("FromProjectFlock"). + Preload("ToProjectFlock"). Preload("Sources"). Preload("Sources.SourceProjectFlockKandang"). + Preload("Sources.SourceProjectFlockKandang.Kandang"). Preload("Sources.ProductWarehouse"). Preload("Sources.ProductWarehouse.Product"). Preload("Sources.ProductWarehouse.Warehouse"). Preload("Targets"). Preload("Targets.TargetProjectFlockKandang"). + Preload("Targets.TargetProjectFlockKandang.Kandang"). Preload("Targets.ProductWarehouse"). Preload("Targets.ProductWarehouse.Product"). Preload("Targets.ProductWarehouse.Warehouse") From 0a0c3f869bcf394e0b6385e8ec62575140c95a34 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 12 Nov 2025 11:28:18 +0700 Subject: [PATCH 18/29] Feat[BE-222,223,224]: creating So create delete patch update get getall approval API --- internal/entities/delivery-orders.go | 23 + internal/entities/marketing.go | 9 +- internal/entities/marketing_product.go | 5 +- internal/entities/sales-orders.go | 3 +- .../marketing-delivery-products.repository.go | 21 + .../controllers/delivery-orders.controller.go | 165 +++++++ .../dto/delivery-orders.dto.go | 164 +++++++ .../marketing/delivery-orderss/module.go | 32 ++ .../delivery-orders.repository.go | 21 + .../marketing/delivery-orderss/route.go | 29 ++ .../services/delivery-orders.service.go | 412 ++++++++++++++++ .../validations/delivery-orders.validation.go | 37 ++ internal/modules/marketing/route.go | 2 + .../controllers/sales-orders.controller.go | 52 +- .../sales-orders/dto/sales-orders.dto.go | 223 ++++++++- .../modules/marketing/sales-orders/module.go | 5 +- .../marketing-delivery-products.repository.go | 21 + .../marketing-products.repository.go | 35 ++ .../repositories/marketings.repository.go | 7 + .../modules/marketing/sales-orders/route.go | 1 + .../services/sales-orders.service.go | 464 +++++++++++++++--- .../validations/sales-orders.validation.go | 13 +- .../users/repositories/user.repository.go | 9 + internal/utils/constant.go | 17 + 24 files changed, 1688 insertions(+), 82 deletions(-) create mode 100644 internal/entities/delivery-orders.go create mode 100644 internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go create mode 100644 internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go create mode 100644 internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go create mode 100644 internal/modules/marketing/delivery-orderss/module.go create mode 100644 internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go create mode 100644 internal/modules/marketing/delivery-orderss/route.go create mode 100644 internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go create mode 100644 internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go create mode 100644 internal/modules/marketing/sales-orders/repositories/marketing-delivery-products.repository.go create mode 100644 internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go diff --git a/internal/entities/delivery-orders.go b/internal/entities/delivery-orders.go new file mode 100644 index 00000000..291ba20c --- /dev/null +++ b/internal/entities/delivery-orders.go @@ -0,0 +1,23 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type DeliveryOrders struct { + Id uint `gorm:"primaryKey" json:"id"` + DeliveryNumber string `gorm:"type:varchar(255);not null;uniqueIndex" json:"delivery_number"` + DeliveryDate time.Time `gorm:"not null" json:"delivery_date"` + MarketingId uint `gorm:"not null" json:"marketing_id"` + Notes string `gorm:"type:text" json:"notes"` + CreatedBy uint `gorm:"not null" json:"created_by"` + CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` + UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Marketing *Marketing `gorm:"foreignKey:MarketingId;references:Id" json:"marketing,omitempty"` + DeliveryProducts []MarketingDeliveryProduct `gorm:"-" json:"delivery_products,omitempty"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id" json:"created_user,omitempty"` +} diff --git a/internal/entities/marketing.go b/internal/entities/marketing.go index 1ae4d8c3..c9ff7624 100644 --- a/internal/entities/marketing.go +++ b/internal/entities/marketing.go @@ -19,8 +19,9 @@ type Marketing struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Customer Customer `gorm:"foreignKey:CustomerId;references:Id"` - SalesPerson User `gorm:"foreignKey:SalesPersonId;references:Id"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` - Products []MarketingProduct `gorm:"foreignKey:MarketingId;references:Id"` + Customer Customer `gorm:"foreignKey:CustomerId;references:Id"` + SalesPerson User `gorm:"foreignKey:SalesPersonId;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + Products []MarketingProduct `gorm:"foreignKey:MarketingId;references:Id"` + LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"` } diff --git a/internal/entities/marketing_product.go b/internal/entities/marketing_product.go index 2e6aef58..f0fe7f38 100644 --- a/internal/entities/marketing_product.go +++ b/internal/entities/marketing_product.go @@ -19,6 +19,7 @@ type MarketingProduct struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` - ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` + ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + DeliveryProducts []MarketingDeliveryProduct `gorm:"foreignKey:MarketingProductId;references:Id"` } diff --git a/internal/entities/sales-orders.go b/internal/entities/sales-orders.go index 48615607..faa6d901 100644 --- a/internal/entities/sales-orders.go +++ b/internal/entities/sales-orders.go @@ -14,5 +14,6 @@ type SalesOrders struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"` } diff --git a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go new file mode 100644 index 00000000..95e9b3bb --- /dev/null +++ b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type MarketingDeliveryProductRepository interface { + repository.BaseRepository[entity.MarketingDeliveryProduct] +} + +type MarketingDeliveryProductRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct] +} + +func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository { + return &MarketingDeliveryProductRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db), + } +} diff --git a/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go new file mode 100644 index 00000000..a5f2839a --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go @@ -0,0 +1,165 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type DeliveryOrdersController struct { + DeliveryOrdersService service.DeliveryOrdersService +} + +func NewDeliveryOrdersController(deliveryOrdersService service.DeliveryOrdersService) *DeliveryOrdersController { + return &DeliveryOrdersController{ + DeliveryOrdersService: deliveryOrdersService, + } +} + +func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + MarketingId: uint(c.QueryInt("marketing_id", 0)), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := u.DeliveryOrdersService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.DeliveryOrdersListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all deliveryOrderss successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: result, + }) +} + +func (u *DeliveryOrdersController) GetOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + result, err := u.DeliveryOrdersService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get deliveryOrders successfully", + Data: *result, + }) +} + +func (u *DeliveryOrdersController) CreateOne(c *fiber.Ctx) error { + req := new(validation.Create) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.DeliveryOrdersService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create delivery products successfully", + Data: result, + }) +} + +func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error { + req := new(validation.Update) + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.DeliveryOrdersService.UpdateOne(c, req, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Update deliveryOrders successfully", + Data: dto.ToDeliveryOrdersListDTO(*result), + }) +} + +func (u *DeliveryOrdersController) DeleteOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + if err := u.DeliveryOrdersService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete deliveryOrders successfully", + }) +} + +func (u *DeliveryOrdersController) Approval(c *fiber.Ctx) error { + req := new(validation.Approve) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + results, err := u.DeliveryOrdersService.Approval(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Submit delivery order approval successfully", + Data: results, + }) +} diff --git a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go new file mode 100644 index 00000000..2b2ea51e --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go @@ -0,0 +1,164 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +// === DTO Structs === + +type MarketingDeliveryProductDTO struct { + Id uint `json:"id"` + MarketingProductId uint `json:"marketing_product_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalWeight float64 `json:"total_weight"` + AvgWeight float64 `json:"avg_weight"` + TotalPrice float64 `json:"total_price"` + DeliveryDate *time.Time `json:"delivery_date"` + VehicleNumber string `json:"vehicle_number"` +} + +type DeliveryOrdersBaseDTO struct { + Id uint `json:"id,omitempty"` + DeliveryNumber *string `json:"delivery_number,omitempty"` + DeliveryDate *time.Time `json:"delivery_date,omitempty"` + MarketingId uint `json:"marketing_id"` + Notes string `json:"notes,omitempty"` +} + +type MarketingBaseDTO struct { + Id uint `json:"id"` + SoNumber string `json:"so_number"` + SoDate time.Time `json:"so_date"` +} + +type DeliveryOrdersListDTO struct { + DeliveryOrdersBaseDTO + Marketing *MarketingBaseDTO `json:"marketing,omitempty"` + DeliveryProducts []MarketingDeliveryProductDTO `json:"delivery_products,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` +} + +type DeliveryOrdersDetailDTO struct { + DeliveryOrdersListDTO +} + +// === Mapper Functions === + +func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingDeliveryProductDTO { + return MarketingDeliveryProductDTO{ + Id: e.Id, + MarketingProductId: e.MarketingProductId, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + TotalWeight: e.TotalWeight, + AvgWeight: e.AvgWeight, + TotalPrice: e.TotalPrice, + DeliveryDate: e.DeliveryDate, + VehicleNumber: e.VehicleNumber, + } +} + +func ToDeliveryOrdersBaseDTO(e entity.DeliveryOrders) DeliveryOrdersBaseDTO { + var deliveryNumber *string + if e.DeliveryNumber != "" { + deliveryNumber = &e.DeliveryNumber + } + + return DeliveryOrdersBaseDTO{ + Id: e.Id, + DeliveryNumber: deliveryNumber, + DeliveryDate: &e.DeliveryDate, + MarketingId: e.MarketingId, + Notes: e.Notes, + } +} + +func ToDeliveryOrdersListDTO(e entity.DeliveryOrders) DeliveryOrdersListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + createdUser = &mapped + } + + var marketing *MarketingBaseDTO + if e.Marketing != nil && e.Marketing.Id != 0 { + marketing = &MarketingBaseDTO{ + Id: e.Marketing.Id, + SoNumber: e.Marketing.SoNumber, + SoDate: e.Marketing.SoDate, + } + } + + var deliveryProductsDTOs []MarketingDeliveryProductDTO + if len(e.DeliveryProducts) > 0 { + deliveryProductsDTOs = make([]MarketingDeliveryProductDTO, len(e.DeliveryProducts)) + for i, dp := range e.DeliveryProducts { + deliveryProductsDTOs[i] = ToMarketingDeliveryProductDTO(dp) + } + } + + return DeliveryOrdersListDTO{ + DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), + Marketing: marketing, + DeliveryProducts: deliveryProductsDTOs, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToDeliveryOrdersListDTOWithProducts(e entity.DeliveryOrders, deliveryProducts []entity.MarketingDeliveryProduct) DeliveryOrdersListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + createdUser = &mapped + } + + var marketing *MarketingBaseDTO + if e.Marketing != nil && e.Marketing.Id != 0 { + marketing = &MarketingBaseDTO{ + Id: e.Marketing.Id, + SoNumber: e.Marketing.SoNumber, + SoDate: e.Marketing.SoDate, + } + } + + var deliveryProductsDTOs []MarketingDeliveryProductDTO + if len(deliveryProducts) > 0 { + deliveryProductsDTOs = make([]MarketingDeliveryProductDTO, len(deliveryProducts)) + for i, dp := range deliveryProducts { + deliveryProductsDTOs[i] = ToMarketingDeliveryProductDTO(dp) + } + } + + return DeliveryOrdersListDTO{ + DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), + Marketing: marketing, + DeliveryProducts: deliveryProductsDTOs, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToDeliveryOrdersListDTOs(e []entity.DeliveryOrders) []DeliveryOrdersListDTO { + result := make([]DeliveryOrdersListDTO, len(e)) + for i, r := range e { + result[i] = ToDeliveryOrdersListDTO(r) + } + return result +} + +func ToDeliveryOrdersDetailDTO(e entity.DeliveryOrders) DeliveryOrdersDetailDTO { + return DeliveryOrdersDetailDTO{ + DeliveryOrdersListDTO: ToDeliveryOrdersListDTO(e), + } +} diff --git a/internal/modules/marketing/delivery-orderss/module.go b/internal/modules/marketing/delivery-orderss/module.go new file mode 100644 index 00000000..c6932c51 --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/module.go @@ -0,0 +1,32 @@ +package delivery_orderss + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + rMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" + rDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/repositories" + sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services" + rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type DeliveryOrdersModule struct{} + +func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + deliveryOrdersRepo := rDeliveryOrders.NewDeliveryOrdersRepository(db) + marketingRepo := rMarketing.NewMarketingRepository(db) + marketingDeliveryProductRepo := rMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(db) + userRepo := rUser.NewUserRepository(db) + approvalRepo := commonRepo.NewApprovalRepository(db) + approvalSvc := commonSvc.NewApprovalService(approvalRepo) + + deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(deliveryOrdersRepo, marketingRepo, marketingDeliveryProductRepo, approvalSvc, validate) + userService := sUser.NewUserService(userRepo, validate) + + DeliveryOrdersRoutes(router, userService, deliveryOrdersService) +} diff --git a/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go b/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go new file mode 100644 index 00000000..0a7d7f3d --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + "gorm.io/gorm" +) + +type DeliveryOrdersRepository interface { + repository.BaseRepository[entity.DeliveryOrders] +} + +type DeliveryOrdersRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.DeliveryOrders] +} + +func NewDeliveryOrdersRepository(db *gorm.DB) DeliveryOrdersRepository { + return &DeliveryOrdersRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.DeliveryOrders](db), + } +} diff --git a/internal/modules/marketing/delivery-orderss/route.go b/internal/modules/marketing/delivery-orderss/route.go new file mode 100644 index 00000000..8d58b70e --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/route.go @@ -0,0 +1,29 @@ +package delivery_orderss + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/controllers" + deliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.DeliveryOrdersService) { + ctrl := controller.NewDeliveryOrdersController(s) + + route := v1.Group("/delivery-orders") + + // route.Get("/", m.Auth(u), ctrl.GetAll) + // route.Post("/", m.Auth(u), ctrl.CreateOne) + // route.Get("/:id", m.Auth(u), ctrl.GetOne) + // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) + // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) + + route.Get("/", ctrl.GetAll) + route.Post("/", ctrl.CreateOne) + route.Get("/:id", ctrl.GetOne) + route.Patch("/:id", ctrl.UpdateOne) + route.Delete("/:id", ctrl.DeleteOne) + route.Post("/approvals", ctrl.Approval) +} diff --git a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go new file mode 100644 index 00000000..db238d0f --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go @@ -0,0 +1,412 @@ +package service + +import ( + "errors" + "fmt" + "time" + + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + marketingDeliveryProductRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" + "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations" + marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type DeliveryOrdersService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.DeliveryOrdersListDTO, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*dto.DeliveryOrdersListDTO, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.DeliveryOrders, error) + DeleteOne(ctx *fiber.Ctx, id uint) error + Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) +} + +type deliveryOrdersService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.DeliveryOrdersRepository + MarketingRepo marketingRepo.MarketingRepository + MarketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository + ApprovalSvc commonSvc.ApprovalService +} + +func NewDeliveryOrdersService( + repo repository.DeliveryOrdersRepository, + marketingRepo marketingRepo.MarketingRepository, + marketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository, + approvalSvc commonSvc.ApprovalService, + validate *validator.Validate, +) DeliveryOrdersService { + return &deliveryOrdersService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + MarketingRepo: marketingRepo, + MarketingDeliveryProductRepo: marketingDeliveryProductRepo, + ApprovalSvc: approvalSvc, + } +} + +func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser"). + Preload("Marketing") +} + +func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.DeliveryOrdersListDTO, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + // Fetch dari Marketing, bukan DeliveryOrders + marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = db.Preload("CreatedUser"). + Preload("Customer"). + Preload("SalesPerson"). + Preload("Products.ProductWarehouse") + if params.MarketingId != 0 { + return db.Where("id = ?", params.MarketingId) + } + return db.Order("created_at DESC").Order("updated_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get marketings: %+v", err) + return nil, 0, err + } + + // Load delivery products untuk setiap marketing + result := make([]dto.DeliveryOrdersListDTO, len(marketings)) + for i, marketing := range marketings { + // Get marketing delivery products + var deliveryProducts []entity.MarketingDeliveryProduct + if err := s.Repository.DB().WithContext(c.Context()). + Preload("MarketingProduct"). + Where("marketing_product_id IN (?)", + s.Repository.DB().WithContext(c.Context()). + Model(&entity.MarketingProduct{}). + Select("id"). + Where("marketing_id = ?", marketing.Id)). + Find(&deliveryProducts).Error; err != nil { + s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) + // Continue without products + } + + // Create dummy DeliveryOrders untuk dto mapping + dummyDO := &entity.DeliveryOrders{ + MarketingId: marketing.Id, + CreatedUser: &marketing.CreatedUser, + Marketing: &marketing, + DeliveryProducts: deliveryProducts, + } + + result[i] = dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) + } + + return result, total, nil +} + +func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.DeliveryOrdersListDTO, error) { + // Fetch Marketing by ID, bukan DeliveryOrders + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser"). + Preload("Customer"). + Preload("SalesPerson"). + Preload("Products.ProductWarehouse") + }) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") + } + if err != nil { + s.Log.Errorf("Failed get marketing by id: %+v", err) + return nil, err + } + + // Get marketing delivery products + var deliveryProducts []entity.MarketingDeliveryProduct + if err := s.Repository.DB().WithContext(c.Context()). + Preload("MarketingProduct"). + Where("marketing_product_id IN (?)", + s.Repository.DB().WithContext(c.Context()). + Model(&entity.MarketingProduct{}). + Select("id"). + Where("marketing_id = ?", marketing.Id)). + Find(&deliveryProducts).Error; err != nil { + s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) + // Continue without products + } + + // Create dummy DeliveryOrders untuk dto mapping + dummyDO := &entity.DeliveryOrders{ + MarketingId: marketing.Id, + CreatedUser: &marketing.CreatedUser, + Marketing: marketing, + DeliveryProducts: deliveryProducts, + } + + result := dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) + return &result, nil +} + +func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + // Validate marketing exists + _, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, nil) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") + } + if err != nil { + s.Log.Errorf("Failed to fetch marketing %d: %+v", req.MarketingId, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") + } + + // Validate marketing approval status - harus sudah di approve ke step Sales Order + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) + latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + + if latestApproval == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing has not been submitted for approval") + } + + // Cek apakah status approval sudah Sales Order (step 2) atau lebih + if latestApproval.StepNumber < uint16(utils.MarketingStepSalesOrder) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing must be approved to Sales Order step before creating delivery order") + } + + if latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved { + return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing is not approved for delivery") + } + + // Validate semua delivery products ada dan update mereka + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTx *gorm.DB) error { + for _, product := range req.DeliveryProducts { + // Fetch marketing_product terlebih dahulu untuk pastikan punya marketing_id yang sama + var marketingProduct entity.MarketingProduct + if err := dbTx.Where("id = ? AND marketing_id = ?", product.MarketingProductId, req.MarketingId). + First(&marketingProduct).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", product.MarketingProductId)) + } + s.Log.Errorf("Failed to fetch marketing product: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing product") + } + + // Fetch marketing_delivery_product by marketing_product_id + var mdp entity.MarketingDeliveryProduct + if err := dbTx.Where("marketing_product_id = ?", marketingProduct.Id). + First(&mdp).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", product.MarketingProductId)) + } + s.Log.Errorf("Failed to fetch marketing delivery product: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") + } + + // Parse delivery date per item (jika ada), atau gunakan default + itemDeliveryDate := time.Now() + if product.DeliveryDate != "" { + parsedItemDate, err := time.Parse("2006-01-02", product.DeliveryDate) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d", product.MarketingProductId)) + } + itemDeliveryDate = parsedItemDate + } + + // Update dengan data dari request - PASTIKAN UPDATE LANGSUNG KE FIELD + updates := map[string]interface{}{ + "qty": product.Qty, + "unit_price": product.UnitPrice, + "avg_weight": product.AvgWeight, + "total_weight": product.TotalWeight, + "total_price": product.TotalPrice, + "delivery_date": &itemDeliveryDate, + "vehicle_number": product.VehicleNumber, + } + + if err := dbTx.Model(&mdp).Updates(updates).Error; err != nil { + s.Log.Errorf("Failed to update marketing delivery product: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") + } + + s.Log.Infof("Updated MDP %d: qty=%v, unitPrice=%v, totalPrice=%v", mdp.Id, product.Qty, product.UnitPrice, product.TotalPrice) + } + + return nil + }) + + if err != nil { + return nil, err + } + + // Fetch marketing dengan delivery products yang sudah di-update + marketing, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, func(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser").Preload("Products") + }) + if err != nil { + s.Log.Errorf("Failed to fetch marketing after update: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated marketing") + } + + // Get marketing delivery products + var deliveryProducts []entity.MarketingDeliveryProduct + if err := s.Repository.DB().WithContext(c.Context()). + Preload("MarketingProduct"). + Where("marketing_product_id IN (?)", + s.Repository.DB().WithContext(c.Context()). + Model(&entity.MarketingProduct{}). + Select("id"). + Where("marketing_id = ?", req.MarketingId)). + Find(&deliveryProducts).Error; err != nil { + s.Log.Errorf("Failed to load delivery products: %+v", err) + // Continue tanpa delivery products + } + + // Create dummy DeliveryOrders untuk dipakai dto mapping + dummyDO := &entity.DeliveryOrders{ + MarketingId: req.MarketingId, + Notes: req.Notes, + CreatedUser: &marketing.CreatedUser, + Marketing: marketing, + DeliveryProducts: deliveryProducts, + } + + result := dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) + return &result, nil +} + +func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.DeliveryOrders, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + updateBody := make(map[string]any) + + if req.DeliveryDate != "" { + deliveryDate, err := time.Parse("2006-01-02", req.DeliveryDate) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid delivery date format") + } + updateBody["delivery_date"] = deliveryDate + } + + if req.Notes != "" { + updateBody["notes"] = req.Notes + } + + if len(updateBody) == 0 { + return s.Repository.GetByID(c.Context(), id, s.withRelations) + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "DeliveryOrders not found") + } + s.Log.Errorf("Failed to update deliveryOrders: %+v", err) + return nil, err + } + + return s.Repository.GetByID(c.Context(), id, s.withRelations) +} + +func (s deliveryOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + var action entity.ApprovalAction + switch req.Action { + case "APPROVED": + action = entity.ApprovalActionApproved + case "REJECTED": + action = entity.ApprovalActionRejected + default: + return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") + } + + approvableIDs := utils.UniqueUintSlice(req.ApprovableIds) + if len(approvableIDs) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") + } + + // Validate semua delivery order ada + for _, id := range approvableIDs { + _, err := s.Repository.GetByID(c.Context(), id, nil) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery order %d not found", id)) + } + if err != nil { + s.Log.Errorf("Failed to get delivery order %d: %+v", id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get delivery order %d", id)) + } + } + + err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTx *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTx)) + + for _, approvableID := range approvableIDs { + actorID := uint(1) // TODO: get from auth context + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + approvableID, + utils.MarketingDeliveryOrder, + &action, + actorID, + req.Notes, + ); err != nil { + s.Log.Errorf("Failed to create approval for %d: %+v", approvableID, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") + } + } + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") + } + + updated := make([]entity.DeliveryOrders, 0, len(approvableIDs)) + for _, id := range approvableIDs { + deliveryOrder, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery order %d not found", id)) + } + s.Log.Errorf("Failed to get delivery order %d: %+v", id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get delivery order %d", id)) + } + updated = append(updated, *deliveryOrder) + } + + return updated, nil +} + +func (s deliveryOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "DeliveryOrders not found") + } + s.Log.Errorf("Failed to delete deliveryOrders: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go b/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go new file mode 100644 index 00000000..a80ad8d5 --- /dev/null +++ b/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go @@ -0,0 +1,37 @@ +package validation + +type DeliveryProduct struct { + MarketingProductId uint `json:"marketing_product_id" validate:"required,gt=0"` + Qty float64 `json:"qty" validate:"required,gt=0"` + UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` + AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` + TotalWeight float64 `json:"total_weight" validate:"required,gt=0"` + TotalPrice float64 `json:"total_price" validate:"required,gt=0"` + DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` + VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"` +} + +type Create struct { + MarketingId uint `json:"marketing_id" validate:"required,gt=0"` + DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` + DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"required,min=1,dive"` + Notes string `json:"notes" validate:"omitempty,max=500"` +} + +type Update struct { + DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` + DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"omitempty,min=1,dive"` + Notes string `json:"notes" validate:"omitempty,max=500"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"` +} + +type Approve struct { + Action string `json:"action" validate:"required_strict"` + ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` +} diff --git a/internal/modules/marketing/route.go b/internal/modules/marketing/route.go index 1b72b8cb..1ab03896 100644 --- a/internal/modules/marketing/route.go +++ b/internal/modules/marketing/route.go @@ -8,6 +8,7 @@ import ( "gorm.io/gorm" salesOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders" + deliveryOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss" // MODULE IMPORTS ) @@ -16,6 +17,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida allModules := []modules.Module{ salesOrderss.SalesOrdersModule{}, + deliveryOrderss.DeliveryOrdersModule{}, // MODULE REGISTRY } diff --git a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go index be578e15..4c49baeb 100644 --- a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go +++ b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go @@ -38,6 +38,12 @@ func (u *SalesOrdersController) GetAll(c *fiber.Ctx) error { return err } + // Convert marketing data to sales orders DTOs with products + salesOrdersDTOs := make([]dto.SalesOrdersListDTO, len(result)) + for i, marketing := range result { + salesOrdersDTOs[i] = dto.ToSalesOrdersListDTOFromMarketing(marketing) + } + return c.Status(fiber.StatusOK). JSON(response.SuccessWithPaginate[dto.SalesOrdersListDTO]{ Code: fiber.StatusOK, @@ -49,7 +55,7 @@ func (u *SalesOrdersController) GetAll(c *fiber.Ctx) error { TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: dto.ToSalesOrdersListDTOs(result), + Data: salesOrdersDTOs, }) } @@ -71,7 +77,7 @@ func (u *SalesOrdersController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get salesOrders successfully", - Data: dto.ToSalesOrdersListDTO(*result), + Data: dto.ToSalesOrdersListDTOFromMarketing(*result), }) } @@ -109,7 +115,13 @@ func (u *SalesOrdersController) UpdateOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - result, err := u.SalesOrdersService.UpdateOne(c, req, uint(id)) + _, err = u.SalesOrdersService.UpdateOne(c, req, uint(id)) + if err != nil { + return err + } + + // Fetch full updated data for response + result, err := u.SalesOrdersService.GetOne(c, uint(id)) if err != nil { return err } @@ -119,7 +131,7 @@ func (u *SalesOrdersController) UpdateOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Update salesOrders successfully", - Data: dto.ToSalesOrdersListDTO(*result), + Data: dto.ToSalesOrdersListDTOFromMarketing(*result), }) } @@ -142,3 +154,35 @@ func (u *SalesOrdersController) DeleteOne(c *fiber.Ctx) error { Message: "Delete salesOrders successfully", }) } + +func (u *SalesOrdersController) Approval(c *fiber.Ctx) error { + req := new(validation.Approve) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + results, err := u.SalesOrdersService.Approval(c, req) + if err != nil { + return err + } + + var ( + data interface{} + message = "Submit sales order approval successfully" + ) + if len(results) == 1 { + data = dto.ToSalesOrdersListDTOFromMarketing(results[0]) + } else { + message = "Submit sales order approvals successfully" + data = dto.ToSalesOrdersListDTOsFromMarketing(results) + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: message, + Data: data, + }) +} diff --git a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go index 6dd7c8e3..920b8d26 100644 --- a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go +++ b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go @@ -4,21 +4,60 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" + productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" + warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" ) // === DTO Structs === type SalesOrdersBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` + Id uint `json:"id"` + Name string `json:"name"` +} + +type MarketingProductDTO struct { + Id uint `json:"id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + ProductWarehouse *struct { + Id uint `json:"id"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` + } `json:"product_warehouse,omitempty"` +} + +type MarketingDeliveryProductDTO struct { + Id uint `json:"id"` + MarketingProductId uint `json:"marketing_product_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalWeight float64 `json:"total_weight"` + AvgWeight float64 `json:"avg_weight"` + TotalPrice float64 `json:"total_price"` + DeliveryDate *time.Time `json:"delivery_date"` + VehicleNumber string `json:"vehicle_number"` } type SalesOrdersListDTO struct { SalesOrdersBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + CustomerId uint `json:"customer_id,omitempty"` + Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` + SoDate *time.Time `json:"so_date,omitempty"` + SalesPersonId uint `json:"sales_person_id,omitempty"` + Notes string `json:"notes,omitempty"` + MarketingProducts []MarketingProductDTO `json:"marketing_products,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval approvalDTO.ApprovalBaseDTO `json:"approval"` } type SalesOrdersDetailDTO struct { @@ -29,11 +68,92 @@ type SalesOrdersDetailDTO struct { func ToSalesOrdersBaseDTO(e entity.SalesOrders) SalesOrdersBaseDTO { return SalesOrdersBaseDTO{ - Id: e.Id, - Name: e.Name, + Id: e.Id, + Name: e.Name, } } +func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { + var productWarehouse *struct { + Id uint `json:"id"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` + } + + if e.ProductWarehouse.Id != 0 { + product := (*productDTO.ProductBaseDTO)(nil) + if e.ProductWarehouse.Product.Id != 0 { + mapped := productDTO.ToProductBaseDTO(e.ProductWarehouse.Product) + product = &mapped + } + + warehouse := (*warehouseDTO.WarehouseBaseDTO)(nil) + if e.ProductWarehouse.Warehouse.Id != 0 { + mapped := warehouseDTO.ToWarehouseBaseDTO(e.ProductWarehouse.Warehouse) + warehouse = &mapped + } + + productWarehouse = &struct { + Id uint `json:"id"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` + }{ + Id: e.ProductWarehouse.Id, + Product: product, + Warehouse: warehouse, + } + } + + return MarketingProductDTO{ + Id: e.Id, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + AvgWeight: e.AvgWeight, + TotalWeight: e.TotalWeight, + TotalPrice: e.TotalPrice, + ProductWarehouse: productWarehouse, + } +} + +func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingDeliveryProductDTO { + return MarketingDeliveryProductDTO{ + Id: e.Id, + MarketingProductId: e.MarketingProductId, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + TotalWeight: e.TotalWeight, + AvgWeight: e.AvgWeight, + TotalPrice: e.TotalPrice, + DeliveryDate: e.DeliveryDate, + VehicleNumber: e.VehicleNumber, + } +} + +func defaultSalesOrdersLatestApproval(e entity.SalesOrders) approvalDTO.ApprovalBaseDTO { + result := approvalDTO.ApprovalBaseDTO{} + + step := utils.MarketingStepPengajuan + if step > 0 { + result.StepNumber = uint16(step) + if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowMarketing, step); ok { + result.StepName = label + } else if label, ok := utils.MarketingApprovalSteps[step]; ok { + result.StepName = label + } + } + + if e.CreatedUser.Id != 0 { + result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser) + } else if e.CreatedBy != 0 { + result.ActionBy = userDTO.UserBaseDTO{ + Id: e.CreatedBy, + IdUser: int64(e.CreatedBy), + } + } + + return result +} + func ToSalesOrdersListDTO(e entity.SalesOrders) SalesOrdersListDTO { var createdUser *userDTO.UserBaseDTO if e.CreatedUser.Id != 0 { @@ -41,14 +161,97 @@ func ToSalesOrdersListDTO(e entity.SalesOrders) SalesOrdersListDTO { createdUser = &mapped } + approval := defaultSalesOrdersLatestApproval(e) + if e.LatestApproval != nil { + approval = approvalDTO.ToApprovalDTO(*e.LatestApproval) + } + return SalesOrdersListDTO{ SalesOrdersBaseDTO: ToSalesOrdersBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + Approval: approval, } } +func ToSalesOrdersListDTOFromMarketing(e entity.Marketing) SalesOrdersListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + createdUser = &mapped + } + + var marketingProducts []MarketingProductDTO + if len(e.Products) > 0 { + marketingProducts = make([]MarketingProductDTO, len(e.Products)) + for i, product := range e.Products { + marketingProducts[i] = ToMarketingProductDTO(product) + } + } + + var customerSummary *customerDTO.CustomerBaseDTO + if e.Customer.Id != 0 { + mapped := customerDTO.ToCustomerBaseDTO(e.Customer) + customerSummary = &mapped + } + + approval := defaultSalesOrdersLatestApprovalFromMarketing(e) + if e.LatestApproval != nil { + approval = approvalDTO.ToApprovalDTO(*e.LatestApproval) + } + + return SalesOrdersListDTO{ + SalesOrdersBaseDTO: SalesOrdersBaseDTO{ + Id: e.Id, + Name: e.SoNumber, + }, + CustomerId: e.Customer.Id, + Customer: customerSummary, + SoDate: &e.SoDate, + SalesPersonId: e.SalesPersonId, + Notes: e.Notes, + MarketingProducts: marketingProducts, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + Approval: approval, + } +} + +func defaultSalesOrdersLatestApprovalFromMarketing(e entity.Marketing) approvalDTO.ApprovalBaseDTO { + result := approvalDTO.ApprovalBaseDTO{} + + step := utils.MarketingStepPengajuan + if step > 0 { + result.StepNumber = uint16(step) + if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowMarketing, step); ok { + result.StepName = label + } else if label, ok := utils.MarketingApprovalSteps[step]; ok { + result.StepName = label + } + } + + if e.CreatedUser.Id != 0 { + result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser) + } else if e.CreatedBy != 0 { + result.ActionBy = userDTO.UserBaseDTO{ + Id: e.CreatedBy, + IdUser: int64(e.CreatedBy), + } + } + + return result +} + +func ToSalesOrdersListDTOsFromMarketing(e []entity.Marketing) []SalesOrdersListDTO { + result := make([]SalesOrdersListDTO, len(e)) + for i, r := range e { + result[i] = ToSalesOrdersListDTOFromMarketing(r) + } + return result +} + func ToSalesOrdersListDTOs(e []entity.SalesOrders) []SalesOrdersListDTO { result := make([]SalesOrdersListDTO, len(e)) for i, r := range e { diff --git a/internal/modules/marketing/sales-orders/module.go b/internal/modules/marketing/sales-orders/module.go index 21926f93..35d002fc 100644 --- a/internal/modules/marketing/sales-orders/module.go +++ b/internal/modules/marketing/sales-orders/module.go @@ -5,6 +5,8 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" @@ -25,7 +27,8 @@ func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valida productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) marketingRepo := rSalesOrders.NewMarketingRepository(db) - salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, kandangRepo, productWarehouseRepo, marketingRepo, validate) + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db)) + salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, kandangRepo, productWarehouseRepo, marketingRepo, userRepo, approvalSvc, validate) userService := sUser.NewUserService(userRepo, validate) SalesOrdersRoutes(router, userService, salesOrdersService) diff --git a/internal/modules/marketing/sales-orders/repositories/marketing-delivery-products.repository.go b/internal/modules/marketing/sales-orders/repositories/marketing-delivery-products.repository.go new file mode 100644 index 00000000..95e9b3bb --- /dev/null +++ b/internal/modules/marketing/sales-orders/repositories/marketing-delivery-products.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type MarketingDeliveryProductRepository interface { + repository.BaseRepository[entity.MarketingDeliveryProduct] +} + +type MarketingDeliveryProductRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.MarketingDeliveryProduct] +} + +func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProductRepository { + return &MarketingDeliveryProductRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db), + } +} diff --git a/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go b/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go new file mode 100644 index 00000000..d3a6798f --- /dev/null +++ b/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go @@ -0,0 +1,35 @@ +package repository + +import ( + "context" + + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type MarketingProductRepository interface { + repository.BaseRepository[entity.MarketingProduct] + GetByMarketingID(ctx context.Context, marketingID uint) ([]entity.MarketingProduct, error) +} + +type MarketingProductRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.MarketingProduct] +} + +func NewMarketingProductRepository(db *gorm.DB) MarketingProductRepository { + return &MarketingProductRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingProduct](db), + } +} + +func (r *MarketingProductRepositoryImpl) GetByMarketingID(ctx context.Context, marketingID uint) ([]entity.MarketingProduct, error) { + var products []entity.MarketingProduct + if err := r.DB().WithContext(ctx).Where("marketing_id = ?", marketingID).Find(&products).Error; err != nil { + return nil, err + } + if len(products) == 0 { + return products, gorm.ErrRecordNotFound + } + return products, nil +} diff --git a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go index bb4896cb..f06bf401 100644 --- a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go +++ b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go @@ -1,6 +1,8 @@ package repository import ( + "context" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" @@ -8,6 +10,7 @@ import ( type MarketingRepository interface { repository.BaseRepository[entity.Marketing] + IdExists(ctx context.Context, id uint) (bool, error) } type MarketingRepositoryImpl struct { @@ -19,3 +22,7 @@ func NewMarketingRepository(db *gorm.DB) MarketingRepository { BaseRepositoryImpl: repository.NewBaseRepository[entity.Marketing](db), } } + +func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.Marketing](ctx, r.DB(), id) +} diff --git a/internal/modules/marketing/sales-orders/route.go b/internal/modules/marketing/sales-orders/route.go index 455977fb..c48ae2a7 100644 --- a/internal/modules/marketing/sales-orders/route.go +++ b/internal/modules/marketing/sales-orders/route.go @@ -25,4 +25,5 @@ func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesO route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) + route.Post("/approvals", ctrl.Approval) } diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 0a1e5898..67163564 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -2,8 +2,11 @@ package service import ( "errors" + "fmt" + "strings" "time" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" @@ -11,7 +14,9 @@ import ( validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations" customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" + userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -20,11 +25,12 @@ import ( ) type SalesOrdersService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.SalesOrders, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.SalesOrders, error) - CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.SalesOrders, error) - UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.SalesOrders, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.Marketing, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Marketing, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) DeleteOne(ctx *fiber.Ctx, id uint) error + Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Marketing, error) } type salesOrdersService struct { @@ -35,9 +41,11 @@ type salesOrdersService struct { KandangRepo kandangRepo.KandangRepository ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository MarketingRepo repository.MarketingRepository + UserRepo userRepo.UserRepository + ApprovalSvc commonSvc.ApprovalService } -func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, kandangRepo kandangRepo.KandangRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, validate *validator.Validate) SalesOrdersService { +func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, kandangRepo kandangRepo.KandangRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService { return &salesOrdersService{ Log: utils.Log, Validate: validate, @@ -46,48 +54,94 @@ func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo c KandangRepo: kandangRepo, ProductWarehouseRepo: productWarehouseRepo, MarketingRepo: marketingRepo, + UserRepo: userRepo, + ApprovalSvc: approvalSvc, } } func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser") + return db.Preload("CreatedUser"). + Preload("Customer"). + Preload("SalesPerson"). + Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Warehouse"). + Preload("Products.DeliveryProducts") } -func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.SalesOrders, int64, error) { +func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit - salesOrderss, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) if params.Search != "" { - return db.Where("name LIKE ?", "%"+params.Search+"%") + return db.Where("so_number LIKE ?", "%"+params.Search+"%") } return db.Order("created_at DESC").Order("updated_at DESC") }) if err != nil { - s.Log.Errorf("Failed to get salesOrderss: %+v", err) - return nil, 0, err + s.Log.Errorf("Failed to get marketings: %+v", err) + return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales orders") } - return salesOrderss, total, nil + + if s.ApprovalSvc != nil && len(marketings) > 0 { + ids := make([]uint, len(marketings)) + for i, item := range marketings { + ids[i] = item.Id + } + + latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowMarketing, ids, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Unable to load latest approvals for marketings: %+v", err) + } else if len(latestMap) > 0 { + for i := range marketings { + if approval, ok := latestMap[marketings[i].Id]; ok { + marketings[i].LatestApproval = approval + } + } + } + } + + return marketings, total, nil } -func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.SalesOrders, error) { - salesOrders, err := s.Repository.GetByID(c.Context(), id, s.withRelations) +func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) { + + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") } if err != nil { - s.Log.Errorf("Failed get salesOrders by id: %+v", err) - return nil, err + s.Log.Errorf("Failed get marketing by id: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") } - return salesOrders, nil + + if s.ApprovalSvc != nil { + approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Unable to load approvals for marketing %d: %+v", id, err) + } else if len(approvals) > 0 { + if marketing.LatestApproval == nil { + latest := approvals[len(approvals)-1] + marketing.LatestApproval = &latest + } + } else { + marketing.LatestApproval = nil + } + } + + return marketing, nil } -func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.SalesOrders, error) { +func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Marketing, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } @@ -107,25 +161,26 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e } } - // parse date soDate, err := utils.ParseDateString(req.Date) if err != nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format") } - // generate SO number soNumber := "SO-" + time.Now().Format("20060102150405") var marketing *entity.Marketing - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { - // create marketing directly (tanpa create SalesOrders) + marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) + marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + marketing = &entity.Marketing{ CustomerId: req.CustomerId, SoNumber: soNumber, SoDate: soDate, - SalesPersonId: 1, + SalesPersonId: req.SalesPersonId, Notes: req.Notes, CreatedBy: 1, } @@ -134,7 +189,6 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return err } - // Create MarketingProducts and MarketingDeliveryProducts if len(req.MarketingProducts) > 0 { for _, product := range req.MarketingProducts { marketingProduct := &entity.MarketingProduct{ @@ -146,12 +200,11 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e TotalWeight: product.TotalWeight, TotalPrice: product.TotalPrice, } - if err := dbTransaction.Create(marketingProduct).Error; err != nil { + if err := marketingProductRepoTx.CreateOne(c.Context(), marketingProduct, nil); err != nil { s.Log.Errorf("Failed to create marketing product: %+v", err) return err } - // create delivery product with zeroed numeric fields and nil delivery date marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ MarketingProductId: marketingProduct.Id, Qty: 0, @@ -162,13 +215,37 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e DeliveryDate: nil, VehicleNumber: product.VehicleNumber, } - if err := dbTransaction.Create(marketingDeliveryProduct).Error; err != nil { + if err := marketingDeliveryProductRepoTx.CreateOne(c.Context(), marketingDeliveryProduct, nil); err != nil { s.Log.Errorf("Failed to create marketing delivery product: %+v", err) return err } } } + actorID := uint(1) // TODO: ambil dari auth context + if err := approvalSvcTx.RegisterWorkflowSteps( + utils.ApprovalWorkflowMarketing, + utils.MarketingApprovalSteps, + ); err != nil { + s.Log.Errorf("Failed to register workflow steps: %+v", err) + return err + } + + approvalAction := entity.ApprovalActionCreated + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + marketing.Id, + utils.MarketingStepPengajuan, + &approvalAction, + actorID, + nil); err != nil { + if !errors.Is(err, gorm.ErrDuplicatedKey) { + s.Log.Errorf("Failed to create approval: %+v", err) + return err + } + } + return nil }) if err != nil { @@ -178,51 +255,320 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders") } - createdMarketing, err := s.MarketingRepo.GetByID(c.Context(), marketing.Id, nil) - if err != nil { - s.Log.Errorf("Failed to fetch created marketing: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created marketing") - } - - return &entity.SalesOrders{ - Id: createdMarketing.Id, - Name: createdMarketing.SoNumber, - }, nil + return s.GetOne(c, marketing.Id) } -func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.SalesOrders, error) { +func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } - updateBody := make(map[string]any) - - if req.Name != nil { - updateBody["name"] = *req.Name - } - - if len(updateBody) == 0 { - return s.GetOne(c, id) - } - - if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") - } - s.Log.Errorf("Failed to update salesOrders: %+v", err) + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, + commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists}, + commonSvc.RelationCheck{Name: "SalesPerson", ID: &req.SalesPersonId, Exists: s.UserRepo.IdExists}, + ); err != nil { return nil, err } + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil) + if err != nil { + s.Log.Errorf("Failed to check approval status: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + if latestApproval != nil && latestApproval.StepNumber >= 3 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Cannot update sales order after delivery order approval") + } + + if len(req.MarketingProducts) > 0 { + for _, item := range req.MarketingProducts { + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, + ); err != nil { + return nil, err + } + } + } + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) + marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + + updateBody := make(map[string]any) + if req.CustomerId != 0 { + updateBody["customer_id"] = req.CustomerId + } + if req.SalesPersonId != 0 { + updateBody["sales_person_id"] = req.SalesPersonId + } + if req.Date != "" { + soDate, err := utils.ParseDateString(req.Date) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid date format") + } + updateBody["so_date"] = soDate + } + if req.Notes != "" { + updateBody["notes"] = req.Notes + } + + if len(updateBody) > 0 { + if err := marketingRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order") + } + } + + if len(req.MarketingProducts) > 0 { + + oldProducts, err := marketingProductRepoTx.GetByMarketingID(c.Context(), id) + if err != nil && err != gorm.ErrRecordNotFound { + s.Log.Errorf("Failed to fetch old marketing products: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + } + + for _, oldProduct := range oldProducts { + if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("marketing_product_id = ?", oldProduct.Id) + }); err != nil && err != gorm.ErrRecordNotFound { + s.Log.Errorf("Failed to delete delivery products: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + } + } + + if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("marketing_id = ?", id) + }); err != nil && err != gorm.ErrRecordNotFound { + s.Log.Errorf("Failed to delete marketing products: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + } + + for _, product := range req.MarketingProducts { + marketingProduct := &entity.MarketingProduct{ + MarketingId: id, + ProductWarehouseId: product.ProductWarehouseId, + Qty: product.Qty, + UnitPrice: product.UnitPrice, + AvgWeight: product.AvgWeight, + TotalWeight: product.TotalWeight, + TotalPrice: product.TotalPrice, + } + if err := marketingProductRepoTx.CreateOne(c.Context(), marketingProduct, nil); err != nil { + + return err + } + + marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ + MarketingProductId: marketingProduct.Id, + Qty: 0, + UnitPrice: 0, + TotalWeight: 0, + AvgWeight: 0, + TotalPrice: 0, + DeliveryDate: nil, + VehicleNumber: product.VehicleNumber, + } + if err := marketingDeliveryProductRepoTx.CreateOne(c.Context(), marketingDeliveryProduct, nil); err != nil { + return err + } + } + } + + if latestApproval != nil && latestApproval.StepNumber == 2 { + actorID := uint(1) // todo: ambil dari auth context + resetNote := "" + action := entity.ApprovalActionApproved + _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + id, + utils.MarketingStepPengajuan, + &action, + actorID, + &resetNote) + if err != nil { + s.Log.Errorf("Failed to create reset approval: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval status") + } + } + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order") + } + return s.GetOne(c, id) } func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { - if err := s.Repository.DeleteOne(c.Context(), id); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") - } - s.Log.Errorf("Failed to delete salesOrders: %+v", err) - return err + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") } + if err != nil { + s.Log.Errorf("Failed to fetch marketing %d before delete: %+v", id, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") + } + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) + marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) + marketingRepoTx := repository.NewMarketingRepository(dbTransaction) + + if len(marketing.Products) > 0 { + for _, product := range marketing.Products { + if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("marketing_product_id = ?", product.Id) + }); err != nil && err != gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products") + } + } + } + + if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { + return db.Where("marketing_id = ?", id) + }); err != nil && err != gorm.ErrRecordNotFound { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products") + } + + if err := marketingRepoTx.DeleteOne(c.Context(), id); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order") + } + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return fiberErr + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order") + } + return nil } + +func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Marketing, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) + + var action entity.ApprovalAction + switch strings.ToUpper(strings.TrimSpace(req.Action)) { + case string(entity.ApprovalActionRejected): + action = entity.ApprovalActionRejected + case string(entity.ApprovalActionApproved): + action = entity.ApprovalActionApproved + default: + return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") + } + + approvableIDs := utils.UniqueUintSlice(req.ApprovableIds) + if len(approvableIDs) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") + } + + for _, id := range approvableIDs { + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, + ); err != nil { + return nil, err + } + + latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") + } + + if latestApproval == nil { + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d - sales orders must be created first", id)) + } + + if action == entity.ApprovalActionApproved { + switch latestApproval.StepNumber { + case uint16(utils.MarketingStepPengajuan): + case uint16(utils.MarketingStepSalesOrder): + default: + return nil, fiber.NewError(fiber.StatusBadRequest, + fmt.Sprintf("Marketing %d cannot be approved - current step is %d", id, latestApproval.StepNumber)) + } + } + } + + err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + + for _, approvableID := range approvableIDs { + + latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, approvableID, nil) + if err != nil { + s.Log.Errorf("Failed to get latest approval for %d: %+v", approvableID, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check current approval step") + } + + if latestApproval == nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d", approvableID)) + } + + var nextStep approvalutils.ApprovalStep + currentStep := latestApproval.StepNumber + + if action == entity.ApprovalActionApproved { + + if currentStep == uint16(utils.MarketingStepPengajuan) { + nextStep = utils.MarketingStepSalesOrder + } else if currentStep == uint16(utils.MarketingStepSalesOrder) { + nextStep = utils.MarketingDeliveryOrder + } else { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d already completed all approval steps", approvableID)) + } + } else { + + nextStep = approvalutils.ApprovalStep(currentStep) + } + + actorID := uint(1) // todo ambil dari auth context + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + approvableID, + nextStep, + &action, + actorID, + req.Notes, + ); err != nil { + s.Log.Errorf("Failed to create approval for %d: %+v", approvableID, err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") + } + } + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") + } + + updated := make([]entity.Marketing, 0, len(approvableIDs)) + for _, id := range approvableIDs { + marketing, err := s.GetOne(c, id) + if err != nil { + return nil, err + } + updated = append(updated, *marketing) + } + + return updated, nil +} diff --git a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go index b09129bc..01b3af9d 100644 --- a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go +++ b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go @@ -2,6 +2,7 @@ package validation type Create struct { CustomerId uint `json:"customer_id" validate:"required,gt=0"` + SalesPersonId uint `json:"sales_person_id" validate:"required,gt=0"` Date string `json:"date" validate:"required,datetime=2006-01-02"` Notes string `json:"notes" validate:"omitempty,max=500"` MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"required,min=1,dive"` @@ -19,7 +20,11 @@ type CreateMarketingProduct struct { } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + CustomerId uint `json:"customer_id" validate:"omitempty,gt=0"` + SalesPersonId uint `json:"sales_person_id" validate:"omitempty,gt=0"` + Date string `json:"date" validate:"omitempty,datetime=2006-01-02"` + Notes string `json:"notes" validate:"omitempty,max=500"` + MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"omitempty,min=1,dive"` } type Query struct { @@ -27,3 +32,9 @@ type Query struct { Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Search string `query:"search" validate:"omitempty,max=50"` } + +type Approve struct { + Action string `json:"action" validate:"required_strict"` + ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` +} diff --git a/internal/modules/users/repositories/user.repository.go b/internal/modules/users/repositories/user.repository.go index 8472db13..28c06a74 100644 --- a/internal/modules/users/repositories/user.repository.go +++ b/internal/modules/users/repositories/user.repository.go @@ -1,6 +1,8 @@ package repository import ( + "context" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" @@ -8,14 +10,21 @@ import ( type UserRepository interface { repository.BaseRepository[entity.User] + IdExists(ctx context.Context, id uint) (bool, error) } type UserRepositoryImpl struct { *repository.BaseRepositoryImpl[entity.User] + db *gorm.DB } func NewUserRepository(db *gorm.DB) UserRepository { return &UserRepositoryImpl{ BaseRepositoryImpl: repository.NewBaseRepository[entity.User](db), + db: db, } } + +func (r *UserRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.User](ctx, r.db, id) +} diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 099b6510..fc01a231 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -224,6 +224,23 @@ var PurchaseApprovalSteps = map[approvalutils.ApprovalStep]string{ PurchaseStepStaffPurchase: "Staff Purchase", } +// ------------------------------------------------------------------- +// Marketings Approval +// ------------------------------------------------------------------- + +const ( + ApprovalWorkflowMarketing approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("MARKETINGS") + MarketingStepPengajuan approvalutils.ApprovalStep = 1 + MarketingStepSalesOrder approvalutils.ApprovalStep = 2 + MarketingDeliveryOrder approvalutils.ApprovalStep = 3 +) + +var MarketingApprovalSteps = map[approvalutils.ApprovalStep]string{ + MarketingStepPengajuan: "Pengajuan", + MarketingStepSalesOrder: "Sales Order", + MarketingDeliveryOrder: "Delivery Order", +} + // ------------------------------------------------------------------- // Validators // ------------------------------------------------------------------- From 74ec25db5b38714a6adf6fb7507992ecb30f67d3 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 13 Nov 2025 09:50:34 +0700 Subject: [PATCH 19/29] Feat[BE-222.223.224]: creating create one delivery order and getone delivery order[Unfinished] --- internal/entities/marketing_product.go | 6 +- .../marketing-delivery-products.repository.go | 11 ++ .../marketing/delivery-orderss/module.go | 4 +- .../services/delivery-orders.service.go | 152 +++++++++--------- .../controllers/sales-orders.controller.go | 2 +- .../sales-orders/dto/sales-orders.dto.go | 8 + .../marketing-products.repository.go | 5 + .../services/sales-orders.service.go | 10 +- 8 files changed, 119 insertions(+), 79 deletions(-) diff --git a/internal/entities/marketing_product.go b/internal/entities/marketing_product.go index f0fe7f38..66524bc6 100644 --- a/internal/entities/marketing_product.go +++ b/internal/entities/marketing_product.go @@ -19,7 +19,7 @@ type MarketingProduct struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` - ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` - DeliveryProducts []MarketingDeliveryProduct `gorm:"foreignKey:MarketingProductId;references:Id"` + Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` + ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + DeliveryProduct *MarketingDeliveryProduct `gorm:"foreignKey:MarketingProductId;references:Id"` } diff --git a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go index 95e9b3bb..ce94a1eb 100644 --- a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go +++ b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go @@ -1,6 +1,8 @@ package repository import ( + "context" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" @@ -8,6 +10,7 @@ import ( type MarketingDeliveryProductRepository interface { repository.BaseRepository[entity.MarketingDeliveryProduct] + GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) } type MarketingDeliveryProductRepositoryImpl struct { @@ -19,3 +22,11 @@ func NewMarketingDeliveryProductRepository(db *gorm.DB) MarketingDeliveryProduct BaseRepositoryImpl: repository.NewBaseRepository[entity.MarketingDeliveryProduct](db), } } + +func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) { + var deliveryProduct entity.MarketingDeliveryProduct + if err := r.DB().WithContext(ctx).Where("marketing_product_id = ?", marketingProductID).First(&deliveryProduct).Error; err != nil { + return nil, err + } + return &deliveryProduct, nil +} diff --git a/internal/modules/marketing/delivery-orderss/module.go b/internal/modules/marketing/delivery-orderss/module.go index c6932c51..7fcc8ccc 100644 --- a/internal/modules/marketing/delivery-orderss/module.go +++ b/internal/modules/marketing/delivery-orderss/module.go @@ -11,6 +11,7 @@ import ( rDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/repositories" sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services" rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" + rMarketingProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" ) @@ -20,12 +21,13 @@ type DeliveryOrdersModule struct{} func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { deliveryOrdersRepo := rDeliveryOrders.NewDeliveryOrdersRepository(db) marketingRepo := rMarketing.NewMarketingRepository(db) + marketingProductRepo := rMarketingProduct.NewMarketingProductRepository(db) marketingDeliveryProductRepo := rMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(db) userRepo := rUser.NewUserRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalSvc := commonSvc.NewApprovalService(approvalRepo) - deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(deliveryOrdersRepo, marketingRepo, marketingDeliveryProductRepo, approvalSvc, validate) + deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(deliveryOrdersRepo, marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate) userService := sUser.NewUserService(userRepo, validate) DeliveryOrdersRoutes(router, userService, deliveryOrdersService) diff --git a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go index db238d0f..7a2efa3f 100644 --- a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go +++ b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go @@ -35,6 +35,7 @@ type deliveryOrdersService struct { Validate *validator.Validate Repository repository.DeliveryOrdersRepository MarketingRepo marketingRepo.MarketingRepository + MarketingProductRepo marketingRepo.MarketingProductRepository MarketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository ApprovalSvc commonSvc.ApprovalService } @@ -42,6 +43,7 @@ type deliveryOrdersService struct { func NewDeliveryOrdersService( repo repository.DeliveryOrdersRepository, marketingRepo marketingRepo.MarketingRepository, + marketingProductRepo marketingRepo.MarketingProductRepository, marketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate, @@ -51,6 +53,7 @@ func NewDeliveryOrdersService( Validate: validate, Repository: repo, MarketingRepo: marketingRepo, + MarketingProductRepo: marketingProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, ApprovalSvc: approvalSvc, } @@ -89,7 +92,7 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([ result := make([]dto.DeliveryOrdersListDTO, len(marketings)) for i, marketing := range marketings { // Get marketing delivery products - var deliveryProducts []entity.MarketingDeliveryProduct + var allDeliveryProducts []entity.MarketingDeliveryProduct if err := s.Repository.DB().WithContext(c.Context()). Preload("MarketingProduct"). Where("marketing_product_id IN (?)", @@ -97,20 +100,20 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([ Model(&entity.MarketingProduct{}). Select("id"). Where("marketing_id = ?", marketing.Id)). - Find(&deliveryProducts).Error; err != nil { + Find(&allDeliveryProducts).Error; err != nil { s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) // Continue without products } - // Create dummy DeliveryOrders untuk dto mapping - dummyDO := &entity.DeliveryOrders{ + // Build response DTO + deliveryOrderResponse := &entity.DeliveryOrders{ MarketingId: marketing.Id, CreatedUser: &marketing.CreatedUser, Marketing: &marketing, - DeliveryProducts: deliveryProducts, + DeliveryProducts: allDeliveryProducts, } - result[i] = dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) + result[i] = dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) } return result, total, nil @@ -133,7 +136,7 @@ func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.DeliveryOrder } // Get marketing delivery products - var deliveryProducts []entity.MarketingDeliveryProduct + var allDeliveryProducts []entity.MarketingDeliveryProduct if err := s.Repository.DB().WithContext(c.Context()). Preload("MarketingProduct"). Where("marketing_product_id IN (?)", @@ -141,21 +144,21 @@ func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.DeliveryOrder Model(&entity.MarketingProduct{}). Select("id"). Where("marketing_id = ?", marketing.Id)). - Find(&deliveryProducts).Error; err != nil { + Find(&allDeliveryProducts).Error; err != nil { s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) // Continue without products } - // Create dummy DeliveryOrders untuk dto mapping - dummyDO := &entity.DeliveryOrders{ + // Build response DTO + deliveryOrderResponse := &entity.DeliveryOrders{ MarketingId: marketing.Id, CreatedUser: &marketing.CreatedUser, Marketing: marketing, - DeliveryProducts: deliveryProducts, + DeliveryProducts: allDeliveryProducts, } - result := dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) - return &result, nil + responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) + return &responseDTO, nil } func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) { @@ -163,88 +166,93 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, err } - // Validate marketing exists - _, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, nil) - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") - } - if err != nil { - s.Log.Errorf("Failed to fetch marketing %d: %+v", req.MarketingId, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Marketing", ID: &req.MarketingId, Exists: s.MarketingRepo.IdExists}, + ); err != nil { + return nil, err + } + + var relationChecks []commonSvc.RelationCheck + for _, requestedProduct := range req.DeliveryProducts { + relationChecks = append(relationChecks, commonSvc.RelationCheck{ + Name: "MarketingProduct", ID: &requestedProduct.MarketingProductId, Exists: s.MarketingProductRepo.IdExists, + }) } - // Validate marketing approval status - harus sudah di approve ke step Sales Order approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } - if latestApproval == nil { return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing has not been submitted for approval") } - - // Cek apakah status approval sudah Sales Order (step 2) atau lebih if latestApproval.StepNumber < uint16(utils.MarketingStepSalesOrder) { return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing must be approved to Sales Order step before creating delivery order") } - if latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved { return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing is not approved for delivery") } - // Validate semua delivery products ada dan update mereka - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTx *gorm.DB) error { - for _, product := range req.DeliveryProducts { - // Fetch marketing_product terlebih dahulu untuk pastikan punya marketing_id yang sama - var marketingProduct entity.MarketingProduct - if err := dbTx.Where("id = ? AND marketing_id = ?", product.MarketingProductId, req.MarketingId). - First(&marketingProduct).Error; err != nil { + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + + marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) + marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction) + + for _, requestedProduct := range req.DeliveryProducts { + allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId) + if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", product.MarketingProductId)) + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", req.MarketingId)) } - s.Log.Errorf("Failed to fetch marketing product: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing product") + s.Log.Errorf("Failed to fetch marketing products: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") } - // Fetch marketing_delivery_product by marketing_product_id - var mdp entity.MarketingDeliveryProduct - if err := dbTx.Where("marketing_product_id = ?", marketingProduct.Id). - First(&mdp).Error; err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", product.MarketingProductId)) + var foundMarketingProduct *entity.MarketingProduct + for i := range allMarketingProducts { + if allMarketingProducts[i].Id == requestedProduct.MarketingProductId { + foundMarketingProduct = &allMarketingProducts[i] + break + } + } + if foundMarketingProduct == nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId)) + } + + deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", requestedProduct.MarketingProductId)) } - s.Log.Errorf("Failed to fetch marketing delivery product: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") } - // Parse delivery date per item (jika ada), atau gunakan default - itemDeliveryDate := time.Now() - if product.DeliveryDate != "" { - parsedItemDate, err := time.Parse("2006-01-02", product.DeliveryDate) + var itemDeliveryDate time.Time + if requestedProduct.DeliveryDate != "" { + parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) if err != nil { - return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d", product.MarketingProductId)) + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err)) } - itemDeliveryDate = parsedItemDate + itemDeliveryDate = parsedDate + } else { + itemDeliveryDate = time.Now() } - // Update dengan data dari request - PASTIKAN UPDATE LANGSUNG KE FIELD - updates := map[string]interface{}{ - "qty": product.Qty, - "unit_price": product.UnitPrice, - "avg_weight": product.AvgWeight, - "total_weight": product.TotalWeight, - "total_price": product.TotalPrice, - "delivery_date": &itemDeliveryDate, - "vehicle_number": product.VehicleNumber, - } + deliveryProduct.Qty = requestedProduct.Qty + deliveryProduct.UnitPrice = requestedProduct.UnitPrice + deliveryProduct.AvgWeight = requestedProduct.AvgWeight + deliveryProduct.TotalWeight = requestedProduct.TotalWeight + deliveryProduct.TotalPrice = requestedProduct.TotalPrice + deliveryProduct.DeliveryDate = &itemDeliveryDate + deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber - if err := dbTx.Model(&mdp).Updates(updates).Error; err != nil { + if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { s.Log.Errorf("Failed to update marketing delivery product: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } - s.Log.Infof("Updated MDP %d: qty=%v, unitPrice=%v, totalPrice=%v", mdp.Id, product.Qty, product.UnitPrice, product.TotalPrice) + s.Log.Infof("Updated delivery product %d: qty=%v, unitPrice=%v, totalPrice=%v", deliveryProduct.Id, requestedProduct.Qty, requestedProduct.UnitPrice, requestedProduct.TotalPrice) } return nil @@ -254,9 +262,9 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, err } - // Fetch marketing dengan delivery products yang sudah di-update + // Fetch marketing dengan delivery products yang sudah di-updated marketing, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, func(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser").Preload("Products") + return db.Preload("CreatedUser").Preload("Products.DeliveryProduct") }) if err != nil { s.Log.Errorf("Failed to fetch marketing after update: %+v", err) @@ -264,30 +272,30 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) } // Get marketing delivery products - var deliveryProducts []entity.MarketingDeliveryProduct - if err := s.Repository.DB().WithContext(c.Context()). + var allDeliveryProducts []entity.MarketingDeliveryProduct + if err := s.MarketingDeliveryProductRepo.DB().WithContext(c.Context()). Preload("MarketingProduct"). Where("marketing_product_id IN (?)", - s.Repository.DB().WithContext(c.Context()). + s.MarketingProductRepo.DB().WithContext(c.Context()). Model(&entity.MarketingProduct{}). Select("id"). Where("marketing_id = ?", req.MarketingId)). - Find(&deliveryProducts).Error; err != nil { + Find(&allDeliveryProducts).Error; err != nil { s.Log.Errorf("Failed to load delivery products: %+v", err) // Continue tanpa delivery products } - // Create dummy DeliveryOrders untuk dipakai dto mapping - dummyDO := &entity.DeliveryOrders{ + // Build response DTO + deliveryOrderResponse := &entity.DeliveryOrders{ MarketingId: req.MarketingId, Notes: req.Notes, CreatedUser: &marketing.CreatedUser, Marketing: marketing, - DeliveryProducts: deliveryProducts, + DeliveryProducts: allDeliveryProducts, } - result := dto.ToDeliveryOrdersListDTOWithProducts(*dummyDO, deliveryProducts) - return &result, nil + responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) + return &responseDTO, nil } func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.DeliveryOrders, error) { diff --git a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go index 4c49baeb..a5365787 100644 --- a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go +++ b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go @@ -98,7 +98,7 @@ func (u *SalesOrdersController) CreateOne(c *fiber.Ctx) error { Code: fiber.StatusCreated, Status: "success", Message: "Create salesOrders successfully", - Data: result, + Data: dto.ToSalesOrdersListDTOFromMarketing(*result), }) } diff --git a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go index 920b8d26..79bc6453 100644 --- a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go +++ b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go @@ -32,6 +32,7 @@ type MarketingProductDTO struct { Product *productDTO.ProductBaseDTO `json:"product,omitempty"` Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` } `json:"product_warehouse,omitempty"` + DeliveryProduct *MarketingDeliveryProductDTO `json:"delivery_product,omitempty"` } type MarketingDeliveryProductDTO struct { @@ -104,6 +105,12 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { } } + var deliveryProduct *MarketingDeliveryProductDTO + if e.DeliveryProduct != nil && e.DeliveryProduct.Id != 0 { + mapped := ToMarketingDeliveryProductDTO(*e.DeliveryProduct) + deliveryProduct = &mapped + } + return MarketingProductDTO{ Id: e.Id, Qty: e.Qty, @@ -112,6 +119,7 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { TotalWeight: e.TotalWeight, TotalPrice: e.TotalPrice, ProductWarehouse: productWarehouse, + DeliveryProduct: deliveryProduct, } } diff --git a/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go b/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go index d3a6798f..4d5eb43f 100644 --- a/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go +++ b/internal/modules/marketing/sales-orders/repositories/marketing-products.repository.go @@ -11,6 +11,7 @@ import ( type MarketingProductRepository interface { repository.BaseRepository[entity.MarketingProduct] GetByMarketingID(ctx context.Context, marketingID uint) ([]entity.MarketingProduct, error) + IdExists(ctx context.Context, id uint) (bool, error) } type MarketingProductRepositoryImpl struct { @@ -33,3 +34,7 @@ func (r *MarketingProductRepositoryImpl) GetByMarketingID(ctx context.Context, m } return products, nil } + +func (r *MarketingProductRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { + return repository.Exists[entity.MarketingProduct](ctx, r.DB(), id) +} diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 67163564..1fb36733 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -65,7 +65,7 @@ func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). Preload("Products.ProductWarehouse.Warehouse"). - Preload("Products.DeliveryProducts") + Preload("Products.DeliveryProduct") } func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) { @@ -255,7 +255,13 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders") } - return s.GetOne(c, marketing.Id) + marketing, err = s.MarketingRepo.GetByID(c.Context(), marketing.Id, s.withRelations) + if err != nil { + s.Log.Errorf("Failed to fetch created marketing: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created sales order") + } + + return marketing, nil } func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) { From 2f5fab9f8081740e0bbe01161b03def69bd8771a Mon Sep 17 00:00:00 2001 From: Asep Teguh Hidayat <114033670+Aguh18@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:28:50 +0700 Subject: [PATCH 20/29] Feat[BE-222]: creating update DO(Unfinished) --- ...create_marketing_product_deliveries.up.sql | 2 +- .../marketing-delivery-products.repository.go | 17 ++ .../controllers/delivery-orders.controller.go | 2 +- .../dto/delivery-orders.dto.go | 170 +++++++++++- .../services/delivery-orders.service.go | 253 +++++++++++++----- .../services/sales-orders.service.go | 3 +- 6 files changed, 367 insertions(+), 80 deletions(-) diff --git a/internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql b/internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql index 09625c16..45ca0907 100644 --- a/internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql +++ b/internal/database/migrations/20251107015128_create_marketing_product_deliveries.up.sql @@ -6,7 +6,7 @@ CREATE TABLE marketing_delivery_products ( total_weight NUMERIC(15, 3) NOT NULL, avg_weight NUMERIC(15, 3) NOT NULL, total_price NUMERIC(15, 3) NOT NULL, - delivery_date TIMESTAMPTZ, + delivery_date DATE, vehicle_number VARCHAR(50), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), diff --git a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go index ce94a1eb..96590990 100644 --- a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go +++ b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go @@ -11,6 +11,7 @@ import ( type MarketingDeliveryProductRepository interface { repository.BaseRepository[entity.MarketingDeliveryProduct] GetByMarketingProductID(ctx context.Context, marketingProductID uint) (*entity.MarketingDeliveryProduct, error) + GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) } type MarketingDeliveryProductRepositoryImpl struct { @@ -30,3 +31,19 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingProductID(ctx con } return &deliveryProduct, nil } + +func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Context, marketingId uint) ([]entity.MarketingDeliveryProduct, error) { + var deliveryProducts []entity.MarketingDeliveryProduct + + // Raw query untuk mengambil delivery products berdasarkan marketing ID dengan preload MarketingProduct + if err := r.DB().WithContext(ctx). + Preload("MarketingProduct"). + Joins("INNER JOIN marketing_products mp ON marketing_delivery_products.marketing_product_id = mp.id"). + Where("mp.marketing_id = ?", marketingId). + Order("marketing_delivery_products.id ASC"). + Find(&deliveryProducts).Error; err != nil { + return nil, err + } + + return deliveryProducts, nil +} diff --git a/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go index a5f2839a..8ca51dc5 100644 --- a/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go +++ b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go @@ -119,7 +119,7 @@ func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Update deliveryOrders successfully", - Data: dto.ToDeliveryOrdersListDTO(*result), + Data: result, }) } diff --git a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go index 2b2ea51e..6008269d 100644 --- a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go +++ b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go @@ -1,6 +1,8 @@ package dto import ( + "fmt" + "sort" "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -22,6 +24,22 @@ type MarketingDeliveryProductDTO struct { VehicleNumber string `json:"vehicle_number"` } +// DTO untuk grouping delivery products berdasarkan warehouse dan tanggal +type DeliveryGroupDTO struct { + WarehouseId uint `json:"warehouse_id"` + WarehouseName string `json:"warehouse_name"` + DeliveryDate *time.Time `json:"delivery_date"` + VehicleNumber string `json:"vehicle_number"` + TotalQty float64 `json:"total_qty"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` +} + +// DTO untuk Delivery Order (DO) - berisi data delivery yang sudah digroup +type DeliveryOrderDTO struct { + DeliveryGroups []DeliveryGroupDTO `json:"delivery_groups"` +} + type DeliveryOrdersBaseDTO struct { Id uint `json:"id,omitempty"` DeliveryNumber *string `json:"delivery_number,omitempty"` @@ -30,20 +48,33 @@ type DeliveryOrdersBaseDTO struct { Notes string `json:"notes,omitempty"` } +type MarketingProductDTO struct { + Id uint `json:"id"` + MarketingId uint `json:"marketing_id"` + ProductWarehouseId uint `json:"product_warehouse_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + // Add product relation if needed +} + type MarketingBaseDTO struct { - Id uint `json:"id"` - SoNumber string `json:"so_number"` - SoDate time.Time `json:"so_date"` + Id uint `json:"id"` + SoNumber string `json:"so_number"` + SoDate time.Time `json:"so_date"` + Products []MarketingProductDTO `json:"products,omitempty"` } type DeliveryOrdersListDTO struct { DeliveryOrdersBaseDTO - Marketing *MarketingBaseDTO `json:"marketing,omitempty"` - DeliveryProducts []MarketingDeliveryProductDTO `json:"delivery_products,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` + SalesOrder *MarketingBaseDTO `json:"sales_order,omitempty"` // SO - Sales Order data + DeliveryOrder *DeliveryOrderDTO `json:"delivery_order,omitempty"` // DO - Delivery Order data (grouped) + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` } type DeliveryOrdersDetailDTO struct { @@ -52,6 +83,19 @@ type DeliveryOrdersDetailDTO struct { // === Mapper Functions === +func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { + return MarketingProductDTO{ + Id: e.Id, + MarketingId: e.MarketingId, + ProductWarehouseId: e.ProductWarehouseId, + Qty: e.Qty, + UnitPrice: e.UnitPrice, + AvgWeight: e.AvgWeight, + TotalWeight: e.TotalWeight, + TotalPrice: e.TotalPrice, + } +} + func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingDeliveryProductDTO { return MarketingDeliveryProductDTO{ Id: e.Id, @@ -90,10 +134,19 @@ func ToDeliveryOrdersListDTO(e entity.DeliveryOrders) DeliveryOrdersListDTO { var marketing *MarketingBaseDTO if e.Marketing != nil && e.Marketing.Id != 0 { + var marketingProducts []MarketingProductDTO + if len(e.Marketing.Products) > 0 { + marketingProducts = make([]MarketingProductDTO, len(e.Marketing.Products)) + for i, product := range e.Marketing.Products { + marketingProducts[i] = ToMarketingProductDTO(product) + } + } + marketing = &MarketingBaseDTO{ Id: e.Marketing.Id, SoNumber: e.Marketing.SoNumber, SoDate: e.Marketing.SoDate, + Products: marketingProducts, } } @@ -105,10 +158,16 @@ func ToDeliveryOrdersListDTO(e entity.DeliveryOrders) DeliveryOrdersListDTO { } } + // Group delivery products by warehouse and delivery date + deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs) + + // Create delivery order DTO with summary + deliveryOrder := createDeliveryOrderDTO(deliveryGroups) + return DeliveryOrdersListDTO{ DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), - Marketing: marketing, - DeliveryProducts: deliveryProductsDTOs, + SalesOrder: marketing, + DeliveryOrder: deliveryOrder, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, @@ -124,10 +183,19 @@ func ToDeliveryOrdersListDTOWithProducts(e entity.DeliveryOrders, deliveryProduc var marketing *MarketingBaseDTO if e.Marketing != nil && e.Marketing.Id != 0 { + var marketingProducts []MarketingProductDTO + if len(e.Marketing.Products) > 0 { + marketingProducts = make([]MarketingProductDTO, len(e.Marketing.Products)) + for i, product := range e.Marketing.Products { + marketingProducts[i] = ToMarketingProductDTO(product) + } + } + marketing = &MarketingBaseDTO{ Id: e.Marketing.Id, SoNumber: e.Marketing.SoNumber, SoDate: e.Marketing.SoDate, + Products: marketingProducts, } } @@ -139,10 +207,16 @@ func ToDeliveryOrdersListDTOWithProducts(e entity.DeliveryOrders, deliveryProduc } } + // Group delivery products by warehouse and delivery date + deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs) + + // Create delivery order DTO with summary + deliveryOrder := createDeliveryOrderDTO(deliveryGroups) + return DeliveryOrdersListDTO{ DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), - Marketing: marketing, - DeliveryProducts: deliveryProductsDTOs, + SalesOrder: marketing, + DeliveryOrder: deliveryOrder, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, @@ -162,3 +236,73 @@ func ToDeliveryOrdersDetailDTO(e entity.DeliveryOrders) DeliveryOrdersDetailDTO DeliveryOrdersListDTO: ToDeliveryOrdersListDTO(e), } } + +// groupDeliveryProducts groups delivery products by warehouse and delivery date +func groupDeliveryProducts(products []MarketingDeliveryProductDTO) []DeliveryGroupDTO { + // Create a map to group products + groupMap := make(map[string]*DeliveryGroupDTO) + + for _, product := range products { + // Create unique key for grouping (warehouse_id + delivery_date) + // Since we're working with DTO, we need to handle the warehouse id differently + warehouseId := uint(0) + warehouseName := "" + + // Extract warehouse info from product (assuming it might be available in future) + // For now, we'll use a simple grouping by delivery date and vehicle number + var key string + if product.DeliveryDate != nil { + key = fmt.Sprintf("%s_%s", product.DeliveryDate.Format("2006-01-02"), product.VehicleNumber) + } else { + key = fmt.Sprintf("no_date_%s", product.VehicleNumber) + } + + // Get or create group + group, exists := groupMap[key] + if !exists { + group = &DeliveryGroupDTO{ + WarehouseId: warehouseId, + WarehouseName: warehouseName, + DeliveryDate: product.DeliveryDate, + VehicleNumber: product.VehicleNumber, + TotalQty: 0, + TotalWeight: 0, + TotalPrice: 0, + } + + groupMap[key] = group + } + + // Update totals + group.TotalQty += product.Qty + group.TotalWeight += product.TotalWeight + group.TotalPrice += product.TotalPrice + } + + // Convert map to slice + var groups []DeliveryGroupDTO + for _, group := range groupMap { + groups = append(groups, *group) + } + + // Sort groups by delivery date + sort.Slice(groups, func(i, j int) bool { + if groups[i].DeliveryDate == nil || groups[j].DeliveryDate == nil { + return false + } + return groups[i].DeliveryDate.Before(*groups[j].DeliveryDate) + }) + + return groups +} + +// createDeliveryOrderDTO creates delivery order DTO +func createDeliveryOrderDTO(deliveryGroups []DeliveryGroupDTO) *DeliveryOrderDTO { + if len(deliveryGroups) == 0 { + return nil + } + + return &DeliveryOrderDTO{ + DeliveryGroups: deliveryGroups, + } +} diff --git a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go index 7a2efa3f..f090ac01 100644 --- a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go +++ b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go @@ -25,7 +25,7 @@ type DeliveryOrdersService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.DeliveryOrdersListDTO, int64, error) GetOne(ctx *fiber.Ctx, id uint) (*dto.DeliveryOrdersListDTO, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) - UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.DeliveryOrders, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*dto.DeliveryOrdersListDTO, error) DeleteOne(ctx *fiber.Ctx, id uint) error Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) } @@ -91,18 +91,11 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([ // Load delivery products untuk setiap marketing result := make([]dto.DeliveryOrdersListDTO, len(marketings)) for i, marketing := range marketings { - // Get marketing delivery products - var allDeliveryProducts []entity.MarketingDeliveryProduct - if err := s.Repository.DB().WithContext(c.Context()). - Preload("MarketingProduct"). - Where("marketing_product_id IN (?)", - s.Repository.DB().WithContext(c.Context()). - Model(&entity.MarketingProduct{}). - Select("id"). - Where("marketing_id = ?", marketing.Id)). - Find(&allDeliveryProducts).Error; err != nil { + // Get marketing delivery products menggunakan repository method + allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), marketing.Id) + if err != nil { s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) - // Continue without products + allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal } // Build response DTO @@ -125,7 +118,8 @@ func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.DeliveryOrder return db.Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). - Preload("Products.ProductWarehouse") + Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Warehouse") }) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") @@ -135,26 +129,52 @@ func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.DeliveryOrder return nil, err } - // Get marketing delivery products - var allDeliveryProducts []entity.MarketingDeliveryProduct - if err := s.Repository.DB().WithContext(c.Context()). - Preload("MarketingProduct"). - Where("marketing_product_id IN (?)", - s.Repository.DB().WithContext(c.Context()). - Model(&entity.MarketingProduct{}). - Select("id"). - Where("marketing_id = ?", marketing.Id)). - Find(&allDeliveryProducts).Error; err != nil { - s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) - // Continue without products + // Get marketing delivery products menggunakan repository method + allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id) + if err != nil { + s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", id, err) + allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal } - // Build response DTO + // Debug: Log jumlah delivery products + s.Log.Infof("Found %d delivery products for marketing %d", len(allDeliveryProducts), id) + + // Jika tidak ada delivery products, buat dummy data untuk testing + if len(allDeliveryProducts) == 0 && len(marketing.Products) > 0 { + s.Log.Infof("Creating dummy delivery products for testing") + for i, product := range marketing.Products { + deliveryDate := marketing.SoDate.AddDate(0, 0, i+7) // 7 hari setelah SO + dummyDeliveryProduct := entity.MarketingDeliveryProduct{ + Id: uint(i + 1), + MarketingProductId: product.Id, + Qty: product.Qty / 2, // Setengah dari qty asli + UnitPrice: product.UnitPrice, + TotalWeight: product.TotalWeight / 2, + AvgWeight: product.AvgWeight, + TotalPrice: (product.Qty / 2) * product.UnitPrice, + DeliveryDate: &deliveryDate, + VehicleNumber: fmt.Sprintf("B%04d%s", (i+1)*1000, "ABC"), + } + allDeliveryProducts = append(allDeliveryProducts, dummyDeliveryProduct) + } + s.Log.Infof("Created %d dummy delivery products", len(allDeliveryProducts)) + } + + // Build response DTO dengan timestamps yang benar deliveryOrderResponse := &entity.DeliveryOrders{ MarketingId: marketing.Id, CreatedUser: &marketing.CreatedUser, Marketing: marketing, DeliveryProducts: allDeliveryProducts, + CreatedAt: marketing.CreatedAt, + UpdatedAt: marketing.UpdatedAt, + } + + // Set delivery_date dari delivery products atau fallback ke marketing date + if len(allDeliveryProducts) > 0 && allDeliveryProducts[0].DeliveryDate != nil { + deliveryOrderResponse.DeliveryDate = *allDeliveryProducts[0].DeliveryDate + } else { + deliveryOrderResponse.DeliveryDate = marketing.SoDate } responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) @@ -255,6 +275,27 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) s.Log.Infof("Updated delivery product %d: qty=%v, unitPrice=%v, totalPrice=%v", deliveryProduct.Id, requestedProduct.Qty, requestedProduct.UnitPrice, requestedProduct.TotalPrice) } + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + actorID := uint(1) // TODO: ambil dari auth context + approvalAction := entity.ApprovalActionCreated + var notes *string + if req.Notes != "" { + notes = &req.Notes + } + if _, err := approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowMarketing, + req.MarketingId, + utils.MarketingDeliveryOrder, + &approvalAction, + actorID, + notes); err != nil { + if !errors.Is(err, gorm.ErrDuplicatedKey) { + s.Log.Errorf("Failed to create delivery order approval: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order approval") + } + } + return nil }) @@ -262,7 +303,6 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, err } - // Fetch marketing dengan delivery products yang sudah di-updated marketing, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, func(db *gorm.DB) *gorm.DB { return db.Preload("CreatedUser").Preload("Products.DeliveryProduct") }) @@ -271,18 +311,11 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated marketing") } - // Get marketing delivery products - var allDeliveryProducts []entity.MarketingDeliveryProduct - if err := s.MarketingDeliveryProductRepo.DB().WithContext(c.Context()). - Preload("MarketingProduct"). - Where("marketing_product_id IN (?)", - s.MarketingProductRepo.DB().WithContext(c.Context()). - Model(&entity.MarketingProduct{}). - Select("id"). - Where("marketing_id = ?", req.MarketingId)). - Find(&allDeliveryProducts).Error; err != nil { + // Get marketing delivery products menggunakan repository method + allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), req.MarketingId) + if err != nil { s.Log.Errorf("Failed to load delivery products: %+v", err) - // Continue tanpa delivery products + allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal } // Build response DTO @@ -298,38 +331,132 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return &responseDTO, nil } -func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.DeliveryOrders, error) { +func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*dto.DeliveryOrdersListDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } - updateBody := make(map[string]any) - - if req.DeliveryDate != "" { - deliveryDate, err := time.Parse("2006-01-02", req.DeliveryDate) - if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid delivery date format") - } - updateBody["delivery_date"] = deliveryDate - } - - if req.Notes != "" { - updateBody["notes"] = req.Notes - } - - if len(updateBody) == 0 { - return s.Repository.GetByID(c.Context(), id, s.withRelations) - } - - if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "DeliveryOrders not found") - } - s.Log.Errorf("Failed to update deliveryOrders: %+v", err) + // Validate bahwa marketing ID yang di-update ada (id parameter adalah marketing_id untuk delivery orders) + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, + ); err != nil { return nil, err } - return s.Repository.GetByID(c.Context(), id, s.withRelations) + // Validate delivery products jika ada + if len(req.DeliveryProducts) > 0 { + for _, requestedProduct := range req.DeliveryProducts { + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "MarketingProduct", ID: &requestedProduct.MarketingProductId, Exists: s.MarketingProductRepo.IdExists}, + ); err != nil { + return nil, err + } + } + } + + err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) + marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction) + + // Update delivery products jika ada dalam request + if len(req.DeliveryProducts) > 0 { + for _, requestedProduct := range req.DeliveryProducts { + // Validate bahwa marketing product ada untuk marketing ini + allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", id)) + } + s.Log.Errorf("Failed to fetch marketing products: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") + } + + var foundMarketingProduct *entity.MarketingProduct + for i := range allMarketingProducts { + if allMarketingProducts[i].Id == requestedProduct.MarketingProductId { + foundMarketingProduct = &allMarketingProducts[i] + break + } + } + if foundMarketingProduct == nil { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId)) + } + + // Get existing delivery product + deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", requestedProduct.MarketingProductId)) + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") + } + + // Parse delivery date + var itemDeliveryDate time.Time + if requestedProduct.DeliveryDate != "" { + parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err)) + } + itemDeliveryDate = parsedDate + } else if deliveryProduct.DeliveryDate != nil { + itemDeliveryDate = *deliveryProduct.DeliveryDate + } else { + itemDeliveryDate = time.Now() + } + + // Update delivery product + deliveryProduct.Qty = requestedProduct.Qty + deliveryProduct.UnitPrice = requestedProduct.UnitPrice + deliveryProduct.AvgWeight = requestedProduct.AvgWeight + deliveryProduct.TotalWeight = requestedProduct.TotalWeight + deliveryProduct.TotalPrice = requestedProduct.TotalPrice + deliveryProduct.DeliveryDate = &itemDeliveryDate + deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber + + if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { + s.Log.Errorf("Failed to update marketing delivery product: %+v", err) + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") + } + + s.Log.Infof("Updated delivery product %d: qty=%v, unitPrice=%v, totalPrice=%v", deliveryProduct.Id, requestedProduct.Qty, requestedProduct.UnitPrice, requestedProduct.TotalPrice) + } + } + + return nil + }) + + if err != nil { + return nil, err + } + + // Fetch updated marketing with delivery products + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser").Preload("Products.DeliveryProduct") + }) + if err != nil { + s.Log.Errorf("Failed to fetch marketing after update: %+v", err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated marketing") + } + + // Get marketing delivery products menggunakan repository method + allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id) + if err != nil { + s.Log.Errorf("Failed to load delivery products: %+v", err) + allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal + } + + // Build response DTO + deliveryOrderResponse := &entity.DeliveryOrders{ + MarketingId: id, + Notes: req.Notes, + CreatedUser: &marketing.CreatedUser, + Marketing: marketing, + DeliveryProducts: allDeliveryProducts, + } + + responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) + return &responseDTO, nil } func (s deliveryOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) { diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 1fb36733..74244496 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -64,8 +64,7 @@ func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). - Preload("Products.ProductWarehouse.Warehouse"). - Preload("Products.DeliveryProduct") + Preload("Products.ProductWarehouse.Warehouse") } func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) { From 903b1143158dddb9ab0a5e06cb79b242450cc235 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Thu, 13 Nov 2025 20:27:49 +0700 Subject: [PATCH 21/29] fix[BE]: fixing null project flock ikandang id on lookup --- .../dto/project_flock_kandang.dto.go | 4 ++- .../dto/projectflock_kandang.dto.go | 25 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go index 5d8e6628..1a076232 100644 --- a/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go +++ b/internal/modules/production/project-flock-kandangs/dto/project_flock_kandang.dto.go @@ -74,7 +74,9 @@ type ProjectFlockKandangDetailDTO struct { func ToProjectFlockKandangBaseDTO(e entity.ProjectFlockKandang) ProjectFlockKandangBaseDTO { return ProjectFlockKandangBaseDTO{ - Id: e.Id, + Id: e.Id, + KandangId: e.KandangId, + ProjectFlockId: e.ProjectFlockId, } } diff --git a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go index 233f655b..24e53d28 100644 --- a/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock_kandang.dto.go @@ -28,12 +28,13 @@ type ProjectFlockWithPivotDTO struct { } type ProjectFlockKandangDTO struct { - Id uint `json:"id"` - - KandangId uint `json:"kandang_id"` - Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"` - ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` - AvailableQuantity float64 `json:"available_quantity"` + Id uint `json:"id"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id"` + ProjectFlockId uint `json:"project_flock_id"` + KandangId uint `json:"kandang_id"` + Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"` + ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` + AvailableQuantity float64 `json:"available_quantity"` } func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { @@ -88,10 +89,12 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD } return ProjectFlockKandangDTO{ - Id: e.Id, - KandangId: e.KandangId, - Kandang: kandang, - ProjectFlock: pf, - AvailableQuantity: 0, + Id: e.Id, + ProjectFlockKandangId: e.Id, + ProjectFlockId: e.ProjectFlockId, + KandangId: e.KandangId, + Kandang: kandang, + ProjectFlock: pf, + AvailableQuantity: 0, } } From 7905bdb0d71dc08fd7e68e131d7e0d940363112e Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 17 Nov 2025 07:16:07 +0700 Subject: [PATCH 22/29] Feat[BE-222]: Completed SO and DO API --- .../entities/marketing_delivery_product.go | 12 +- .../marketing-delivery-products.repository.go | 2 + .../dto/product_warehouse.dto.go | 43 +- .../controllers/delivery-orders.controller.go | 43 +- .../dto/delivery-orders.dto.go | 391 +++++++-------- .../marketing/delivery-orderss/module.go | 15 +- .../marketing/delivery-orderss/route.go | 9 +- .../services/delivery-orders.service.go | 445 +++++++----------- .../validations/delivery-orders.validation.go | 14 +- .../controllers/sales-orders.controller.go | 68 +-- .../sales-orders/dto/sales-orders.dto.go | 247 ++-------- .../modules/marketing/sales-orders/module.go | 13 +- .../repositories/marketings.repository.go | 9 + .../modules/marketing/sales-orders/route.go | 7 +- .../services/sales-orders.service.go | 286 +++++------ .../validations/sales-orders.validation.go | 7 - 16 files changed, 600 insertions(+), 1011 deletions(-) diff --git a/internal/entities/marketing_delivery_product.go b/internal/entities/marketing_delivery_product.go index 85b4591a..253c00b2 100644 --- a/internal/entities/marketing_delivery_product.go +++ b/internal/entities/marketing_delivery_product.go @@ -9,13 +9,13 @@ import ( type MarketingDeliveryProduct struct { Id uint `gorm:"primaryKey;autoIncrement"` MarketingProductId uint `gorm:"uniqueIndex;not null"` - Qty float64 `gorm:"type:numeric(15,3);not null"` - UnitPrice float64 `gorm:"type:numeric(15,3);not null"` - TotalWeight float64 `gorm:"type:numeric(15,3);not null"` - AvgWeight float64 `gorm:"type:numeric(15,3);not null"` - TotalPrice float64 `gorm:"type:numeric(15,3);not null"` + Qty float64 `gorm:"type:numeric(15,3)"` + UnitPrice float64 `gorm:"type:numeric(15,3)"` + TotalWeight float64 `gorm:"type:numeric(15,3)"` + AvgWeight float64 `gorm:"type:numeric(15,3)"` + TotalPrice float64 `gorm:"type:numeric(15,3)"` DeliveryDate *time.Time `gorm:"type:timestamptz"` - VehicleNumber string `gorm:"type:varchar(50)"` + VehicleNumber string `gorm:"type:varchar(50)"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` diff --git a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go index 96590990..512a5786 100644 --- a/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go +++ b/internal/modules/inventory/marketing-delivery-products/repositories/marketing-delivery-products.repository.go @@ -36,10 +36,12 @@ func (r *MarketingDeliveryProductRepositoryImpl) GetByMarketingId(ctx context.Co var deliveryProducts []entity.MarketingDeliveryProduct // Raw query untuk mengambil delivery products berdasarkan marketing ID dengan preload MarketingProduct + // Filter: hanya ambil yang sudah memiliki delivery_date (delivery date tidak null) if err := r.DB().WithContext(ctx). Preload("MarketingProduct"). Joins("INNER JOIN marketing_products mp ON marketing_delivery_products.marketing_product_id = mp.id"). Where("mp.marketing_id = ?", marketingId). + Where("marketing_delivery_products.delivery_date IS NOT NULL"). Order("marketing_delivery_products.id ASC"). Find(&deliveryProducts).Error; err != nil { return nil, err diff --git a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go index 8c9f3846..c6ac5931 100644 --- a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go +++ b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go @@ -4,6 +4,7 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" ) // === DTO Structs === @@ -15,13 +16,19 @@ type ProductWarehouseBaseDTO struct { Quantity float64 `json:"quantity"` } +type ProductWarehousNestedDTO struct { + Id uint `json:"id"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` +} + type ProductWarehouseListDTO struct { ProductWarehouseBaseDTO - Product *ProductBaseDTO `json:"product,omitempty"` - Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` - CreatedUser *UserBaseDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Product *productDTO.ProductBaseDTO `json:"product,omitempty"` + Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` + CreatedUser *UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type UserBaseDTO struct { @@ -75,6 +82,19 @@ func ToProductWarehouseBaseDTO(e entity.ProductWarehouse) ProductWarehouseBaseDT } } +func ToProductWarehouseNestedDTO(e entity.ProductWarehouse) ProductWarehousNestedDTO { + product := productDTO.ToProductBaseDTO(e.Product) + + return ProductWarehousNestedDTO{ + Id: e.Id, + Product: &product, + Warehouse: &WarehouseBaseDTO{ + Id: e.Warehouse.Id, + Name: e.Warehouse.Name, + }, + } +} + func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDTO { dto := ProductWarehouseListDTO{ ProductWarehouseBaseDTO: ToProductWarehouseBaseDTO(e), @@ -84,18 +104,7 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT // Map Product relation jika ada if e.Product.Id != 0 { - product := ProductBaseDTO{ - Id: e.Product.Id, - Name: e.Product.Name, - } - if e.Product.Sku != nil { - product.Sku = *e.Product.Sku - } - if len(e.Product.Flags) > 0 { - for _, f := range e.Product.Flags { - product.Flags = append(product.Flags, f.Name) - } - } + product := productDTO.ToProductBaseDTO(e.Product) dto.Product = &product } diff --git a/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go index 8ca51dc5..292381d0 100644 --- a/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go +++ b/internal/modules/marketing/delivery-orderss/controllers/delivery-orders.controller.go @@ -39,7 +39,7 @@ func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error { } return c.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.DeliveryOrdersListDTO]{ + JSON(response.SuccessWithPaginate[dto.MarketingListDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Get all deliveryOrderss successfully", @@ -122,44 +122,3 @@ func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error { Data: result, }) } - -func (u *DeliveryOrdersController) DeleteOne(c *fiber.Ctx) error { - param := c.Params("id") - - id, err := strconv.Atoi(param) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") - } - - if err := u.DeliveryOrdersService.DeleteOne(c, uint(id)); err != nil { - return err - } - - return c.Status(fiber.StatusOK). - JSON(response.Common{ - Code: fiber.StatusOK, - Status: "success", - Message: "Delete deliveryOrders successfully", - }) -} - -func (u *DeliveryOrdersController) Approval(c *fiber.Ctx) error { - req := new(validation.Approve) - - if err := c.BodyParser(req); err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") - } - - results, err := u.DeliveryOrdersService.Approval(c, req) - if err != nil { - return err - } - - return c.Status(fiber.StatusOK). - JSON(response.Success{ - Code: fiber.StatusOK, - Status: "success", - Message: "Submit delivery order approval successfully", - Data: results, - }) -} diff --git a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go index 6008269d..be662412 100644 --- a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go +++ b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go @@ -7,83 +7,100 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + productwarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto" + customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) -// === DTO Structs === +type MarketingBaseDTO struct { + Id uint `json:"id"` + SoNumber string `json:"so_number"` + SoDate time.Time `json:"so_date"` + Notes string `json:"notes,omitempty"` +} +type MarketingListDTO struct { + MarketingBaseDTO + Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` + SalesPerson *userDTO.UserBaseDTO `json:"sales_person,omitempty"` + SoDocs string `json:"so_docs,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` +} + +type MarketingDetailDTO struct { + MarketingBaseDTO + Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` + SalesPerson *userDTO.UserBaseDTO `json:"sales_person,omitempty"` + SoDocs string `json:"so_docs,omitempty"` + SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"` + DeliveryOrder []DeliveryGroupDTO `json:"delivery_order,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + LatestApproval *approvalDTO.ApprovalBaseDTO `json:"latest_approval,omitempty"` +} type MarketingDeliveryProductDTO struct { - Id uint `json:"id"` - MarketingProductId uint `json:"marketing_product_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalWeight float64 `json:"total_weight"` - AvgWeight float64 `json:"avg_weight"` - TotalPrice float64 `json:"total_price"` - DeliveryDate *time.Time `json:"delivery_date"` - VehicleNumber string `json:"vehicle_number"` + Id uint `json:"id"` + MarketingProductId uint `json:"marketing_product_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalWeight float64 `json:"total_weight"` + AvgWeight float64 `json:"avg_weight"` + TotalPrice float64 `json:"total_price"` + DeliveryDate *time.Time `json:"delivery_date"` + VehicleNumber string `json:"vehicle_number"` + ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` +} + +type DeliveryItemDTO struct { + ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + TotalWeight float64 `json:"total_weight"` + AvgWeight float64 `json:"avg_weight"` + TotalPrice float64 `json:"total_price"` + VehicleNumber string `json:"vehicle_number"` } -// DTO untuk grouping delivery products berdasarkan warehouse dan tanggal type DeliveryGroupDTO struct { - WarehouseId uint `json:"warehouse_id"` - WarehouseName string `json:"warehouse_name"` - DeliveryDate *time.Time `json:"delivery_date"` - VehicleNumber string `json:"vehicle_number"` - TotalQty float64 `json:"total_qty"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` -} - -// DTO untuk Delivery Order (DO) - berisi data delivery yang sudah digroup -type DeliveryOrderDTO struct { - DeliveryGroups []DeliveryGroupDTO `json:"delivery_groups"` -} - -type DeliveryOrdersBaseDTO struct { - Id uint `json:"id,omitempty"` - DeliveryNumber *string `json:"delivery_number,omitempty"` - DeliveryDate *time.Time `json:"delivery_date,omitempty"` - MarketingId uint `json:"marketing_id"` - Notes string `json:"notes,omitempty"` + DoNumber string `json:"do_number"` + DeliveryDate *time.Time `json:"delivery_date"` + Warehouse *productwarehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` + Deliveries []DeliveryItemDTO `json:"deliveries"` } type MarketingProductDTO struct { - Id uint `json:"id"` - MarketingId uint `json:"marketing_id"` - ProductWarehouseId uint `json:"product_warehouse_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - AvgWeight float64 `json:"avg_weight"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` - // Add product relation if needed + Id uint `json:"id"` + MarketingId uint `json:"marketing_id"` + ProductWarehouseId uint `json:"product_warehouse_id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + ProductWarehouse *productwarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` + VehicleNumber string `json:"vehicle_number,omitempty"` } -type MarketingBaseDTO struct { - Id uint `json:"id"` - SoNumber string `json:"so_number"` - SoDate time.Time `json:"so_date"` - Products []MarketingProductDTO `json:"products,omitempty"` +func ToMarketingBaseDTO(marketing *entity.Marketing) MarketingBaseDTO { + return MarketingBaseDTO{ + Id: marketing.Id, + SoNumber: marketing.SoNumber, + SoDate: marketing.SoDate, + Notes: marketing.Notes, + } } -type DeliveryOrdersListDTO struct { - DeliveryOrdersBaseDTO - SalesOrder *MarketingBaseDTO `json:"sales_order,omitempty"` // SO - Sales Order data - DeliveryOrder *DeliveryOrderDTO `json:"delivery_order,omitempty"` // DO - Delivery Order data (grouped) - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Approval *approvalDTO.ApprovalBaseDTO `json:"approval,omitempty"` -} - -type DeliveryOrdersDetailDTO struct { - DeliveryOrdersListDTO -} - -// === Mapper Functions === - func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { + var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO + if e.ProductWarehouse.Id != 0 { + mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse) + productWarehouse = &mapped + } + return MarketingProductDTO{ Id: e.Id, MarketingId: e.MarketingId, @@ -93,6 +110,8 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { AvgWeight: e.AvgWeight, TotalWeight: e.TotalWeight, TotalPrice: e.TotalPrice, + ProductWarehouse: productWarehouse, + VehicleNumber: getVehicleNumber(e), } } @@ -110,92 +129,67 @@ func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingD } } -func ToDeliveryOrdersBaseDTO(e entity.DeliveryOrders) DeliveryOrdersBaseDTO { - var deliveryNumber *string - if e.DeliveryNumber != "" { - deliveryNumber = &e.DeliveryNumber - } - - return DeliveryOrdersBaseDTO{ - Id: e.Id, - DeliveryNumber: deliveryNumber, - DeliveryDate: &e.DeliveryDate, - MarketingId: e.MarketingId, - Notes: e.Notes, - } -} - -func ToDeliveryOrdersListDTO(e entity.DeliveryOrders) DeliveryOrdersListDTO { +func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.MarketingDeliveryProduct) MarketingListDTO { var createdUser *userDTO.UserBaseDTO - if e.CreatedUser != nil && e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + if marketing.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(marketing.CreatedUser) createdUser = &mapped } - var marketing *MarketingBaseDTO - if e.Marketing != nil && e.Marketing.Id != 0 { - var marketingProducts []MarketingProductDTO - if len(e.Marketing.Products) > 0 { - marketingProducts = make([]MarketingProductDTO, len(e.Marketing.Products)) - for i, product := range e.Marketing.Products { - marketingProducts[i] = ToMarketingProductDTO(product) - } - } - - marketing = &MarketingBaseDTO{ - Id: e.Marketing.Id, - SoNumber: e.Marketing.SoNumber, - SoDate: e.Marketing.SoDate, - Products: marketingProducts, - } + var customer *customerDTO.CustomerBaseDTO + if marketing.Customer.Id != 0 { + mapped := customerDTO.ToCustomerBaseDTO(marketing.Customer) + customer = &mapped } - var deliveryProductsDTOs []MarketingDeliveryProductDTO - if len(e.DeliveryProducts) > 0 { - deliveryProductsDTOs = make([]MarketingDeliveryProductDTO, len(e.DeliveryProducts)) - for i, dp := range e.DeliveryProducts { - deliveryProductsDTOs[i] = ToMarketingDeliveryProductDTO(dp) - } + var salesPerson *userDTO.UserBaseDTO + if marketing.SalesPerson.Id != 0 { + mapped := userDTO.ToUserBaseDTO(marketing.SalesPerson) + salesPerson = &mapped } - // Group delivery products by warehouse and delivery date - deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs) + var latestApproval *approvalDTO.ApprovalBaseDTO + if marketing.LatestApproval != nil { + mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) + latestApproval = &mapped + } - // Create delivery order DTO with summary - deliveryOrder := createDeliveryOrderDTO(deliveryGroups) - - return DeliveryOrdersListDTO{ - DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), - SalesOrder: marketing, - DeliveryOrder: deliveryOrder, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + return MarketingListDTO{ + MarketingBaseDTO: ToMarketingBaseDTO(marketing), + Customer: customer, + SalesPerson: salesPerson, + SoDocs: marketing.SoDocs, + CreatedUser: createdUser, + CreatedAt: marketing.CreatedAt, + UpdatedAt: marketing.UpdatedAt, + LatestApproval: latestApproval, } } -func ToDeliveryOrdersListDTOWithProducts(e entity.DeliveryOrders, deliveryProducts []entity.MarketingDeliveryProduct) DeliveryOrdersListDTO { +func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity.MarketingDeliveryProduct) MarketingDetailDTO { var createdUser *userDTO.UserBaseDTO - if e.CreatedUser != nil && e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) + if marketing.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(marketing.CreatedUser) createdUser = &mapped } - var marketing *MarketingBaseDTO - if e.Marketing != nil && e.Marketing.Id != 0 { - var marketingProducts []MarketingProductDTO - if len(e.Marketing.Products) > 0 { - marketingProducts = make([]MarketingProductDTO, len(e.Marketing.Products)) - for i, product := range e.Marketing.Products { - marketingProducts[i] = ToMarketingProductDTO(product) - } - } + var customer *customerDTO.CustomerBaseDTO + if marketing.Customer.Id != 0 { + mapped := customerDTO.ToCustomerBaseDTO(marketing.Customer) + customer = &mapped + } - marketing = &MarketingBaseDTO{ - Id: e.Marketing.Id, - SoNumber: e.Marketing.SoNumber, - SoDate: e.Marketing.SoDate, - Products: marketingProducts, + var salesPerson *userDTO.UserBaseDTO + if marketing.SalesPerson.Id != 0 { + mapped := userDTO.ToUserBaseDTO(marketing.SalesPerson) + salesPerson = &mapped + } + + var salesOrderProducts []MarketingProductDTO + if len(marketing.Products) > 0 { + salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products)) + for i, product := range marketing.Products { + salesOrderProducts[i] = ToMarketingProductDTO(product) } } @@ -205,87 +199,108 @@ func ToDeliveryOrdersListDTOWithProducts(e entity.DeliveryOrders, deliveryProduc for i, dp := range deliveryProducts { deliveryProductsDTOs[i] = ToMarketingDeliveryProductDTO(dp) } + deliveryProductsDTOs = enrichDeliveryProductDTOsWithWarehouse(deliveryProductsDTOs, marketing) } - // Group delivery products by warehouse and delivery date - deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs) + deliveryGroups := groupDeliveryProducts(deliveryProductsDTOs, marketing.SoNumber) - // Create delivery order DTO with summary - deliveryOrder := createDeliveryOrderDTO(deliveryGroups) + var latestApproval *approvalDTO.ApprovalBaseDTO + if marketing.LatestApproval != nil { + mapped := approvalDTO.ToApprovalDTO(*marketing.LatestApproval) + latestApproval = &mapped + } - return DeliveryOrdersListDTO{ - DeliveryOrdersBaseDTO: ToDeliveryOrdersBaseDTO(e), - SalesOrder: marketing, - DeliveryOrder: deliveryOrder, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + return MarketingDetailDTO{ + MarketingBaseDTO: ToMarketingBaseDTO(marketing), + SoDocs: marketing.SoDocs, + Customer: customer, + SalesPerson: salesPerson, + SalesOrder: salesOrderProducts, + DeliveryOrder: deliveryGroups, + CreatedUser: createdUser, + CreatedAt: marketing.CreatedAt, + UpdatedAt: marketing.UpdatedAt, + LatestApproval: latestApproval, } } -func ToDeliveryOrdersListDTOs(e []entity.DeliveryOrders) []DeliveryOrdersListDTO { - result := make([]DeliveryOrdersListDTO, len(e)) - for i, r := range e { - result[i] = ToDeliveryOrdersListDTO(r) +func ToMarketingListDTOs(marketings []entity.Marketing) []MarketingListDTO { + result := make([]MarketingListDTO, len(marketings)) + for i, m := range marketings { + result[i] = ToMarketingListDTO(&m, []entity.MarketingDeliveryProduct{}) } return result } -func ToDeliveryOrdersDetailDTO(e entity.DeliveryOrders) DeliveryOrdersDetailDTO { - return DeliveryOrdersDetailDTO{ - DeliveryOrdersListDTO: ToDeliveryOrdersListDTO(e), +func enrichDeliveryProductDTOsWithWarehouse(deliveryProductDTOs []MarketingDeliveryProductDTO, marketing *entity.Marketing) []MarketingDeliveryProductDTO { + if len(deliveryProductDTOs) == 0 || marketing == nil || len(marketing.Products) == 0 { + return deliveryProductDTOs } + + productMap := make(map[uint]*entity.MarketingProduct) + for i := range marketing.Products { + productMap[marketing.Products[i].Id] = &marketing.Products[i] + } + + for i := range deliveryProductDTOs { + if product, exists := productMap[deliveryProductDTOs[i].MarketingProductId]; exists { + if product.ProductWarehouse.Id != 0 { + mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(product.ProductWarehouse) + deliveryProductDTOs[i].ProductWarehouse = &mapped + } + } + } + + return deliveryProductDTOs } -// groupDeliveryProducts groups delivery products by warehouse and delivery date -func groupDeliveryProducts(products []MarketingDeliveryProductDTO) []DeliveryGroupDTO { - // Create a map to group products +func groupDeliveryProducts(products []MarketingDeliveryProductDTO, soNumber string) []DeliveryGroupDTO { groupMap := make(map[string]*DeliveryGroupDTO) for _, product := range products { - // Create unique key for grouping (warehouse_id + delivery_date) - // Since we're working with DTO, we need to handle the warehouse id differently - warehouseId := uint(0) - warehouseName := "" - - // Extract warehouse info from product (assuming it might be available in future) - // For now, we'll use a simple grouping by delivery date and vehicle number - var key string - if product.DeliveryDate != nil { - key = fmt.Sprintf("%s_%s", product.DeliveryDate.Format("2006-01-02"), product.VehicleNumber) - } else { - key = fmt.Sprintf("no_date_%s", product.VehicleNumber) + if product.DeliveryDate == nil { + continue } - // Get or create group + var warehouseId uint + var warehouseName string + if product.ProductWarehouse != nil { + warehouseId = product.ProductWarehouse.Warehouse.Id + warehouseName = product.ProductWarehouse.Warehouse.Name + } + + key := fmt.Sprintf("%d_%s", warehouseId, product.DeliveryDate.Format("2006-01-02")) + group, exists := groupMap[key] if !exists { group = &DeliveryGroupDTO{ - WarehouseId: warehouseId, - WarehouseName: warehouseName, - DeliveryDate: product.DeliveryDate, - VehicleNumber: product.VehicleNumber, - TotalQty: 0, - TotalWeight: 0, - TotalPrice: 0, + DeliveryDate: product.DeliveryDate, + Warehouse: &productwarehouseDTO.WarehouseBaseDTO{ + Id: warehouseId, + Name: warehouseName, + }, + Deliveries: []DeliveryItemDTO{}, } - groupMap[key] = group } - // Update totals - group.TotalQty += product.Qty - group.TotalWeight += product.TotalWeight - group.TotalPrice += product.TotalPrice + deliveryItem := DeliveryItemDTO{ + ProductWarehouse: product.ProductWarehouse, + Qty: product.Qty, + UnitPrice: product.UnitPrice, + TotalWeight: product.TotalWeight, + AvgWeight: product.AvgWeight, + TotalPrice: product.TotalPrice, + VehicleNumber: product.VehicleNumber, + } + group.Deliveries = append(group.Deliveries, deliveryItem) } - // Convert map to slice var groups []DeliveryGroupDTO for _, group := range groupMap { groups = append(groups, *group) } - // Sort groups by delivery date sort.Slice(groups, func(i, j int) bool { if groups[i].DeliveryDate == nil || groups[j].DeliveryDate == nil { return false @@ -293,16 +308,20 @@ func groupDeliveryProducts(products []MarketingDeliveryProductDTO) []DeliveryGro return groups[i].DeliveryDate.Before(*groups[j].DeliveryDate) }) + for i := range groups { + if groups[i].DeliveryDate != nil { + dateStr := groups[i].DeliveryDate.Format("20060102") + groups[i].DoNumber = fmt.Sprintf("%s-%s-%d", soNumber, dateStr, groups[i].Warehouse.Id) + } + } + return groups } -// createDeliveryOrderDTO creates delivery order DTO -func createDeliveryOrderDTO(deliveryGroups []DeliveryGroupDTO) *DeliveryOrderDTO { - if len(deliveryGroups) == 0 { - return nil - } - - return &DeliveryOrderDTO{ - DeliveryGroups: deliveryGroups, +// getVehicleNumber mengambil vehicle number dari DeliveryProduct jika ada +func getVehicleNumber(e entity.MarketingProduct) string { + if e.DeliveryProduct != nil && e.DeliveryProduct.VehicleNumber != "" { + return e.DeliveryProduct.VehicleNumber } + return "" } diff --git a/internal/modules/marketing/delivery-orderss/module.go b/internal/modules/marketing/delivery-orderss/module.go index 7fcc8ccc..99bd8396 100644 --- a/internal/modules/marketing/delivery-orderss/module.go +++ b/internal/modules/marketing/delivery-orderss/module.go @@ -1,6 +1,8 @@ package delivery_orderss import ( + "fmt" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "gorm.io/gorm" @@ -8,26 +10,29 @@ import ( commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" rMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" - rDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/repositories" sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services" rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" - rMarketingProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) type DeliveryOrdersModule struct{} func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { - deliveryOrdersRepo := rDeliveryOrders.NewDeliveryOrdersRepository(db) marketingRepo := rMarketing.NewMarketingRepository(db) - marketingProductRepo := rMarketingProduct.NewMarketingProductRepository(db) + marketingProductRepo := rMarketing.NewMarketingProductRepository(db) marketingDeliveryProductRepo := rMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(db) userRepo := rUser.NewUserRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db) approvalSvc := commonSvc.NewApprovalService(approvalRepo) - deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(deliveryOrdersRepo, marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate) + // Register workflow steps for MARKETINGS approval + if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err)) + } + + deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate) userService := sUser.NewUserService(userRepo, validate) DeliveryOrdersRoutes(router, userService, deliveryOrdersService) diff --git a/internal/modules/marketing/delivery-orderss/route.go b/internal/modules/marketing/delivery-orderss/route.go index 8d58b70e..09e48f29 100644 --- a/internal/modules/marketing/delivery-orderss/route.go +++ b/internal/modules/marketing/delivery-orderss/route.go @@ -12,6 +12,10 @@ import ( func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.DeliveryOrdersService) { ctrl := controller.NewDeliveryOrdersController(s) + v1.Get("/", ctrl.GetAll) + v1.Get("/:id", ctrl.GetOne) + + // Sisanya di group /delivery-orders route := v1.Group("/delivery-orders") // route.Get("/", m.Auth(u), ctrl.GetAll) @@ -20,10 +24,7 @@ func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders. // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Get("/", ctrl.GetAll) route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) - route.Post("/approvals", ctrl.Approval) + } diff --git a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go index f090ac01..45b11d6d 100644 --- a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go +++ b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go @@ -1,6 +1,7 @@ package service import ( + "context" "errors" "fmt" "time" @@ -9,8 +10,8 @@ import ( commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" marketingDeliveryProductRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" + productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto" - repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations" marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -22,18 +23,15 @@ import ( ) type DeliveryOrdersService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.DeliveryOrdersListDTO, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*dto.DeliveryOrdersListDTO, error) - CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) - UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*dto.DeliveryOrdersListDTO, error) - DeleteOne(ctx *fiber.Ctx, id uint) error - Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error) } type deliveryOrdersService struct { Log *logrus.Logger Validate *validator.Validate - Repository repository.DeliveryOrdersRepository MarketingRepo marketingRepo.MarketingRepository MarketingProductRepo marketingRepo.MarketingProductRepository MarketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository @@ -41,7 +39,6 @@ type deliveryOrdersService struct { } func NewDeliveryOrdersService( - repo repository.DeliveryOrdersRepository, marketingRepo marketingRepo.MarketingRepository, marketingProductRepo marketingRepo.MarketingProductRepository, marketingDeliveryProductRepo marketingDeliveryProductRepo.MarketingDeliveryProductRepository, @@ -51,7 +48,6 @@ func NewDeliveryOrdersService( return &deliveryOrdersService{ Log: utils.Log, Validate: validate, - Repository: repo, MarketingRepo: marketingRepo, MarketingProductRepo: marketingProductRepo, MarketingDeliveryProductRepo: marketingDeliveryProductRepo, @@ -60,20 +56,45 @@ func NewDeliveryOrdersService( } func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser"). - Preload("Marketing") + return db. + Preload("CreatedUser"). + Preload("Customer"). + Preload("SalesPerson"). + Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Warehouse"). + Preload("Products.DeliveryProduct") } -func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.DeliveryOrdersListDTO, int64, error) { +func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketingId uint) (*dto.MarketingDetailDTO, error) { + marketing, err := s.MarketingRepo.GetByID(c.Context(), marketingId, s.withRelations) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing") + } + + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketingId, nil) + if err != nil { + } + marketing.LatestApproval = latestApproval + + allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), marketingId) + if err != nil { + allDeliveryProducts = []entity.MarketingDeliveryProduct{} + } + + responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts) + return &responseDTO, nil +} + +func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } offset := (params.Page - 1) * params.Limit - // Fetch dari Marketing, bukan DeliveryOrders marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - db = db.Preload("CreatedUser"). + db = db. + Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse") @@ -87,101 +108,58 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([ s.Log.Errorf("Failed to get marketings: %+v", err) return nil, 0, err } - - // Load delivery products untuk setiap marketing - result := make([]dto.DeliveryOrdersListDTO, len(marketings)) - for i, marketing := range marketings { - // Get marketing delivery products menggunakan repository method - allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), marketing.Id) + for i := range marketings { + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketings[i].Id, nil) if err != nil { - s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", marketing.Id, err) - allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal + s.Log.Warnf("Failed to load approval for marketing %d: %+v", marketings[i].Id, err) } + marketings[i].LatestApproval = latestApproval + } - // Build response DTO - deliveryOrderResponse := &entity.DeliveryOrders{ - MarketingId: marketing.Id, - CreatedUser: &marketing.CreatedUser, - Marketing: &marketing, - DeliveryProducts: allDeliveryProducts, - } - - result[i] = dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) + result := make([]dto.MarketingListDTO, len(marketings)) + for i, marketing := range marketings { + result[i] = dto.ToMarketingListDTO(&marketing, []entity.MarketingDeliveryProduct{}) } return result, total, nil } -func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.DeliveryOrdersListDTO, error) { - // Fetch Marketing by ID, bukan DeliveryOrders - marketing, err := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser"). - Preload("Customer"). - Preload("SalesPerson"). - Preload("Products.ProductWarehouse.Product"). - Preload("Products.ProductWarehouse.Warehouse") - }) +func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) { + + marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found") } if err != nil { - s.Log.Errorf("Failed get marketing by id: %+v", err) return nil, err } - // Get marketing delivery products menggunakan repository method allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id) if err != nil { - s.Log.Errorf("Failed to load delivery products for marketing %d: %+v", id, err) - allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal + allDeliveryProducts = []entity.MarketingDeliveryProduct{} } - // Debug: Log jumlah delivery products - s.Log.Infof("Found %d delivery products for marketing %d", len(allDeliveryProducts), id) + if s.ApprovalSvc != nil { + approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketing.Id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { - // Jika tidak ada delivery products, buat dummy data untuk testing - if len(allDeliveryProducts) == 0 && len(marketing.Products) > 0 { - s.Log.Infof("Creating dummy delivery products for testing") - for i, product := range marketing.Products { - deliveryDate := marketing.SoDate.AddDate(0, 0, i+7) // 7 hari setelah SO - dummyDeliveryProduct := entity.MarketingDeliveryProduct{ - Id: uint(i + 1), - MarketingProductId: product.Id, - Qty: product.Qty / 2, // Setengah dari qty asli - UnitPrice: product.UnitPrice, - TotalWeight: product.TotalWeight / 2, - AvgWeight: product.AvgWeight, - TotalPrice: (product.Qty / 2) * product.UnitPrice, - DeliveryDate: &deliveryDate, - VehicleNumber: fmt.Sprintf("B%04d%s", (i+1)*1000, "ABC"), + } else if len(approvals) > 0 { + if marketing.LatestApproval == nil { + latest := approvals[len(approvals)-1] + marketing.LatestApproval = &latest } - allDeliveryProducts = append(allDeliveryProducts, dummyDeliveryProduct) + } else { + marketing.LatestApproval = nil } - s.Log.Infof("Created %d dummy delivery products", len(allDeliveryProducts)) } - // Build response DTO dengan timestamps yang benar - deliveryOrderResponse := &entity.DeliveryOrders{ - MarketingId: marketing.Id, - CreatedUser: &marketing.CreatedUser, - Marketing: marketing, - DeliveryProducts: allDeliveryProducts, - CreatedAt: marketing.CreatedAt, - UpdatedAt: marketing.UpdatedAt, - } - - // Set delivery_date dari delivery products atau fallback ke marketing date - if len(allDeliveryProducts) > 0 && allDeliveryProducts[0].DeliveryDate != nil { - deliveryOrderResponse.DeliveryDate = *allDeliveryProducts[0].DeliveryDate - } else { - deliveryOrderResponse.DeliveryDate = marketing.SoDate - } - - responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) + responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts) return &responseDTO, nil } -func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.DeliveryOrdersListDTO, error) { +func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } @@ -192,14 +170,8 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return nil, err } - var relationChecks []commonSvc.RelationCheck - for _, requestedProduct := range req.DeliveryProducts { - relationChecks = append(relationChecks, commonSvc.RelationCheck{ - Name: "MarketingProduct", ID: &requestedProduct.MarketingProductId, Exists: s.MarketingProductRepo.IdExists, - }) - } - approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) + latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil) if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") @@ -210,25 +182,28 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) if latestApproval.StepNumber < uint16(utils.MarketingStepSalesOrder) { return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing must be approved to Sales Order step before creating delivery order") } + if latestApproval.StepNumber >= uint16(utils.MarketingDeliveryOrder) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery order already exists for this marketing") + } if latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved { - return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing is not approved for delivery") + return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing is not approved - current status: %v", *latestApproval.Action)) } - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction) + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + + allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", req.MarketingId)) + } + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") + } for _, requestedProduct := range req.DeliveryProducts { - allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", req.MarketingId)) - } - s.Log.Errorf("Failed to fetch marketing products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") - } - var foundMarketingProduct *entity.MarketingProduct for i := range allMarketingProducts { if allMarketingProducts[i].Id == requestedProduct.MarketingProductId { @@ -248,15 +223,13 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") } - var itemDeliveryDate time.Time + var itemDeliveryDate *time.Time if requestedProduct.DeliveryDate != "" { parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err)) } - itemDeliveryDate = parsedDate - } else { - itemDeliveryDate = time.Now() + itemDeliveryDate = &parsedDate } deliveryProduct.Qty = requestedProduct.Qty @@ -264,24 +237,22 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.TotalWeight = requestedProduct.TotalWeight deliveryProduct.TotalPrice = requestedProduct.TotalPrice - deliveryProduct.DeliveryDate = &itemDeliveryDate + deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber + if requestedProduct.Qty > 0 { + if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, requestedProduct.Qty); err != nil { + return err + } + } if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { - s.Log.Errorf("Failed to update marketing delivery product: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") } - s.Log.Infof("Updated delivery product %d: qty=%v, unitPrice=%v, totalPrice=%v", deliveryProduct.Id, requestedProduct.Qty, requestedProduct.UnitPrice, requestedProduct.TotalPrice) } - approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) actorID := uint(1) // TODO: ambil dari auth context - approvalAction := entity.ApprovalActionCreated - var notes *string - if req.Notes != "" { - notes = &req.Notes - } + approvalAction := entity.ApprovalActionApproved if _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowMarketing, @@ -289,9 +260,8 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) utils.MarketingDeliveryOrder, &approvalAction, actorID, - notes); err != nil { + nil); err != nil { if !errors.Is(err, gorm.ErrDuplicatedKey) { - s.Log.Errorf("Failed to create delivery order approval: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order approval") } } @@ -300,76 +270,38 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) }) if err != nil { - return nil, err + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order") } - marketing, err := s.MarketingRepo.GetByID(c.Context(), req.MarketingId, func(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser").Preload("Products.DeliveryProduct") - }) - if err != nil { - s.Log.Errorf("Failed to fetch marketing after update: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated marketing") - } - - // Get marketing delivery products menggunakan repository method - allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), req.MarketingId) - if err != nil { - s.Log.Errorf("Failed to load delivery products: %+v", err) - allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal - } - - // Build response DTO - deliveryOrderResponse := &entity.DeliveryOrders{ - MarketingId: req.MarketingId, - Notes: req.Notes, - CreatedUser: &marketing.CreatedUser, - Marketing: marketing, - DeliveryProducts: allDeliveryProducts, - } - - responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) - return &responseDTO, nil + return s.getMarketingWithDeliveries(c, req.MarketingId) } -func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*dto.DeliveryOrdersListDTO, error) { +func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error) { if err := s.Validate.Struct(req); err != nil { return nil, err } - // Validate bahwa marketing ID yang di-update ada (id parameter adalah marketing_id untuk delivery orders) if err := commonSvc.EnsureRelations(c.Context(), commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, ); err != nil { return nil, err } - // Validate delivery products jika ada - if len(req.DeliveryProducts) > 0 { - for _, requestedProduct := range req.DeliveryProducts { - if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "MarketingProduct", ID: &requestedProduct.MarketingProductId, Exists: s.MarketingProductRepo.IdExists}, - ); err != nil { - return nil, err - } - } - } + err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { - err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepositoryTx := marketingDeliveryProductRepo.NewMarketingDeliveryProductRepository(dbTransaction) - // Update delivery products jika ada dalam request + allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") + } + if len(req.DeliveryProducts) > 0 { for _, requestedProduct := range req.DeliveryProducts { - // Validate bahwa marketing product ada untuk marketing ini - allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", id)) - } - s.Log.Errorf("Failed to fetch marketing products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products") - } var foundMarketingProduct *entity.MarketingProduct for i := range allMarketingProducts { @@ -382,7 +314,6 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId)) } - // Get existing delivery product deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id) if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { @@ -391,157 +322,101 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, i return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product") } - // Parse delivery date - var itemDeliveryDate time.Time + var itemDeliveryDate *time.Time if requestedProduct.DeliveryDate != "" { parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate) if err != nil { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err)) } - itemDeliveryDate = parsedDate + itemDeliveryDate = &parsedDate } else if deliveryProduct.DeliveryDate != nil { - itemDeliveryDate = *deliveryProduct.DeliveryDate - } else { - itemDeliveryDate = time.Now() + itemDeliveryDate = deliveryProduct.DeliveryDate } - // Update delivery product + oldQty := deliveryProduct.Qty deliveryProduct.Qty = requestedProduct.Qty deliveryProduct.UnitPrice = requestedProduct.UnitPrice deliveryProduct.AvgWeight = requestedProduct.AvgWeight deliveryProduct.TotalWeight = requestedProduct.TotalWeight deliveryProduct.TotalPrice = requestedProduct.TotalPrice - deliveryProduct.DeliveryDate = &itemDeliveryDate + deliveryProduct.DeliveryDate = itemDeliveryDate deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber - if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { - s.Log.Errorf("Failed to update marketing delivery product: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") + qtyChange := requestedProduct.Qty - oldQty + if qtyChange > 0 { + if err := s.validateAndReduceProductWarehouse(c.Context(), dbTransaction, foundMarketingProduct, qtyChange); err != nil { + return err + } + } else if qtyChange < 0 { + if err := s.restoreProductWarehouseStock(c.Context(), dbTransaction, foundMarketingProduct, -qtyChange); err != nil { + return err + } } - s.Log.Infof("Updated delivery product %d: qty=%v, unitPrice=%v, totalPrice=%v", deliveryProduct.Id, requestedProduct.Qty, requestedProduct.UnitPrice, requestedProduct.TotalPrice) + if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product") + } } } return nil }) - if err != nil { - return nil, err - } - - // Fetch updated marketing with delivery products - marketing, err := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser").Preload("Products.DeliveryProduct") - }) - if err != nil { - s.Log.Errorf("Failed to fetch marketing after update: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch updated marketing") - } - - // Get marketing delivery products menggunakan repository method - allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id) - if err != nil { - s.Log.Errorf("Failed to load delivery products: %+v", err) - allDeliveryProducts = []entity.MarketingDeliveryProduct{} // Set empty slice jika gagal - } - - // Build response DTO - deliveryOrderResponse := &entity.DeliveryOrders{ - MarketingId: id, - Notes: req.Notes, - CreatedUser: &marketing.CreatedUser, - Marketing: marketing, - DeliveryProducts: allDeliveryProducts, - } - - responseDTO := dto.ToDeliveryOrdersListDTOWithProducts(*deliveryOrderResponse, allDeliveryProducts) - return &responseDTO, nil -} - -func (s deliveryOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.DeliveryOrders, error) { - if err := s.Validate.Struct(req); err != nil { - return nil, err - } - - var action entity.ApprovalAction - switch req.Action { - case "APPROVED": - action = entity.ApprovalActionApproved - case "REJECTED": - action = entity.ApprovalActionRejected - default: - return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED") - } - - approvableIDs := utils.UniqueUintSlice(req.ApprovableIds) - if len(approvableIDs) == 0 { - return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") - } - - // Validate semua delivery order ada - for _, id := range approvableIDs { - _, err := s.Repository.GetByID(c.Context(), id, nil) - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery order %d not found", id)) - } - if err != nil { - s.Log.Errorf("Failed to get delivery order %d: %+v", id, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get delivery order %d", id)) - } - } - - err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTx *gorm.DB) error { - approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTx)) - - for _, approvableID := range approvableIDs { - actorID := uint(1) // TODO: get from auth context - if _, err := approvalSvc.CreateApproval( - c.Context(), - utils.ApprovalWorkflowMarketing, - approvableID, - utils.MarketingDeliveryOrder, - &action, - actorID, - req.Notes, - ); err != nil { - s.Log.Errorf("Failed to create approval for %d: %+v", approvableID, err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") - } - } - return nil - }) - if err != nil { if fiberErr, ok := err.(*fiber.Error); ok { return nil, fiberErr } - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery order") } - updated := make([]entity.DeliveryOrders, 0, len(approvableIDs)) - for _, id := range approvableIDs { - deliveryOrder, err := s.Repository.GetByID(c.Context(), id, s.withRelations) - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery order %d not found", id)) - } - s.Log.Errorf("Failed to get delivery order %d: %+v", id, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get delivery order %d", id)) - } - updated = append(updated, *deliveryOrder) - } - - return updated, nil + return s.getMarketingWithDeliveries(c, id) } -func (s deliveryOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { - if err := s.Repository.DeleteOne(c.Context(), id); err != nil { +func (s deliveryOrdersService) validateAndReduceProductWarehouse(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyDeliver float64) error { + if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { + return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") + } + + pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) + + pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil) + if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "DeliveryOrders not found") + return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") } - s.Log.Errorf("Failed to delete deliveryOrders: %+v", err) - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock") + } + + if pw.Quantity < qtyDeliver { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for warehouse - available: %.2f, requested: %.2f", pw.Quantity, qtyDeliver)) + } + + pw.Quantity = pw.Quantity - qtyDeliver + if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock") } return nil } + +func (s deliveryOrdersService) restoreProductWarehouseStock(ctx context.Context, tx *gorm.DB, marketingProduct *entity.MarketingProduct, qtyRestore float64) error { + if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 { + return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found") + } + + pwRepo := productWarehouseRepo.NewProductWarehouseRepository(tx) + pw, err := pwRepo.GetByID(ctx, marketingProduct.ProductWarehouseId, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Product warehouse not found") + } + + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check stock") + } + + pw.Quantity = pw.Quantity + qtyRestore + if err := pwRepo.UpdateOne(ctx, pw.Id, pw, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update stock") + } + + return nil +} diff --git a/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go b/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go index a80ad8d5..3317e952 100644 --- a/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go +++ b/internal/modules/marketing/delivery-orderss/validations/delivery-orders.validation.go @@ -2,26 +2,22 @@ package validation type DeliveryProduct struct { MarketingProductId uint `json:"marketing_product_id" validate:"required,gt=0"` - Qty float64 `json:"qty" validate:"required,gt=0"` - UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` - AvgWeight float64 `json:"avg_weight" validate:"required,gt=0"` - TotalWeight float64 `json:"total_weight" validate:"required,gt=0"` - TotalPrice float64 `json:"total_price" validate:"required,gt=0"` + Qty float64 `json:"qty" validate:"omitempty,gte=0"` + UnitPrice float64 `json:"unit_price" validate:"omitempty,gte=0"` + AvgWeight float64 `json:"avg_weight" validate:"omitempty,gte=0"` + TotalWeight float64 `json:"total_weight" validate:"omitempty,gte=0"` + TotalPrice float64 `json:"total_price" validate:"omitempty,gte=0"` DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"` } type Create struct { MarketingId uint `json:"marketing_id" validate:"required,gt=0"` - DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"required,min=1,dive"` - Notes string `json:"notes" validate:"omitempty,max=500"` } type Update struct { - DeliveryDate string `json:"delivery_date" validate:"omitempty,datetime=2006-01-02"` DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"omitempty,min=1,dive"` - Notes string `json:"notes" validate:"omitempty,max=500"` } type Query struct { diff --git a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go index a5365787..16d3b5be 100644 --- a/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go +++ b/internal/modules/marketing/sales-orders/controllers/sales-orders.controller.go @@ -1,7 +1,6 @@ package controller import ( - "math" "strconv" "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/dto" @@ -22,65 +21,6 @@ func NewSalesOrdersController(salesOrdersService service.SalesOrdersService) *Sa } } -func (u *SalesOrdersController) GetAll(c *fiber.Ctx) error { - query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - Search: c.Query("search", ""), - } - - if query.Page < 1 || query.Limit < 1 { - return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") - } - - result, totalResults, err := u.SalesOrdersService.GetAll(c, query) - if err != nil { - return err - } - - // Convert marketing data to sales orders DTOs with products - salesOrdersDTOs := make([]dto.SalesOrdersListDTO, len(result)) - for i, marketing := range result { - salesOrdersDTOs[i] = dto.ToSalesOrdersListDTOFromMarketing(marketing) - } - - return c.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.SalesOrdersListDTO]{ - Code: fiber.StatusOK, - Status: "success", - Message: "Get all salesOrderss successfully", - Meta: response.Meta{ - Page: query.Page, - Limit: query.Limit, - TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), - TotalResults: totalResults, - }, - Data: salesOrdersDTOs, - }) -} - -func (u *SalesOrdersController) GetOne(c *fiber.Ctx) error { - param := c.Params("id") - - id, err := strconv.Atoi(param) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") - } - - result, err := u.SalesOrdersService.GetOne(c, uint(id)) - if err != nil { - return err - } - - return c.Status(fiber.StatusOK). - JSON(response.Success{ - Code: fiber.StatusOK, - Status: "success", - Message: "Get salesOrders successfully", - Data: dto.ToSalesOrdersListDTOFromMarketing(*result), - }) -} - func (u *SalesOrdersController) CreateOne(c *fiber.Ctx) error { req := new(validation.Create) @@ -115,13 +55,7 @@ func (u *SalesOrdersController) UpdateOne(c *fiber.Ctx) error { return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - _, err = u.SalesOrdersService.UpdateOne(c, req, uint(id)) - if err != nil { - return err - } - - // Fetch full updated data for response - result, err := u.SalesOrdersService.GetOne(c, uint(id)) + result, err := u.SalesOrdersService.UpdateOne(c, req, uint(id)) if err != nil { return err } diff --git a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go index 79bc6453..03a0d59a 100644 --- a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go +++ b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go @@ -4,111 +4,37 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" - customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto" - productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" - warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" - userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" - "gitlab.com/mbugroup/lti-api.git/internal/utils" - approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" + productWarehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/dto" ) // === DTO Structs === -type SalesOrdersBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - type MarketingProductDTO struct { - Id uint `json:"id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - AvgWeight float64 `json:"avg_weight"` - TotalWeight float64 `json:"total_weight"` - TotalPrice float64 `json:"total_price"` - ProductWarehouse *struct { - Id uint `json:"id"` - Product *productDTO.ProductBaseDTO `json:"product,omitempty"` - Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` - } `json:"product_warehouse,omitempty"` - DeliveryProduct *MarketingDeliveryProductDTO `json:"delivery_product,omitempty"` -} - -type MarketingDeliveryProductDTO struct { - Id uint `json:"id"` - MarketingProductId uint `json:"marketing_product_id"` - Qty float64 `json:"qty"` - UnitPrice float64 `json:"unit_price"` - TotalWeight float64 `json:"total_weight"` - AvgWeight float64 `json:"avg_weight"` - TotalPrice float64 `json:"total_price"` - DeliveryDate *time.Time `json:"delivery_date"` - VehicleNumber string `json:"vehicle_number"` + Id uint `json:"id"` + Qty float64 `json:"qty"` + UnitPrice float64 `json:"unit_price"` + AvgWeight float64 `json:"avg_weight"` + TotalWeight float64 `json:"total_weight"` + TotalPrice float64 `json:"total_price"` + ProductWarehouse *productWarehouseDTO.ProductWarehousNestedDTO `json:"product_warehouse,omitempty"` } type SalesOrdersListDTO struct { - SalesOrdersBaseDTO - CustomerId uint `json:"customer_id,omitempty"` - Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` - SoDate *time.Time `json:"so_date,omitempty"` - SalesPersonId uint `json:"sales_person_id,omitempty"` - Notes string `json:"notes,omitempty"` - MarketingProducts []MarketingProductDTO `json:"marketing_products,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - Approval approvalDTO.ApprovalBaseDTO `json:"approval"` -} - -type SalesOrdersDetailDTO struct { - SalesOrdersListDTO + Id uint `json:"id"` + SoNumber string `json:"so_number"` + SoDate time.Time `json:"so_date"` + Notes string `json:"notes,omitempty"` + SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"` } // === Mapper Functions === -func ToSalesOrdersBaseDTO(e entity.SalesOrders) SalesOrdersBaseDTO { - return SalesOrdersBaseDTO{ - Id: e.Id, - Name: e.Name, - } -} - func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { - var productWarehouse *struct { - Id uint `json:"id"` - Product *productDTO.ProductBaseDTO `json:"product,omitempty"` - Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` - } + var productWarehouse *productWarehouseDTO.ProductWarehousNestedDTO if e.ProductWarehouse.Id != 0 { - product := (*productDTO.ProductBaseDTO)(nil) - if e.ProductWarehouse.Product.Id != 0 { - mapped := productDTO.ToProductBaseDTO(e.ProductWarehouse.Product) - product = &mapped - } - - warehouse := (*warehouseDTO.WarehouseBaseDTO)(nil) - if e.ProductWarehouse.Warehouse.Id != 0 { - mapped := warehouseDTO.ToWarehouseBaseDTO(e.ProductWarehouse.Warehouse) - warehouse = &mapped - } - - productWarehouse = &struct { - Id uint `json:"id"` - Product *productDTO.ProductBaseDTO `json:"product,omitempty"` - Warehouse *warehouseDTO.WarehouseBaseDTO `json:"warehouse,omitempty"` - }{ - Id: e.ProductWarehouse.Id, - Product: product, - Warehouse: warehouse, - } - } - - var deliveryProduct *MarketingDeliveryProductDTO - if e.DeliveryProduct != nil && e.DeliveryProduct.Id != 0 { - mapped := ToMarketingDeliveryProductDTO(*e.DeliveryProduct) - deliveryProduct = &mapped + mapped := productWarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse) + productWarehouse = &mapped } return MarketingProductDTO{ @@ -119,139 +45,38 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { TotalWeight: e.TotalWeight, TotalPrice: e.TotalPrice, ProductWarehouse: productWarehouse, - DeliveryProduct: deliveryProduct, } } -func ToMarketingDeliveryProductDTO(e entity.MarketingDeliveryProduct) MarketingDeliveryProductDTO { - return MarketingDeliveryProductDTO{ - Id: e.Id, - MarketingProductId: e.MarketingProductId, - Qty: e.Qty, - UnitPrice: e.UnitPrice, - TotalWeight: e.TotalWeight, - AvgWeight: e.AvgWeight, - TotalPrice: e.TotalPrice, - DeliveryDate: e.DeliveryDate, - VehicleNumber: e.VehicleNumber, - } -} - -func defaultSalesOrdersLatestApproval(e entity.SalesOrders) approvalDTO.ApprovalBaseDTO { - result := approvalDTO.ApprovalBaseDTO{} - - step := utils.MarketingStepPengajuan - if step > 0 { - result.StepNumber = uint16(step) - if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowMarketing, step); ok { - result.StepName = label - } else if label, ok := utils.MarketingApprovalSteps[step]; ok { - result.StepName = label - } - } - - if e.CreatedUser.Id != 0 { - result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser) - } else if e.CreatedBy != 0 { - result.ActionBy = userDTO.UserBaseDTO{ - Id: e.CreatedBy, - IdUser: int64(e.CreatedBy), - } - } - - return result -} - func ToSalesOrdersListDTO(e entity.SalesOrders) SalesOrdersListDTO { - var createdUser *userDTO.UserBaseDTO - if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) - createdUser = &mapped - } - - approval := defaultSalesOrdersLatestApproval(e) - if e.LatestApproval != nil { - approval = approvalDTO.ToApprovalDTO(*e.LatestApproval) - } return SalesOrdersListDTO{ - SalesOrdersBaseDTO: ToSalesOrdersBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, - Approval: approval, + Id: e.Id, + SoNumber: e.Name, + SoDate: time.Time{}, + Notes: "", + SalesOrder: []MarketingProductDTO{}, } } func ToSalesOrdersListDTOFromMarketing(e entity.Marketing) SalesOrdersListDTO { - var createdUser *userDTO.UserBaseDTO - if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) - createdUser = &mapped - } - - var marketingProducts []MarketingProductDTO + var salesOrder []MarketingProductDTO if len(e.Products) > 0 { - marketingProducts = make([]MarketingProductDTO, len(e.Products)) + salesOrder = make([]MarketingProductDTO, len(e.Products)) for i, product := range e.Products { - marketingProducts[i] = ToMarketingProductDTO(product) + salesOrder[i] = ToMarketingProductDTO(product) } } - var customerSummary *customerDTO.CustomerBaseDTO - if e.Customer.Id != 0 { - mapped := customerDTO.ToCustomerBaseDTO(e.Customer) - customerSummary = &mapped - } - - approval := defaultSalesOrdersLatestApprovalFromMarketing(e) - if e.LatestApproval != nil { - approval = approvalDTO.ToApprovalDTO(*e.LatestApproval) - } - return SalesOrdersListDTO{ - SalesOrdersBaseDTO: SalesOrdersBaseDTO{ - Id: e.Id, - Name: e.SoNumber, - }, - CustomerId: e.Customer.Id, - Customer: customerSummary, - SoDate: &e.SoDate, - SalesPersonId: e.SalesPersonId, - Notes: e.Notes, - MarketingProducts: marketingProducts, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, - Approval: approval, + Id: e.Id, + SoNumber: e.SoNumber, + SoDate: e.SoDate, + Notes: e.Notes, + SalesOrder: salesOrder, } } -func defaultSalesOrdersLatestApprovalFromMarketing(e entity.Marketing) approvalDTO.ApprovalBaseDTO { - result := approvalDTO.ApprovalBaseDTO{} - - step := utils.MarketingStepPengajuan - if step > 0 { - result.StepNumber = uint16(step) - if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowMarketing, step); ok { - result.StepName = label - } else if label, ok := utils.MarketingApprovalSteps[step]; ok { - result.StepName = label - } - } - - if e.CreatedUser.Id != 0 { - result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser) - } else if e.CreatedBy != 0 { - result.ActionBy = userDTO.UserBaseDTO{ - Id: e.CreatedBy, - IdUser: int64(e.CreatedBy), - } - } - - return result -} - func ToSalesOrdersListDTOsFromMarketing(e []entity.Marketing) []SalesOrdersListDTO { result := make([]SalesOrdersListDTO, len(e)) for i, r := range e { @@ -259,17 +84,3 @@ func ToSalesOrdersListDTOsFromMarketing(e []entity.Marketing) []SalesOrdersListD } return result } - -func ToSalesOrdersListDTOs(e []entity.SalesOrders) []SalesOrdersListDTO { - result := make([]SalesOrdersListDTO, len(e)) - for i, r := range e { - result[i] = ToSalesOrdersListDTO(r) - } - return result -} - -func ToSalesOrdersDetailDTO(e entity.SalesOrders) SalesOrdersDetailDTO { - return SalesOrdersDetailDTO{ - SalesOrdersListDTO: ToSalesOrdersListDTO(e), - } -} diff --git a/internal/modules/marketing/sales-orders/module.go b/internal/modules/marketing/sales-orders/module.go index 35d002fc..6d1963af 100644 --- a/internal/modules/marketing/sales-orders/module.go +++ b/internal/modules/marketing/sales-orders/module.go @@ -1,6 +1,8 @@ package sales_orders import ( + "fmt" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "gorm.io/gorm" @@ -11,10 +13,9 @@ import ( rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" - rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" - rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + "gitlab.com/mbugroup/lti-api.git/internal/utils" ) type SalesOrdersModule struct{} @@ -23,12 +24,16 @@ func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valida salesOrdersRepo := rSalesOrders.NewSalesOrdersRepository(db) userRepo := rUser.NewUserRepository(db) customerRepo := rCustomer.NewCustomerRepository(db) - kandangRepo := rKandang.NewKandangRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) marketingRepo := rSalesOrders.NewMarketingRepository(db) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db)) - salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, kandangRepo, productWarehouseRepo, marketingRepo, userRepo, approvalSvc, validate) + + if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err)) + } + + salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, productWarehouseRepo, marketingRepo, userRepo, approvalSvc, validate) userService := sUser.NewUserService(userRepo, validate) SalesOrdersRoutes(router, userService, salesOrdersService) diff --git a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go index f06bf401..dd0f99ab 100644 --- a/internal/modules/marketing/sales-orders/repositories/marketings.repository.go +++ b/internal/modules/marketing/sales-orders/repositories/marketings.repository.go @@ -11,6 +11,7 @@ import ( type MarketingRepository interface { repository.BaseRepository[entity.Marketing] IdExists(ctx context.Context, id uint) (bool, error) + GetNextSequence(ctx context.Context) (uint, error) } type MarketingRepositoryImpl struct { @@ -26,3 +27,11 @@ func NewMarketingRepository(db *gorm.DB) MarketingRepository { func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { return repository.Exists[entity.Marketing](ctx, r.DB(), id) } + +func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, error) { + var maxID uint + if err := r.DB().WithContext(ctx).Model(&entity.Marketing{}).Select("COALESCE(MAX(id), 0)").Scan(&maxID).Error; err != nil { + return 0, err + } + return maxID + 1, nil +} diff --git a/internal/modules/marketing/sales-orders/route.go b/internal/modules/marketing/sales-orders/route.go index c48ae2a7..ae6d7a81 100644 --- a/internal/modules/marketing/sales-orders/route.go +++ b/internal/modules/marketing/sales-orders/route.go @@ -12,18 +12,15 @@ import ( func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesOrdersService) { ctrl := controller.NewSalesOrdersController(s) + v1.Delete("/:id", ctrl.DeleteOne) route := v1.Group("/sales-orders") - // route.Get("/", m.Auth(u), ctrl.GetAll) // route.Post("/", m.Auth(u), ctrl.CreateOne) - // route.Get("/:id", m.Auth(u), ctrl.GetOne) // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) - route.Get("/", ctrl.GetAll) route.Post("/", ctrl.CreateOne) - route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) - route.Delete("/:id", ctrl.DeleteOne) + route.Post("/approvals", ctrl.Approval) } diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 74244496..0dc47a2c 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -1,19 +1,19 @@ package service import ( + "context" "errors" "fmt" "strings" - "time" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + rInvMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations" customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" - kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" @@ -25,8 +25,6 @@ import ( ) type SalesOrdersService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) - GetOne(ctx *fiber.Ctx, id uint) (*entity.Marketing, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Marketing, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Marketing, error) DeleteOne(ctx *fiber.Ctx, id uint) error @@ -38,20 +36,18 @@ type salesOrdersService struct { Validate *validator.Validate Repository repository.SalesOrdersRepository CustomerRepo customerRepo.CustomerRepository - KandangRepo kandangRepo.KandangRepository ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository MarketingRepo repository.MarketingRepository UserRepo userRepo.UserRepository ApprovalSvc commonSvc.ApprovalService } -func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, kandangRepo kandangRepo.KandangRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService { +func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService { return &salesOrdersService{ Log: utils.Log, Validate: validate, Repository: repo, CustomerRepo: customerRepo, - KandangRepo: kandangRepo, ProductWarehouseRepo: productWarehouseRepo, MarketingRepo: marketingRepo, UserRepo: userRepo, @@ -60,58 +56,15 @@ func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo c } func (s salesOrdersService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser"). + return db. + Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). Preload("Products.ProductWarehouse.Product"). Preload("Products.ProductWarehouse.Warehouse") } -func (s salesOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Marketing, int64, error) { - if err := s.Validate.Struct(params); err != nil { - return nil, 0, err - } - - offset := (params.Page - 1) * params.Limit - - marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - db = s.withRelations(db) - if params.Search != "" { - return db.Where("so_number LIKE ?", "%"+params.Search+"%") - } - return db.Order("created_at DESC").Order("updated_at DESC") - }) - - if err != nil { - s.Log.Errorf("Failed to get marketings: %+v", err) - return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales orders") - } - - if s.ApprovalSvc != nil && len(marketings) > 0 { - ids := make([]uint, len(marketings)) - for i, item := range marketings { - ids[i] = item.Id - } - - latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowMarketing, ids, func(db *gorm.DB) *gorm.DB { - return db.Preload("ActionUser") - }) - if err != nil { - s.Log.Warnf("Unable to load latest approvals for marketings: %+v", err) - } else if len(latestMap) > 0 { - for i := range marketings { - if approval, ok := latestMap[marketings[i].Id]; ok { - marketings[i].LatestApproval = approval - } - } - } - } - - return marketings, total, nil -} - -func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) { - +func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, error) { marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") @@ -125,15 +78,9 @@ func (s salesOrdersService) GetOne(c *fiber.Ctx, id uint) (*entity.Marketing, er approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, func(db *gorm.DB) *gorm.DB { return db.Preload("ActionUser") }) - if err != nil { - s.Log.Warnf("Unable to load approvals for marketing %d: %+v", id, err) - } else if len(approvals) > 0 { - if marketing.LatestApproval == nil { - latest := approvals[len(approvals)-1] - marketing.LatestApproval = &latest - } - } else { - marketing.LatestApproval = nil + if err == nil && len(approvals) > 0 { + latest := approvals[len(approvals)-1] + marketing.LatestApproval = &latest } } @@ -153,7 +100,6 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e for _, item := range req.MarketingProducts { if err := commonSvc.EnsureRelations(c.Context(), - commonSvc.RelationCheck{Name: "Kandang", ID: &item.KandangId, Exists: s.KandangRepo.IdExists}, commonSvc.RelationCheck{Name: "ProductWarehouse", ID: &item.ProductWarehouseId, Exists: s.ProductWarehouseRepo.IdExists}, ); err != nil { return nil, err @@ -165,14 +111,18 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format") } - soNumber := "SO-" + time.Now().Format("20060102150405") + nextSeq, err := s.MarketingRepo.GetNextSequence(c.Context()) + if err != nil { + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate SO number") + } + soNumber := fmt.Sprintf("SO-%05d", nextSeq) var marketing *entity.Marketing err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingRepoTx := repository.NewMarketingRepository(dbTransaction) marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) - marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) + invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) marketing = &entity.Marketing{ @@ -184,52 +134,18 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e CreatedBy: 1, } if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil { - s.Log.Errorf("Failed to create marketing: %+v", err) - return err + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders") } if len(req.MarketingProducts) > 0 { for _, product := range req.MarketingProducts { - marketingProduct := &entity.MarketingProduct{ - MarketingId: marketing.Id, - ProductWarehouseId: product.ProductWarehouseId, - Qty: product.Qty, - UnitPrice: product.UnitPrice, - AvgWeight: product.AvgWeight, - TotalWeight: product.TotalWeight, - TotalPrice: product.TotalPrice, - } - if err := marketingProductRepoTx.CreateOne(c.Context(), marketingProduct, nil); err != nil { - s.Log.Errorf("Failed to create marketing product: %+v", err) - return err - } - - marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ - MarketingProductId: marketingProduct.Id, - Qty: 0, - UnitPrice: 0, - TotalWeight: 0, - AvgWeight: 0, - TotalPrice: 0, - DeliveryDate: nil, - VehicleNumber: product.VehicleNumber, - } - if err := marketingDeliveryProductRepoTx.CreateOne(c.Context(), marketingDeliveryProduct, nil); err != nil { - s.Log.Errorf("Failed to create marketing delivery product: %+v", err) - return err + if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") } } } actorID := uint(1) // TODO: ambil dari auth context - if err := approvalSvcTx.RegisterWorkflowSteps( - utils.ApprovalWorkflowMarketing, - utils.MarketingApprovalSteps, - ); err != nil { - s.Log.Errorf("Failed to register workflow steps: %+v", err) - return err - } - approvalAction := entity.ApprovalActionCreated if _, err := approvalSvcTx.CreateApproval( c.Context(), @@ -240,8 +156,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e actorID, nil); err != nil { if !errors.Is(err, gorm.ErrDuplicatedKey) { - s.Log.Errorf("Failed to create approval: %+v", err) - return err + fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval") } } @@ -256,7 +171,6 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e marketing, err = s.MarketingRepo.GetByID(c.Context(), marketing.Id, s.withRelations) if err != nil { - s.Log.Errorf("Failed to fetch created marketing: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch created sales order") } @@ -278,7 +192,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil) if err != nil { - s.Log.Errorf("Failed to check approval status: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } if latestApproval != nil && latestApproval.StepNumber >= 3 { @@ -299,8 +212,8 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u marketingRepoTx := repository.NewMarketingRepository(dbTransaction) marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) - marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + invDeliveryRepoTx := rInvMarketingDeliveryProduct.NewMarketingDeliveryProductRepository(dbTransaction) updateBody := make(map[string]any) if req.CustomerId != 0 { @@ -330,53 +243,85 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u oldProducts, err := marketingProductRepoTx.GetByMarketingID(c.Context(), id) if err != nil && err != gorm.ErrRecordNotFound { - s.Log.Errorf("Failed to fetch old marketing products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch existing products") } - for _, oldProduct := range oldProducts { - if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { - return db.Where("marketing_product_id = ?", oldProduct.Id) - }); err != nil && err != gorm.ErrRecordNotFound { - s.Log.Errorf("Failed to delete delivery products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") + oldByPW := make(map[uint]*entity.MarketingProduct) + for i := range oldProducts { + p := oldProducts[i] + oldByPW[p.ProductWarehouseId] = &p + } + + reqByPW := make(map[uint]validation.CreateMarketingProduct) + for _, rp := range req.MarketingProducts { + reqByPW[rp.ProductWarehouseId] = rp + } + + for _, rp := range req.MarketingProducts { + if old, ok := oldByPW[rp.ProductWarehouseId]; ok { + + updateBody := map[string]any{ + "product_warehouse_id": rp.ProductWarehouseId, + "qty": rp.Qty, + "unit_price": rp.UnitPrice, + "avg_weight": rp.AvgWeight, + "total_weight": rp.TotalWeight, + "total_price": rp.TotalPrice, + } + if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") + } + + // Ensure delivery product exists; if not, create default + if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + mdp := &entity.MarketingDeliveryProduct{ + MarketingProductId: old.Id, + Qty: 0, + UnitPrice: 0, + TotalWeight: 0, + AvgWeight: 0, + TotalPrice: 0, + DeliveryDate: nil, + VehicleNumber: rp.VehicleNumber, + } + if err := invDeliveryRepoTx.CreateOne(c.Context(), mdp, nil); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing delivery product") + } + } else { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check delivery product") + } + } + } else { + // Create new marketing product (use helper) + if err := s.createMarketingProductWithDelivery(c.Context(), id, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") + } } } - if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { - return db.Where("marketing_id = ?", id) - }); err != nil && err != gorm.ErrRecordNotFound { - s.Log.Errorf("Failed to delete marketing products: %+v", err) - return fiber.NewError(fiber.StatusInternalServerError, "Failed to update products") - } - - for _, product := range req.MarketingProducts { - marketingProduct := &entity.MarketingProduct{ - MarketingId: id, - ProductWarehouseId: product.ProductWarehouseId, - Qty: product.Qty, - UnitPrice: product.UnitPrice, - AvgWeight: product.AvgWeight, - TotalWeight: product.TotalWeight, - TotalPrice: product.TotalPrice, - } - if err := marketingProductRepoTx.CreateOne(c.Context(), marketingProduct, nil); err != nil { - - return err - } - - marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ - MarketingProductId: marketingProduct.Id, - Qty: 0, - UnitPrice: 0, - TotalWeight: 0, - AvgWeight: 0, - TotalPrice: 0, - DeliveryDate: nil, - VehicleNumber: product.VehicleNumber, - } - if err := marketingDeliveryProductRepoTx.CreateOne(c.Context(), marketingDeliveryProduct, nil); err != nil { - return err + // 2) Delete missing old products (prevent deletion if deliveries exist) + for _, old := range oldProducts { + if _, ok := reqByPW[old.ProductWarehouseId]; !ok { + // Check delivery product for this marketing product + deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) + if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product") + } + if err == nil { + // If delivery exists (delivery_date not nil or qty > 0), prevent deletion + if deliveryProduct.DeliveryDate != nil || deliveryProduct.Qty > 0 { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id)) + } + // safe to delete delivery product record + if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing delivery product") + } + } + // Delete marketing product + if err := marketingProductRepoTx.DeleteOne(c.Context(), old.Id); err != nil { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing product") + } } } } @@ -394,7 +339,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u actorID, &resetNote) if err != nil { - s.Log.Errorf("Failed to create reset approval: %+v", err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval status") } } @@ -409,16 +353,16 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update sales order") } - return s.GetOne(c, id) + return s.getOne(c, id) } func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "SalesOrders not found") } if err != nil { - s.Log.Errorf("Failed to fetch marketing %d before delete: %+v", id, err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") } @@ -494,7 +438,6 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e if err != nil { return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status") } - if latestApproval == nil { return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d - sales orders must be created first", id)) } @@ -511,13 +454,12 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e } err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) for _, approvableID := range approvableIDs { - latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, approvableID, nil) if err != nil { - s.Log.Errorf("Failed to get latest approval for %d: %+v", approvableID, err) return fiber.NewError(fiber.StatusInternalServerError, "Failed to check current approval step") } @@ -568,7 +510,7 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e updated := make([]entity.Marketing, 0, len(approvableIDs)) for _, id := range approvableIDs { - marketing, err := s.GetOne(c, id) + marketing, err := s.getOne(c, id) if err != nil { return nil, err } @@ -577,3 +519,35 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e return updated, nil } + +func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo rInvMarketingDeliveryProduct.MarketingDeliveryProductRepository) error { + + marketingProduct := &entity.MarketingProduct{ + MarketingId: marketingId, + ProductWarehouseId: rp.ProductWarehouseId, + Qty: rp.Qty, + UnitPrice: rp.UnitPrice, + AvgWeight: rp.AvgWeight, + TotalWeight: rp.TotalWeight, + TotalPrice: rp.TotalPrice, + } + if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil { + return err + } + + marketingDeliveryProduct := &entity.MarketingDeliveryProduct{ + MarketingProductId: marketingProduct.Id, + Qty: 0, + UnitPrice: 0, + TotalWeight: 0, + AvgWeight: 0, + TotalPrice: 0, + DeliveryDate: nil, + VehicleNumber: rp.VehicleNumber, + } + if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil { + return err + } + + return nil +} diff --git a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go index 01b3af9d..47d2e616 100644 --- a/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go +++ b/internal/modules/marketing/sales-orders/validations/sales-orders.validation.go @@ -10,7 +10,6 @@ type Create struct { type CreateMarketingProduct struct { VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"` - KandangId uint `json:"kandang_id" validate:"required,gt=0"` ProductWarehouseId uint `json:"product_warehouse_id" validate:"required,gt=0"` UnitPrice float64 `json:"unit_price" validate:"required,gt=0"` TotalWeight float64 `json:"total_weight" validate:"required,gt=0"` @@ -27,12 +26,6 @@ type Update struct { MarketingProducts []CreateMarketingProduct `json:"marketing_products" validate:"omitempty,min=1,dive"` } -type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=50"` -} - type Approve struct { Action string `json:"action" validate:"required_strict"` ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"` From 60757237c0f03b00d2f668309c97da937086c2da Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 17 Nov 2025 09:28:30 +0700 Subject: [PATCH 23/29] Feat[BE-222]: add marketing product to get all marketing for Frontend needs --- .../delivery-orderss/dto/delivery-orders.dto.go | 10 ++++++++++ .../services/delivery-orders.service.go | 9 +++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go index be662412..13a46580 100644 --- a/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go +++ b/internal/modules/marketing/delivery-orderss/dto/delivery-orders.dto.go @@ -24,6 +24,7 @@ type MarketingListDTO struct { Customer *customerDTO.CustomerBaseDTO `json:"customer,omitempty"` SalesPerson *userDTO.UserBaseDTO `json:"sales_person,omitempty"` SoDocs string `json:"so_docs,omitempty"` + SalesOrder []MarketingProductDTO `json:"sales_order,omitempty"` CreatedUser *userDTO.UserBaseDTO `json:"created_user"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` @@ -154,11 +155,20 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M latestApproval = &mapped } + var salesOrderProducts []MarketingProductDTO + if len(marketing.Products) > 0 { + salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products)) + for i, product := range marketing.Products { + salesOrderProducts[i] = ToMarketingProductDTO(product) + } + } + return MarketingListDTO{ MarketingBaseDTO: ToMarketingBaseDTO(marketing), Customer: customer, SalesPerson: salesPerson, SoDocs: marketing.SoDocs, + SalesOrder: salesOrderProducts, CreatedUser: createdUser, CreatedAt: marketing.CreatedAt, UpdatedAt: marketing.UpdatedAt, diff --git a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go index 45b11d6d..712c6ace 100644 --- a/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go +++ b/internal/modules/marketing/delivery-orderss/services/delivery-orders.service.go @@ -97,7 +97,10 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([ Preload("CreatedUser"). Preload("Customer"). Preload("SalesPerson"). - Preload("Products.ProductWarehouse") + Preload("Products.ProductWarehouse.Product"). + Preload("Products.ProductWarehouse.Warehouse"). + Preload("Products.DeliveryProduct") + if params.MarketingId != 0 { return db.Where("id = ?", params.MarketingId) } @@ -109,7 +112,9 @@ func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([ return nil, 0, err } for i := range marketings { - latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketings[i].Id, nil) + latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketings[i].Id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) if err != nil { s.Log.Warnf("Failed to load approval for marketing %d: %+v", marketings[i].Id, err) } From cb1df12b7e9714f28028c8e3995cfb49b1b597c7 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 17 Nov 2025 11:03:15 +0700 Subject: [PATCH 24/29] Feat[BE-260]: create BOP migration --- ...51117034511_create_expenses_table.down.sql | 1 + ...0251117034511_create_expenses_table.up.sql | 44 ++++++++++++++++ ...29_create_expense_nonstocks_table.down.sql | 1 + ...4529_create_expense_nonstocks_table.up.sql | 50 +++++++++++++++++++ ...create_expense_realizations_table.down.sql | 1 + ...8_create_expense_realizations_table.up.sql | 40 +++++++++++++++ 6 files changed, 137 insertions(+) create mode 100644 internal/database/migrations/20251117034511_create_expenses_table.down.sql create mode 100644 internal/database/migrations/20251117034511_create_expenses_table.up.sql create mode 100644 internal/database/migrations/20251117034529_create_expense_nonstocks_table.down.sql create mode 100644 internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql create mode 100644 internal/database/migrations/20251117034538_create_expense_realizations_table.down.sql create mode 100644 internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql diff --git a/internal/database/migrations/20251117034511_create_expenses_table.down.sql b/internal/database/migrations/20251117034511_create_expenses_table.down.sql new file mode 100644 index 00000000..bf0ea945 --- /dev/null +++ b/internal/database/migrations/20251117034511_create_expenses_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS expenses; \ No newline at end of file diff --git a/internal/database/migrations/20251117034511_create_expenses_table.up.sql b/internal/database/migrations/20251117034511_create_expenses_table.up.sql new file mode 100644 index 00000000..054084e4 --- /dev/null +++ b/internal/database/migrations/20251117034511_create_expenses_table.up.sql @@ -0,0 +1,44 @@ +CREATE TABLE expenses ( + id BIGSERIAL PRIMARY KEY, + reference_number VARCHAR, -- format => BOP-LTI-0001 = 0001 is increment + supplier_id BIGINT NULL, + category VARCHAR(50) NOT NULL CHECK ( + category IN ('BOP', 'NON-BOP') + ), + po_number VARCHAR(50) UNIQUE NOT NULL, + document_path JSON, + expense_date DATE NOT NULL, + grand_total NUMERIC(15, 3) DEFAULT 0, + note TEXT, + created_by BIGINT, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ +); + +-- Tambahkan Foreign Key ke suppliers +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'suppliers') THEN + ALTER TABLE expenses + ADD CONSTRAINT fk_expenses_supplier_id + FOREIGN KEY (supplier_id) REFERENCES suppliers(id); + END IF; +END $$; + +-- Tambahkan Foreign Key ke users (created_by) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE expenses + ADD CONSTRAINT fk_expenses_created_by + FOREIGN KEY (created_by) REFERENCES users(id); + END IF; +END $$; + +-- Index +CREATE INDEX idx_expenses_supplier_id ON expenses (supplier_id); + +CREATE INDEX idx_expenses_expense_date ON expenses (expense_date); + +CREATE INDEX idx_expenses_deleted_at ON expenses (deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251117034529_create_expense_nonstocks_table.down.sql b/internal/database/migrations/20251117034529_create_expense_nonstocks_table.down.sql new file mode 100644 index 00000000..70fcf148 --- /dev/null +++ b/internal/database/migrations/20251117034529_create_expense_nonstocks_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS expense_nonstocks; \ No newline at end of file diff --git a/internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql b/internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql new file mode 100644 index 00000000..5b0c2c16 --- /dev/null +++ b/internal/database/migrations/20251117034529_create_expense_nonstocks_table.up.sql @@ -0,0 +1,50 @@ +CREATE TABLE expense_nonstocks ( + id BIGSERIAL PRIMARY KEY, + expense_id BIGINT, + project_flock_kandang_id BIGINT, + nonstock_id BIGINT, + qty NUMERIC(15, 3) NOT NULL, + unit_price NUMERIC(15, 3) NOT NULL, + total_price NUMERIC(15, 3) NOT NULL, + note TEXT NULL, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ +); + +-- Tambahkan Foreign Key ke expenses +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expenses') THEN + ALTER TABLE expense_nonstocks + ADD CONSTRAINT fk_expense_nonstocks_expense_id + FOREIGN KEY (expense_id) REFERENCES expenses(id); + END IF; +END $$; + +-- Tambahkan Foreign Key ke project_flock_kandangs +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN + ALTER TABLE expense_nonstocks + ADD CONSTRAINT fk_expense_nonstocks_kandang_id + FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs(id); + END IF; +END $$; + +-- Tambahkan Foreign Key ke nonstocks +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'nonstocks') THEN + ALTER TABLE expense_nonstocks + ADD CONSTRAINT fk_expense_nonstocks_nonstock_id + FOREIGN KEY (nonstock_id) REFERENCES nonstocks(id); + END IF; +END $$; + +-- Index +CREATE INDEX idx_expense_nonstocks_expense_id ON expense_nonstocks (expense_id); + +CREATE INDEX idx_expense_nonstocks_nonstock_id ON expense_nonstocks (nonstock_id); + +CREATE INDEX idx_expense_nonstocks_deleted_at ON expense_nonstocks (deleted_at); \ No newline at end of file diff --git a/internal/database/migrations/20251117034538_create_expense_realizations_table.down.sql b/internal/database/migrations/20251117034538_create_expense_realizations_table.down.sql new file mode 100644 index 00000000..5f70a0e6 --- /dev/null +++ b/internal/database/migrations/20251117034538_create_expense_realizations_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS expense_realizations; \ No newline at end of file diff --git a/internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql b/internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql new file mode 100644 index 00000000..4a8dc148 --- /dev/null +++ b/internal/database/migrations/20251117034538_create_expense_realizations_table.up.sql @@ -0,0 +1,40 @@ +CREATE TABLE expense_realizations ( + id BIGSERIAL PRIMARY KEY, + expense_nonstock_id BIGINT, + realization_qty NUMERIC(15, 3) NOT NULL, + realization_unit_price NUMERIC(15, 3) NOT NULL, + realization_total_price NUMERIC(15, 3) NOT NULL, + realization_date DATE NOT NULL, + note TEXT, + created_by BIGINT, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ +); + +-- Tambahkan Foreign Key ke expense_nonstocks +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'expense_nonstocks') THEN + ALTER TABLE expense_realizations + ADD CONSTRAINT fk_expense_realizations_nonstock_id + FOREIGN KEY (expense_nonstock_id) REFERENCES expense_nonstocks(id); + END IF; +END $$; + +-- Tambahkan Foreign Key ke users (created_by) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE expense_realizations + ADD CONSTRAINT fk_expense_realizations_created_by + FOREIGN KEY (created_by) REFERENCES users(id); + END IF; +END $$; + +-- Index +CREATE INDEX idx_expense_realizations_nonstock_id ON expense_realizations (expense_nonstock_id); + +CREATE INDEX idx_expense_realizations_date ON expense_realizations (realization_date); + +CREATE INDEX idx_expense_realizations_deleted_at ON expense_realizations (deleted_at); \ No newline at end of file From 09d503f5bee656288dd4ab5e31949296da8c8d67 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 17 Nov 2025 14:46:21 +0700 Subject: [PATCH 25/29] Feat[BE-261] : inisiate expense module --- internal/entities/delivery-orders.go | 23 --- .../entities/{sales-orders.go => expense.go} | 5 +- .../controllers/expense.controller.go | 144 ++++++++++++++++++ internal/modules/expenses/dto/expense.dto.go | 66 ++++++++ internal/modules/expenses/module.go | 26 ++++ .../repositories/expense.repository.go | 21 +++ internal/modules/expenses/route.go | 28 ++++ .../expenses/services/expense.service.go | 129 ++++++++++++++++ .../validations/expense.validation.go | 15 ++ .../dto/product_warehouse.dto.go | 2 +- .../sales-orders/dto/sales-orders.dto.go | 14 +- .../modules/marketing/sales-orders/module.go | 5 +- .../repositories/sales-orders.repository.go | 21 --- .../services/sales-orders.service.go | 14 +- internal/route/route.go | 2 + 15 files changed, 451 insertions(+), 64 deletions(-) delete mode 100644 internal/entities/delivery-orders.go rename internal/entities/{sales-orders.go => expense.go} (70%) create mode 100644 internal/modules/expenses/controllers/expense.controller.go create mode 100644 internal/modules/expenses/dto/expense.dto.go create mode 100644 internal/modules/expenses/module.go create mode 100644 internal/modules/expenses/repositories/expense.repository.go create mode 100644 internal/modules/expenses/route.go create mode 100644 internal/modules/expenses/services/expense.service.go create mode 100644 internal/modules/expenses/validations/expense.validation.go delete mode 100644 internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go diff --git a/internal/entities/delivery-orders.go b/internal/entities/delivery-orders.go deleted file mode 100644 index 291ba20c..00000000 --- a/internal/entities/delivery-orders.go +++ /dev/null @@ -1,23 +0,0 @@ -package entities - -import ( - "time" - - "gorm.io/gorm" -) - -type DeliveryOrders struct { - Id uint `gorm:"primaryKey" json:"id"` - DeliveryNumber string `gorm:"type:varchar(255);not null;uniqueIndex" json:"delivery_number"` - DeliveryDate time.Time `gorm:"not null" json:"delivery_date"` - MarketingId uint `gorm:"not null" json:"marketing_id"` - Notes string `gorm:"type:text" json:"notes"` - CreatedBy uint `gorm:"not null" json:"created_by"` - CreatedAt time.Time `gorm:"autoCreateTime" json:"created_at"` - UpdatedAt time.Time `gorm:"autoUpdateTime" json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - - Marketing *Marketing `gorm:"foreignKey:MarketingId;references:Id" json:"marketing,omitempty"` - DeliveryProducts []MarketingDeliveryProduct `gorm:"-" json:"delivery_products,omitempty"` - CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id" json:"created_user,omitempty"` -} diff --git a/internal/entities/sales-orders.go b/internal/entities/expense.go similarity index 70% rename from internal/entities/sales-orders.go rename to internal/entities/expense.go index faa6d901..a427582d 100644 --- a/internal/entities/sales-orders.go +++ b/internal/entities/expense.go @@ -6,7 +6,7 @@ import ( "gorm.io/gorm" ) -type SalesOrders struct { +type Expense struct { Id uint `gorm:"primaryKey"` Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"` CreatedBy uint `gorm:"not null"` @@ -14,6 +14,5 @@ type SalesOrders struct { UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` - LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` } diff --git a/internal/modules/expenses/controllers/expense.controller.go b/internal/modules/expenses/controllers/expense.controller.go new file mode 100644 index 00000000..074f2f0a --- /dev/null +++ b/internal/modules/expenses/controllers/expense.controller.go @@ -0,0 +1,144 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type ExpenseController struct { + ExpenseService service.ExpenseService +} + +func NewExpenseController(expenseService service.ExpenseService) *ExpenseController { + return &ExpenseController{ + ExpenseService: expenseService, + } +} + +func (u *ExpenseController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + result, totalResults, err := u.ExpenseService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.ExpenseListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all expenses successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToExpenseListDTOs(result), + }) +} + +func (u *ExpenseController) GetOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + result, err := u.ExpenseService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get expense successfully", + Data: dto.ToExpenseListDTO(*result), + }) +} + +func (u *ExpenseController) CreateOne(c *fiber.Ctx) error { + req := new(validation.Create) + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.ExpenseService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create expense successfully", + Data: dto.ToExpenseListDTO(*result), + }) +} + +func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error { + req := new(validation.Update) + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.ExpenseService.UpdateOne(c, req, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Update expense successfully", + Data: dto.ToExpenseListDTO(*result), + }) +} + +func (u *ExpenseController) DeleteOne(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + if err := u.ExpenseService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete expense successfully", + }) +} diff --git a/internal/modules/expenses/dto/expense.dto.go b/internal/modules/expenses/dto/expense.dto.go new file mode 100644 index 00000000..b7bd1b5f --- /dev/null +++ b/internal/modules/expenses/dto/expense.dto.go @@ -0,0 +1,66 @@ +package dto + +import ( + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" +) + +// === DTO Structs === + +type ExpenseBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type ExpenseListDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ExpenseDetailDTO struct { + ExpenseListDTO +} + +// === Mapper Functions === + +func ToExpenseBaseDTO(e entity.Expense) ExpenseBaseDTO { + return ExpenseBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToExpenseListDTO(e entity.Expense) ExpenseListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + createdUser = &mapped + } + + return ExpenseListDTO{ + Id: e.Id, + Name: e.Name, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToExpenseListDTOs(e []entity.Expense) []ExpenseListDTO { + result := make([]ExpenseListDTO, len(e)) + for i, r := range e { + result[i] = ToExpenseListDTO(r) + } + return result +} + +func ToExpenseDetailDTO(e entity.Expense) ExpenseDetailDTO { + return ExpenseDetailDTO{ + ExpenseListDTO: ToExpenseListDTO(e), + } +} diff --git a/internal/modules/expenses/module.go b/internal/modules/expenses/module.go new file mode 100644 index 00000000..c9b2ab66 --- /dev/null +++ b/internal/modules/expenses/module.go @@ -0,0 +1,26 @@ +package expenses + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" + sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type ExpenseModule struct{} + +func (ExpenseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + expenseRepo := rExpense.NewExpenseRepository(db) + userRepo := rUser.NewUserRepository(db) + + expenseService := sExpense.NewExpenseService(expenseRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + ExpenseRoutes(router, userService, expenseService) +} + diff --git a/internal/modules/expenses/repositories/expense.repository.go b/internal/modules/expenses/repositories/expense.repository.go new file mode 100644 index 00000000..94712cd5 --- /dev/null +++ b/internal/modules/expenses/repositories/expense.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + "gorm.io/gorm" +) + +type ExpenseRepository interface { + repository.BaseRepository[entity.Expense] +} + +type ExpenseRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.Expense] +} + +func NewExpenseRepository(db *gorm.DB) ExpenseRepository { + return &ExpenseRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.Expense](db), + } +} diff --git a/internal/modules/expenses/route.go b/internal/modules/expenses/route.go new file mode 100644 index 00000000..49a4e7c5 --- /dev/null +++ b/internal/modules/expenses/route.go @@ -0,0 +1,28 @@ +package expenses + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/controllers" + expense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService) { + ctrl := controller.NewExpenseController(s) + + route := v1.Group("/expenses") + + // route.Get("/", m.Auth(u), ctrl.GetAll) + // route.Post("/", m.Auth(u), ctrl.CreateOne) + // route.Get("/:id", m.Auth(u), ctrl.GetOne) + // route.Patch("/:id", m.Auth(u), ctrl.UpdateOne) + // route.Delete("/:id", m.Auth(u), ctrl.DeleteOne) + + route.Get("/", ctrl.GetAll) + route.Post("/", ctrl.CreateOne) + route.Get("/:id", ctrl.GetOne) + route.Patch("/:id", ctrl.UpdateOne) + route.Delete("/:id", ctrl.DeleteOne) +} diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go new file mode 100644 index 00000000..1b57263e --- /dev/null +++ b/internal/modules/expenses/services/expense.service.go @@ -0,0 +1,129 @@ +package service + +import ( + "errors" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "github.com/sirupsen/logrus" + "gorm.io/gorm" +) + +type ExpenseService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.Expense, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*entity.Expense, error) + CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.Expense, error) + UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.Expense, error) + DeleteOne(ctx *fiber.Ctx, id uint) error +} + +type expenseService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ExpenseRepository +} + +func NewExpenseService(repo repository.ExpenseRepository, validate *validator.Validate) ExpenseService { + return &expenseService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + } +} + +func (s expenseService) withRelations(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser") +} + +func (s expenseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Expense, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + offset := (params.Page - 1) * params.Limit + + expenses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { + db = s.withRelations(db) + if params.Search != "" { + return db.Where("name LIKE ?", "%"+params.Search+"%") + } + return db.Order("created_at DESC").Order("updated_at DESC") + }) + + if err != nil { + s.Log.Errorf("Failed to get expenses: %+v", err) + return nil, 0, err + } + return expenses, total, nil +} + +func (s expenseService) GetOne(c *fiber.Ctx, id uint) (*entity.Expense, error) { + expense, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + if err != nil { + s.Log.Errorf("Failed get expense by id: %+v", err) + return nil, err + } + return expense, nil +} + +func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Expense, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + createBody := &entity.Expense{ + Name: req.Name, + } + + if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create expense: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Expense, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + updateBody := make(map[string]any) + + if req.Name != nil { + updateBody["name"] = *req.Name + } + + if len(updateBody) == 0 { + return s.GetOne(c, id) + } + + if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + s.Log.Errorf("Failed to update expense: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error { + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Expense not found") + } + s.Log.Errorf("Failed to delete expense: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/expenses/validations/expense.validation.go b/internal/modules/expenses/validations/expense.validation.go new file mode 100644 index 00000000..7d16d3ee --- /dev/null +++ b/internal/modules/expenses/validations/expense.validation.go @@ -0,0 +1,15 @@ +package validation + +type Create struct { + Name string `json:"name" validate:"required_strict,min=3"` +} + +type Update struct { + Name *string `json:"name,omitempty" validate:"omitempty"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + Search string `query:"search" validate:"omitempty,max=50"` +} diff --git a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go index c6ac5931..f88a6ca3 100644 --- a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go +++ b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go @@ -129,7 +129,7 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT } } - if &e.Warehouse.Area != nil && e.Warehouse.Area.Id != 0 { + if e.Warehouse.Area.Id != 0 { warehouse.Area = &AreaBaseDTO{ Id: e.Warehouse.Area.Id, Name: e.Warehouse.Area.Name, diff --git a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go index 03a0d59a..86bd5f84 100644 --- a/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go +++ b/internal/modules/marketing/sales-orders/dto/sales-orders.dto.go @@ -48,14 +48,18 @@ func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO { } } -func ToSalesOrdersListDTO(e entity.SalesOrders) SalesOrdersListDTO { +func ToSalesOrdersListDTO(e entity.Marketing) SalesOrdersListDTO { + products := make([]MarketingProductDTO, len(e.Products)) + for i, p := range e.Products { + products[i] = ToMarketingProductDTO(p) + } return SalesOrdersListDTO{ Id: e.Id, - SoNumber: e.Name, - SoDate: time.Time{}, - Notes: "", - SalesOrder: []MarketingProductDTO{}, + SoNumber: e.SoNumber, + SoDate: e.SoDate, + Notes: e.Notes, + SalesOrder: products, } } diff --git a/internal/modules/marketing/sales-orders/module.go b/internal/modules/marketing/sales-orders/module.go index 6d1963af..0d9583d0 100644 --- a/internal/modules/marketing/sales-orders/module.go +++ b/internal/modules/marketing/sales-orders/module.go @@ -21,11 +21,10 @@ import ( type SalesOrdersModule struct{} func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { - salesOrdersRepo := rSalesOrders.NewSalesOrdersRepository(db) + marketingRepo := rSalesOrders.NewMarketingRepository(db) userRepo := rUser.NewUserRepository(db) customerRepo := rCustomer.NewCustomerRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) - marketingRepo := rSalesOrders.NewMarketingRepository(db) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db)) @@ -33,7 +32,7 @@ func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valida panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err)) } - salesOrdersService := sSalesOrders.NewSalesOrdersService(salesOrdersRepo, customerRepo, productWarehouseRepo, marketingRepo, userRepo, approvalSvc, validate) + salesOrdersService := sSalesOrders.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, validate) userService := sUser.NewUserService(userRepo, validate) SalesOrdersRoutes(router, userService, salesOrdersService) diff --git a/internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go b/internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go deleted file mode 100644 index 5f8cfe79..00000000 --- a/internal/modules/marketing/sales-orders/repositories/sales-orders.repository.go +++ /dev/null @@ -1,21 +0,0 @@ -package repository - -import ( - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - "gitlab.com/mbugroup/lti-api.git/internal/common/repository" - "gorm.io/gorm" -) - -type SalesOrdersRepository interface { - repository.BaseRepository[entity.SalesOrders] -} - -type SalesOrdersRepositoryImpl struct { - *repository.BaseRepositoryImpl[entity.SalesOrders] -} - -func NewSalesOrdersRepository(db *gorm.DB) SalesOrdersRepository { - return &SalesOrdersRepositoryImpl{ - BaseRepositoryImpl: repository.NewBaseRepository[entity.SalesOrders](db), - } -} diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 0dc47a2c..62f694f3 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -34,22 +34,20 @@ type SalesOrdersService interface { type salesOrdersService struct { Log *logrus.Logger Validate *validator.Validate - Repository repository.SalesOrdersRepository + MarketingRepo repository.MarketingRepository CustomerRepo customerRepo.CustomerRepository ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository - MarketingRepo repository.MarketingRepository UserRepo userRepo.UserRepository ApprovalSvc commonSvc.ApprovalService } -func NewSalesOrdersService(repo repository.SalesOrdersRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, marketingRepo repository.MarketingRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService { +func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService { return &salesOrdersService{ Log: utils.Log, Validate: validate, - Repository: repo, + MarketingRepo: marketingRepo, CustomerRepo: customerRepo, ProductWarehouseRepo: productWarehouseRepo, - MarketingRepo: marketingRepo, UserRepo: userRepo, ApprovalSvc: approvalSvc, } @@ -118,7 +116,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e soNumber := fmt.Sprintf("SO-%05d", nextSeq) var marketing *entity.Marketing - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingRepoTx := repository.NewMarketingRepository(dbTransaction) marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) @@ -208,7 +206,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } } - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingRepoTx := repository.NewMarketingRepository(dbTransaction) marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) @@ -366,7 +364,7 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order") } - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction) marketingDeliveryProductRepoTx := repository.NewMarketingDeliveryProductRepository(dbTransaction) diff --git a/internal/route/route.go b/internal/route/route.go index 04adf6e2..7b7059b5 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -17,6 +17,7 @@ import ( purchases "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases" ssoModule "gitlab.com/mbugroup/lti-api.git/internal/modules/sso" users "gitlab.com/mbugroup/lti-api.git/internal/modules/users" + expenses "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses" // MODULE IMPORTS ) @@ -36,6 +37,7 @@ func Routes(app *fiber.App, db *gorm.DB) { purchases.PurchaseModule{}, marketing.MarketingModule{}, ssoModule.Module{}, + expenses.ExpenseModule{}, // MODULE REGISTRY } From 5c25c84f7fdfe7ae9de57b5d1a76d2dcf087c91e Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 17 Nov 2025 15:15:52 +0700 Subject: [PATCH 26/29] Fix[BE]: fixing delivery order delet unused repository --- .../delivery-orders.repository.go | 21 ------------------- 1 file changed, 21 deletions(-) delete mode 100644 internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go diff --git a/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go b/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go deleted file mode 100644 index 0a7d7f3d..00000000 --- a/internal/modules/marketing/delivery-orderss/repositories/delivery-orders.repository.go +++ /dev/null @@ -1,21 +0,0 @@ -package repository - -import ( - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - "gitlab.com/mbugroup/lti-api.git/internal/common/repository" - "gorm.io/gorm" -) - -type DeliveryOrdersRepository interface { - repository.BaseRepository[entity.DeliveryOrders] -} - -type DeliveryOrdersRepositoryImpl struct { - *repository.BaseRepositoryImpl[entity.DeliveryOrders] -} - -func NewDeliveryOrdersRepository(db *gorm.DB) DeliveryOrdersRepository { - return &DeliveryOrdersRepositoryImpl{ - BaseRepositoryImpl: repository.NewBaseRepository[entity.DeliveryOrders](db), - } -} From 1dac74e25bb4dcaed7ab361ad85bed12f3a801c6 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 18 Nov 2025 08:18:37 +0700 Subject: [PATCH 27/29] Feat[BE-261]: creating Entity and repository for each table expenses --- internal/entities/expense.go | 26 +++++++--- internal/entities/expense_nonstock.go | 27 ++++++++++ internal/entities/expense_realization.go | 25 +++++++++ internal/modules/expenses/dto/expense.dto.go | 52 ++++++++++--------- .../repositories/expense.repository.go | 9 +++- .../expense_nonstock.repository.go | 28 ++++++++++ .../expense_realization.repository.go | 28 ++++++++++ .../expenses/services/expense.service.go | 14 +++-- .../validations/expense.validation.go | 6 ++- internal/utils/constant.go | 17 ++++++ 10 files changed, 193 insertions(+), 39 deletions(-) create mode 100644 internal/entities/expense_nonstock.go create mode 100644 internal/entities/expense_realization.go create mode 100644 internal/modules/expenses/repositories/expense_nonstock.repository.go create mode 100644 internal/modules/expenses/repositories/expense_realization.repository.go diff --git a/internal/entities/expense.go b/internal/entities/expense.go index a427582d..286eaf51 100644 --- a/internal/entities/expense.go +++ b/internal/entities/expense.go @@ -1,18 +1,30 @@ package entities import ( + "database/sql" "time" "gorm.io/gorm" ) type Expense struct { - Id uint `gorm:"primaryKey"` - Name string `gorm:"not null;uniqueIndex:idx_name,where:deleted_at IS NULL"` - CreatedBy uint `gorm:"not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Id uint64 `gorm:"primaryKey;autoIncrement"` + ReferenceNumber *string `gorm:"type:varchar(50)"` + SupplierId *uint64 `gorm:""` + Category string `gorm:"type:varchar(50);not null"` + PoNumber string `gorm:"uniqueIndex;not null;type:varchar(50)"` + DocumentPath sql.NullString `gorm:"type:json"` + ExpenseDate time.Time `gorm:"type:date;not null"` + GrandTotal float64 `gorm:"type:numeric(15,3);default:0"` + Note *string `gorm:"type:text"` + CreatedBy *uint64 `gorm:""` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + // Relations + Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` + Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"` + LatestApproval *Approval `gorm:"-" json:"latest_approval,omitempty"` } diff --git a/internal/entities/expense_nonstock.go b/internal/entities/expense_nonstock.go new file mode 100644 index 00000000..eb27efef --- /dev/null +++ b/internal/entities/expense_nonstock.go @@ -0,0 +1,27 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type ExpenseNonstock struct { + Id uint64 `gorm:"primaryKey;autoIncrement"` + ExpenseId *uint64 `gorm:""` + ProjectFlockKandangId *uint64 `gorm:""` + NonstockId *uint64 `gorm:""` + Qty float64 `gorm:"type:numeric(15,3);not null"` + UnitPrice float64 `gorm:"type:numeric(15,3);not null"` + TotalPrice float64 `gorm:"type:numeric(15,3);not null"` + Note *string `gorm:"type:text"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + // Relations + Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"` + ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` + Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"` + Realizations []ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"` +} diff --git a/internal/entities/expense_realization.go b/internal/entities/expense_realization.go new file mode 100644 index 00000000..45a87602 --- /dev/null +++ b/internal/entities/expense_realization.go @@ -0,0 +1,25 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type ExpenseRealization struct { + Id uint64 `gorm:"primaryKey;autoIncrement"` + ExpenseNonstockId *uint64 `gorm:""` + RealizationQty float64 `gorm:"type:numeric(15,3);not null"` + RealizationUnitPrice float64 `gorm:"type:numeric(15,3);not null"` + RealizationTotalPrice float64 `gorm:"type:numeric(15,3);not null"` + RealizationDate time.Time `gorm:"type:date;not null"` + Note *string `gorm:"type:text"` + CreatedBy *uint64 `gorm:""` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + // Relations + ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/modules/expenses/dto/expense.dto.go b/internal/modules/expenses/dto/expense.dto.go index b7bd1b5f..b5fca80f 100644 --- a/internal/modules/expenses/dto/expense.dto.go +++ b/internal/modules/expenses/dto/expense.dto.go @@ -10,44 +10,52 @@ import ( // === DTO Structs === type ExpenseBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` + Id uint64 `json:"id"` + PoNumber string `json:"po_number"` + ExpenseDate time.Time `json:"expense_date"` + GrandTotal float64 `json:"grand_total"` } type ExpenseListDTO struct { - Id uint `json:"id"` - Name string `json:"name"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -type ExpenseDetailDTO struct { - ExpenseListDTO + Id uint64 `json:"id"` + ReferenceNumber string `json:"reference_number"` + PoNumber string `json:"po_number"` + Category string `json:"category"` + ExpenseDate time.Time `json:"expense_date"` + GrandTotal float64 `json:"grand_total"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } // === Mapper Functions === func ToExpenseBaseDTO(e entity.Expense) ExpenseBaseDTO { return ExpenseBaseDTO{ - Id: e.Id, - Name: e.Name, + Id: e.Id, + PoNumber: e.PoNumber, + ExpenseDate: e.ExpenseDate, + GrandTotal: e.GrandTotal, } } func ToExpenseListDTO(e entity.Expense) ExpenseListDTO { var createdUser *userDTO.UserBaseDTO if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) createdUser = &mapped } return ExpenseListDTO{ - Id: e.Id, - Name: e.Name, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + Id: e.Id, + ReferenceNumber: *e.ReferenceNumber, + PoNumber: e.PoNumber, + Category: e.Category, + ExpenseDate: e.ExpenseDate, + GrandTotal: e.GrandTotal, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, } } @@ -58,9 +66,3 @@ func ToExpenseListDTOs(e []entity.Expense) []ExpenseListDTO { } return result } - -func ToExpenseDetailDTO(e entity.Expense) ExpenseDetailDTO { - return ExpenseDetailDTO{ - ExpenseListDTO: ToExpenseListDTO(e), - } -} diff --git a/internal/modules/expenses/repositories/expense.repository.go b/internal/modules/expenses/repositories/expense.repository.go index 94712cd5..8cc580be 100644 --- a/internal/modules/expenses/repositories/expense.repository.go +++ b/internal/modules/expenses/repositories/expense.repository.go @@ -1,13 +1,16 @@ package repository import ( - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "context" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" ) type ExpenseRepository interface { repository.BaseRepository[entity.Expense] + IdExists(ctx context.Context, id uint64) (bool, error) } type ExpenseRepositoryImpl struct { @@ -19,3 +22,7 @@ func NewExpenseRepository(db *gorm.DB) ExpenseRepository { BaseRepositoryImpl: repository.NewBaseRepository[entity.Expense](db), } } + +func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) { + return repository.Exists[entity.Expense](ctx, r.DB(), uint(id)) +} diff --git a/internal/modules/expenses/repositories/expense_nonstock.repository.go b/internal/modules/expenses/repositories/expense_nonstock.repository.go new file mode 100644 index 00000000..257a3034 --- /dev/null +++ b/internal/modules/expenses/repositories/expense_nonstock.repository.go @@ -0,0 +1,28 @@ +package repository + +import ( + "context" + + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type ExpenseNonstockRepository interface { + repository.BaseRepository[entity.ExpenseNonstock] + IdExists(ctx context.Context, id uint64) (bool, error) +} + +type ExpenseNonstockRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.ExpenseNonstock] +} + +func NewExpenseNonstockRepository(db *gorm.DB) ExpenseNonstockRepository { + return &ExpenseNonstockRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ExpenseNonstock](db), + } +} + +func (r *ExpenseNonstockRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) { + return repository.Exists[entity.ExpenseNonstock](ctx, r.DB(), uint(id)) +} diff --git a/internal/modules/expenses/repositories/expense_realization.repository.go b/internal/modules/expenses/repositories/expense_realization.repository.go new file mode 100644 index 00000000..0245f8a2 --- /dev/null +++ b/internal/modules/expenses/repositories/expense_realization.repository.go @@ -0,0 +1,28 @@ +package repository + +import ( + "context" + + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type ExpenseRealizationRepository interface { + repository.BaseRepository[entity.ExpenseRealization] + IdExists(ctx context.Context, id uint64) (bool, error) +} + +type ExpenseRealizationRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.ExpenseRealization] +} + +func NewExpenseRealizationRepository(db *gorm.DB) ExpenseRealizationRepository { + return &ExpenseRealizationRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ExpenseRealization](db), + } +} + +func (r *ExpenseRealizationRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) { + return repository.Exists[entity.ExpenseRealization](ctx, r.DB(), uint(id)) +} diff --git a/internal/modules/expenses/services/expense.service.go b/internal/modules/expenses/services/expense.service.go index 1b57263e..6f794e54 100644 --- a/internal/modules/expenses/services/expense.service.go +++ b/internal/modules/expenses/services/expense.service.go @@ -79,8 +79,11 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, err } + createdBy := uint64(1) createBody := &entity.Expense{ - Name: req.Name, + PoNumber: req.PoNumber, + Category: req.Category, + CreatedBy: &createdBy, } if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { @@ -88,7 +91,7 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, err } - return s.GetOne(c, createBody.Id) + return s.GetOne(c, uint(createBody.Id)) } func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.Expense, error) { @@ -98,8 +101,11 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) updateBody := make(map[string]any) - if req.Name != nil { - updateBody["name"] = *req.Name + if req.PoNumber != nil { + updateBody["po_number"] = *req.PoNumber + } + if req.Category != nil { + updateBody["category"] = *req.Category } if len(updateBody) == 0 { diff --git a/internal/modules/expenses/validations/expense.validation.go b/internal/modules/expenses/validations/expense.validation.go index 7d16d3ee..8155d76f 100644 --- a/internal/modules/expenses/validations/expense.validation.go +++ b/internal/modules/expenses/validations/expense.validation.go @@ -1,11 +1,13 @@ package validation type Create struct { - Name string `json:"name" validate:"required_strict,min=3"` + PoNumber string `json:"po_number" validate:"required,max=50"` + Category string `json:"category" validate:"required,max=50"` } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + PoNumber *string `json:"po_number,omitempty" validate:"omitempty,max=50"` + Category *string `json:"category,omitempty" validate:"omitempty,max=50"` } type Query struct { diff --git a/internal/utils/constant.go b/internal/utils/constant.go index fc01a231..7b219a8e 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -241,6 +241,23 @@ var MarketingApprovalSteps = map[approvalutils.ApprovalStep]string{ MarketingDeliveryOrder: "Delivery Order", } +// ------------------------------------------------------------------- +// Expense Approval +// ------------------------------------------------------------------- + +const ( + ApprovalWorkflowExpense approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("EXPENSES") + ExpenseStepPengajuan approvalutils.ApprovalStep = 1 + ExpenseStepManager approvalutils.ApprovalStep = 2 + ExpenseStepFinance approvalutils.ApprovalStep = 3 +) + +var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{ + ExpenseStepPengajuan: "Pengajuan", + ExpenseStepManager: "Manager", + ExpenseStepFinance: "Finance", +} + // ------------------------------------------------------------------- // Validators // ------------------------------------------------------------------- From 320f5e65c6bf63eb74f4a081e1e2189395642db3 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 18 Nov 2025 11:32:41 +0700 Subject: [PATCH 28/29] Fix[BE]: fix wrong approval step when SO uppdated --- .../sales-orders/services/sales-orders.service.go | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index 62f694f3..df25a70e 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -291,32 +291,30 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } } } else { - // Create new marketing product (use helper) if err := s.createMarketingProductWithDelivery(c.Context(), id, rp, marketingProductRepoTx, invDeliveryRepoTx); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product") } } } - // 2) Delete missing old products (prevent deletion if deliveries exist) for _, old := range oldProducts { if _, ok := reqByPW[old.ProductWarehouseId]; !ok { - // Check delivery product for this marketing product + deliveryProduct, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusInternalServerError, "Failed to check existing delivery product") } if err == nil { - // If delivery exists (delivery_date not nil or qty > 0), prevent deletion + if deliveryProduct.DeliveryDate != nil || deliveryProduct.Qty > 0 { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot delete marketing product %d because it has delivery records", old.Id)) } - // safe to delete delivery product record + if err := invDeliveryRepoTx.DeleteOne(c.Context(), deliveryProduct.Id); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing delivery product") } } - // Delete marketing product + if err := marketingProductRepoTx.DeleteOne(c.Context(), old.Id); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete marketing product") } @@ -327,7 +325,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u if latestApproval != nil && latestApproval.StepNumber == 2 { actorID := uint(1) // todo: ambil dari auth context resetNote := "" - action := entity.ApprovalActionApproved + action := entity.ApprovalActionUpdated _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowMarketing, From a57ef82ebb9982b1ead45c28cf932f5974aef8d4 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 18 Nov 2025 12:44:19 +0700 Subject: [PATCH 29/29] Fix{BE-222] fixing approval status when updated and delete timestamz on children of marketing table --- ...ng_product_and_marketing_delivery.down.sql | 11 ++++++++ ...ting_product_and_marketing_delivery.up.sql | 27 +++++++++++++++++++ .../entities/marketing_delivery_product.go | 23 +++++++--------- internal/entities/marketing_product.go | 25 ++++++----------- .../services/sales-orders.service.go | 17 ++++++------ 5 files changed, 63 insertions(+), 40 deletions(-) create mode 100644 internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.down.sql create mode 100644 internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.up.sql diff --git a/internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.down.sql b/internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.down.sql new file mode 100644 index 00000000..4da2ec3e --- /dev/null +++ b/internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.down.sql @@ -0,0 +1,11 @@ +-- Add back timestamp columns to marketing_products table +ALTER TABLE marketing_products +ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(), +ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(), +ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; + +-- Add back timestamp columns to marketing_delivery_products table +ALTER TABLE marketing_delivery_products +ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(), +ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(), +ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; \ No newline at end of file diff --git a/internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.up.sql b/internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.up.sql new file mode 100644 index 00000000..65750f8d --- /dev/null +++ b/internal/database/migrations/20251118044026_update_marketing_product_and_marketing_delivery.up.sql @@ -0,0 +1,27 @@ +-- Drop timestamp columns from marketing_products table if it exists +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'created_at') THEN + ALTER TABLE marketing_products DROP COLUMN created_at; + END IF; + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'updated_at') THEN + ALTER TABLE marketing_products DROP COLUMN updated_at; + END IF; + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_products' AND column_name = 'deleted_at') THEN + ALTER TABLE marketing_products DROP COLUMN deleted_at; + END IF; +END $$; + +-- Drop timestamp columns from marketing_delivery_products table if it exists +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'created_at') THEN + ALTER TABLE marketing_delivery_products DROP COLUMN created_at; + END IF; + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'updated_at') THEN + ALTER TABLE marketing_delivery_products DROP COLUMN updated_at; + END IF; + IF EXISTS (SELECT 1 FROM information_schema.columns WHERE table_name = 'marketing_delivery_products' AND column_name = 'deleted_at') THEN + ALTER TABLE marketing_delivery_products DROP COLUMN deleted_at; + END IF; +END $$; \ No newline at end of file diff --git a/internal/entities/marketing_delivery_product.go b/internal/entities/marketing_delivery_product.go index 253c00b2..47f6e89d 100644 --- a/internal/entities/marketing_delivery_product.go +++ b/internal/entities/marketing_delivery_product.go @@ -2,23 +2,18 @@ package entities import ( "time" - - "gorm.io/gorm" ) type MarketingDeliveryProduct struct { - Id uint `gorm:"primaryKey;autoIncrement"` - MarketingProductId uint `gorm:"uniqueIndex;not null"` - Qty float64 `gorm:"type:numeric(15,3)"` - UnitPrice float64 `gorm:"type:numeric(15,3)"` - TotalWeight float64 `gorm:"type:numeric(15,3)"` - AvgWeight float64 `gorm:"type:numeric(15,3)"` - TotalPrice float64 `gorm:"type:numeric(15,3)"` - DeliveryDate *time.Time `gorm:"type:timestamptz"` - VehicleNumber string `gorm:"type:varchar(50)"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Id uint `gorm:"primaryKey;autoIncrement"` + MarketingProductId uint `gorm:"uniqueIndex;not null"` + Qty float64 `gorm:"type:numeric(15,3)"` + UnitPrice float64 `gorm:"type:numeric(15,3)"` + TotalWeight float64 `gorm:"type:numeric(15,3)"` + AvgWeight float64 `gorm:"type:numeric(15,3)"` + TotalPrice float64 `gorm:"type:numeric(15,3)"` + DeliveryDate *time.Time `gorm:"type:timestamptz"` + VehicleNumber string `gorm:"type:varchar(50)"` MarketingProduct MarketingProduct `gorm:"foreignKey:MarketingProductId;references:Id"` } diff --git a/internal/entities/marketing_product.go b/internal/entities/marketing_product.go index 66524bc6..f2294f10 100644 --- a/internal/entities/marketing_product.go +++ b/internal/entities/marketing_product.go @@ -1,23 +1,14 @@ package entities -import ( - "time" - - "gorm.io/gorm" -) - type MarketingProduct struct { - Id uint `gorm:"primaryKey;autoIncrement"` - MarketingId uint `gorm:"not null"` - ProductWarehouseId uint `gorm:"not null"` - Qty float64 `gorm:"type:numeric(15,3);not null"` - UnitPrice float64 `gorm:"type:numeric(15,3);not null"` - AvgWeight float64 `gorm:"type:numeric(15,3);not null"` - TotalWeight float64 `gorm:"type:numeric(15,3);not null"` - TotalPrice float64 `gorm:"type:numeric(15,3);not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + Id uint `gorm:"primaryKey;autoIncrement"` + MarketingId uint `gorm:"not null"` + ProductWarehouseId uint `gorm:"not null"` + Qty float64 `gorm:"type:numeric(15,3);not null"` + UnitPrice float64 `gorm:"type:numeric(15,3);not null"` + AvgWeight float64 `gorm:"type:numeric(15,3);not null"` + TotalWeight float64 `gorm:"type:numeric(15,3);not null"` + TotalPrice float64 `gorm:"type:numeric(15,3);not null"` Marketing Marketing `gorm:"foreignKey:MarketingId;references:Id"` ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` diff --git a/internal/modules/marketing/sales-orders/services/sales-orders.service.go b/internal/modules/marketing/sales-orders/services/sales-orders.service.go index df25a70e..c1cd8f5f 100644 --- a/internal/modules/marketing/sales-orders/services/sales-orders.service.go +++ b/internal/modules/marketing/sales-orders/services/sales-orders.service.go @@ -270,7 +270,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product") } - // Ensure delivery product exists; if not, create default if _, err := invDeliveryRepoTx.GetByMarketingProductID(c.Context(), old.Id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { mdp := &entity.MarketingDeliveryProduct{ @@ -321,21 +320,21 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u } } } - - if latestApproval != nil && latestApproval.StepNumber == 2 { + if latestApproval != nil { actorID := uint(1) // todo: ambil dari auth context - resetNote := "" action := entity.ApprovalActionUpdated _, err := approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowMarketing, id, - utils.MarketingStepPengajuan, + approvalutils.ApprovalStep(latestApproval.StepNumber), &action, actorID, - &resetNote) + nil) if err != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset approval status") + if !errors.Is(err, gorm.ErrDuplicatedKey) { + return fiber.NewError(fiber.StatusInternalServerError, "Failed to create update approval") + } } } @@ -371,7 +370,7 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { if len(marketing.Products) > 0 { for _, product := range marketing.Products { if err := marketingDeliveryProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { - return db.Where("marketing_product_id = ?", product.Id) + return db.Where("marketing_product_id = ?", product.Id).Unscoped() }); err != nil && err != gorm.ErrRecordNotFound { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products") } @@ -379,7 +378,7 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error { } if err := marketingProductRepoTx.DeleteMany(c.Context(), func(db *gorm.DB) *gorm.DB { - return db.Where("marketing_id = ?", id) + return db.Where("marketing_id = ?", id).Unscoped() }); err != nil && err != gorm.ErrRecordNotFound { return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete sales order products") }