From eaa208f73321d21494f961066bb65f71e2f74f83 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 13 Jan 2026 14:11:53 +0700 Subject: [PATCH 1/6] [FIX/BE-US-281] response recording and add payload record_at only in createOne --- .DS_Store | Bin 6148 -> 8196 bytes .../recordings/dto/recording.dto.go | 419 +++++++++++------- .../repositories/recording.repository.go | 11 +- .../recordings/services/recording.service.go | 63 ++- .../validations/recording.validation.go | 1 + 5 files changed, 342 insertions(+), 152 deletions(-) diff --git a/.DS_Store b/.DS_Store index e39247fdff6549a6304ce8065c332c38da11c1a4..be6f22d7458c6ebeb623d831ffa7f5c4a8127cb7 100644 GIT binary patch delta 715 zcmd5)u}Z^G6uplm=8*<%Q9>52?dD+64-l&8(8+?UAZ-&R_+qq;Lnk|Q%M>0i4qf~J zDH;79!9Nf|)RQ=vWO4PueYw1M?m73|m)Xv&V-pcOxOI1lvXmpTZ@re>Sc%hO?M3fd zNjLe2@~Cs3OjXuSP4$y`1`TOM9=V7sipZxviLMBT0}&1~2E-G>`N|ivjM=3Sb(g3a z(KfP>0=mXN#)(5BzXcES32uZuRhuWPe^gDN3~X7Oy;rmiI{ej^J>8O))@as?r*sY% zqS%S-Xsy&(3fPtb37;;|c0V^8!t}T-ncJO9q3_-4FqDnQhq 0 { - eggs = ToRecordingEggDTOs(e.Eggs) - } + listDTO := toRecordingListDTO(e) return RecordingDetailDTO{ RecordingListDTO: listDTO, - Depletions: ToRecordingDepletionDTOs(e.Depletions), - Stocks: ToRecordingStockDTOs(e.Stocks), - Eggs: eggs, + Warehouse: recordingWarehouseDTO(e), + ProductCategory: recordingProductCategory(e), + Depletions: ToRecordingDepletionDTOs(e.Depletions), + Stocks: ToRecordingStockDTOs(e.Stocks), + Eggs: ToRecordingEggDTOs(e.Eggs), } } @@ -233,11 +160,15 @@ func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO { if s.UsageQty != nil { usageAmount = *s.UsageQty } + var pendingQty float64 + if s.PendingQty != nil { + pendingQty = *s.PendingQty + } result[i] = RecordingStockDTO{ ProductWarehouseId: s.ProductWarehouseId, UsageAmount: usageAmount, - PendingQty: s.PendingQty, + PendingQty: pendingQty, ProductWarehouse: mapProductWarehouseDTO(&s.ProductWarehouse), } } @@ -258,6 +189,184 @@ func ToRecordingEggDTOs(eggs []entity.RecordingEgg) []RecordingEggDTO { return result } +func toRecordingListDTO(e entity.Recording) RecordingListDTO { + relation := toRecordingRelationDTO(e) + + var createdUser *userDTO.UserRelationDTO + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserRelationDTO(*e.CreatedUser) + createdUser = &mapped + } + + return RecordingListDTO{ + RecordingRelationDTO: relation, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func toRecordingRelationDTO(e entity.Recording) RecordingRelationDTO { + latestApproval := defaultRecordingLatestApproval(e) + if e.LatestApproval != nil { + snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval) + latestApproval = snapshot + } + + return RecordingRelationDTO{ + Id: e.Id, + ProjectFlock: toRecordingProjectFlockDTO(e), + RecordDatetime: e.RecordDatetime, + Day: intValue(e.Day), + TotalDepletionQty: floatValue(e.TotalDepletionQty), + CumDepletionRate: floatValue(e.CumDepletionRate), + CumIntake: intValue(e.CumIntake), + FcrValue: floatValue(e.FcrValue), + HenDay: floatValue(e.HenDay), + HenHouse: floatValue(e.HenHouse), + FeedIntake: floatValue(e.FeedIntake), + EggMass: floatValue(e.EggMass), + EggWeight: floatValue(e.EggWeight), + Approval: latestApproval, + } +} + +func toRecordingProjectFlockDTO(e entity.Recording) RecordingProjectFlockDTO { + result := RecordingProjectFlockDTO{ + ProjectFlockKandangId: e.ProjectFlockKandangId, + } + + pfk := e.ProjectFlockKandang + if pfk == nil { + return result + } + + if pfk.ProjectFlock.Id != 0 { + result.FlockName = pfk.ProjectFlock.FlockName + if pfk.ProjectFlock.Category != "" { + result.ProjectFlockCategory = strings.ToUpper(pfk.ProjectFlock.Category) + } + } + + result.Period = pfk.Period + + if pfk.ProjectFlock.ProductionStandard.Id != 0 { + result.ProductionStandart = &RecordingProductionStandardDTO{ + Id: pfk.ProjectFlock.ProductionStandard.Id, + Week: recordingWeekValue(e), + Name: pfk.ProjectFlock.ProductionStandard.Name, + HenDayStd: floatValue(e.StandardHenDay), + HenHouseStd: floatValue(e.StandardHenHouse), + FeedIntakeStd: floatValue(e.StandardFeedIntake), + MaxDepletionStd: floatValue(e.StandardMaxDepletion), + EggMassStd: floatValue(e.StandardEggMass), + EggWeightStd: floatValue(e.StandardEggWeight), + } + } + + if pfk.ProjectFlock.Fcr.Id != 0 || e.StandardFcr != nil { + result.Fcr = &RecordingFcrDTO{ + Id: pfk.ProjectFlock.Fcr.Id, + Name: pfk.ProjectFlock.Fcr.Name, + FcrStd: floatValue(e.StandardFcr), + } + } + + result.TotalChickQty = floatValue(e.TotalChickQty) + + return result +} + +func recordingWeekValue(e entity.Recording) int { + day := intValue(e.Day) + if day <= 0 { + return 0 + } + weekBase := 1 + if isLayingRecording(e) { + weekBase = 18 + } + return ((day - 1) / 7) + weekBase +} + +func isLayingRecording(e entity.Recording) bool { + if e.ProjectFlockKandang == nil { + return false + } + return strings.EqualFold(e.ProjectFlockKandang.ProjectFlock.Category, string(utils.ProjectFlockCategoryLaying)) +} + +func recordingProductCategory(e entity.Recording) string { + if e.ProjectFlockKandang == nil { + return "" + } + project := e.ProjectFlockKandang.ProjectFlock + if project.Id == 0 { + return "" + } + if project.ProductionStandard.Id != 0 && project.ProductionStandard.ProjectCategory != "" { + return strings.ToUpper(project.ProductionStandard.ProjectCategory) + } + if project.Category != "" { + return strings.ToUpper(project.Category) + } + return "" +} + +func recordingWarehouseDTO(e entity.Recording) *RecordingWarehouseDTO { + pw := primaryProductWarehouse(e) + if pw == nil || pw.Warehouse.Id == 0 { + return nil + } + return mapWarehouseDTO(&pw.Warehouse) +} + +func primaryProductWarehouse(e entity.Recording) *entity.ProductWarehouse { + if len(e.Stocks) > 0 { + pw := e.Stocks[0].ProductWarehouse + if pw.Id != 0 { + return &pw + } + } + if len(e.Depletions) > 0 { + pw := e.Depletions[0].ProductWarehouse + if pw.Id != 0 { + return &pw + } + } + if len(e.Eggs) > 0 { + pw := e.Eggs[0].ProductWarehouse + if pw.Id != 0 { + return &pw + } + } + return nil +} + +func mapWarehouseDTO(wh *entity.Warehouse) *RecordingWarehouseDTO { + if wh == nil || wh.Id == 0 { + return nil + } + dto := &RecordingWarehouseDTO{ + Id: wh.Id, + Name: wh.Name, + } + if wh.Area.Id != 0 { + dto.Area = &RecordingAreaDTO{ + Id: wh.Area.Id, + Name: wh.Area.Name, + } + } + if wh.Location != nil && wh.Location.Id != 0 { + dto.Location = &RecordingLocationDTO{ + Id: wh.Location.Id, + Name: wh.Location.Name, + Address: wh.Location.Address, + } + } + return dto +} + func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.ProductWarehouseDTO { if pw == nil { return productWarehouseDTO.ProductWarehouseDTO{} @@ -271,6 +380,20 @@ func mapProductWarehouseDTO(pw *entity.ProductWarehouse) productWarehouseDTO.Pro return *mapped } +func floatValue(value *float64) float64 { + if value == nil { + return 0 + } + return *value +} + +func intValue(value *int) int { + if value == nil { + return 0 + } + return *value +} + func defaultRecordingLatestApproval(e entity.Recording) approvalDTO.ApprovalRelationDTO { result := approvalDTO.ApprovalRelationDTO{} diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 941d4507..dafd92ce 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -64,19 +64,28 @@ func (r *RecordingRepositoryImpl) WithRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). Preload("ProjectFlockKandang"). + Preload("ProjectFlockKandang.Kandang"). Preload("ProjectFlockKandang.ProjectFlock"). + Preload("ProjectFlockKandang.ProjectFlock.ProductionStandard"). + Preload("ProjectFlockKandang.ProjectFlock.Fcr"). Preload("Depletions"). Preload("Depletions.ProductWarehouse"). Preload("Depletions.ProductWarehouse.Product"). Preload("Depletions.ProductWarehouse.Warehouse"). + Preload("Depletions.ProductWarehouse.Warehouse.Area"). + Preload("Depletions.ProductWarehouse.Warehouse.Location"). Preload("Stocks"). Preload("Stocks.ProductWarehouse"). Preload("Stocks.ProductWarehouse.Product"). Preload("Stocks.ProductWarehouse.Warehouse"). + Preload("Stocks.ProductWarehouse.Warehouse.Area"). + Preload("Stocks.ProductWarehouse.Warehouse.Location"). Preload("Eggs"). Preload("Eggs.ProductWarehouse"). Preload("Eggs.ProductWarehouse.Product"). - Preload("Eggs.ProductWarehouse.Warehouse") + Preload("Eggs.ProductWarehouse.Warehouse"). + Preload("Eggs.ProductWarehouse.Warehouse.Area"). + Preload("Eggs.ProductWarehouse.Warehouse.Location") } func (r *RecordingRepositoryImpl) GetLatestByProjectFlockKandangID(ctx context.Context, projectFlockKandangId uint) (*entity.Recording, error) { diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 819552dc..18c00966 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -169,6 +169,14 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent } ctx := c.Context() + recordTime := time.Now().UTC() + if req.RecordDate != nil && strings.TrimSpace(*req.RecordDate) != "" { + parsed, err := time.Parse("2006-01-02", strings.TrimSpace(*req.RecordDate)) + if err != nil { + return nil, fiber.NewError(fiber.StatusBadRequest, "record_date must be in YYYY-MM-DD format") + } + recordTime = parsed.UTC() + } pfk, err := s.ProjectFlockKandangRepo.GetByID(ctx, req.ProjectFlockKandangId) if err != nil { @@ -188,6 +196,9 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent if err := s.ensureChickInExists(ctx, pfk.Id); err != nil { return nil, err } + if err := s.ensureProductionStandardWeekStart(ctx, pfk); err != nil { + return nil, err + } if !isLaying && len(req.Eggs) > 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "Egg details permitted only for laying project flocks") @@ -211,7 +222,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent return err } - recordTime := time.Now().UTC() existsToday, err := s.Repository.ExistsOnDate(ctx, req.ProjectFlockKandangId, recordTime) if err != nil { s.Log.Errorf("Failed to verify existing recording on date: %+v", err) @@ -1330,12 +1340,16 @@ func (s *recordingService) attachProductionStandard(ctx context.Context, item *e return nil } - week := ((int(*item.Day) - 1) / 7) + 1 + category := strings.ToUpper(item.ProjectFlockKandang.ProjectFlock.Category) + weekBase := 1 + if category == string(utils.ProjectFlockCategoryLaying) { + weekBase = 18 + } + week := ((int(*item.Day) - 1) / 7) + weekBase if week <= 0 { return nil } - category := strings.ToUpper(item.ProjectFlockKandang.ProjectFlock.Category) db := s.Repository.DB() standardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db) growthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db) @@ -1462,3 +1476,46 @@ func (s *recordingService) ensureChickInExists(ctx context.Context, projectFlock return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording") } + +func (s *recordingService) ensureProductionStandardWeekStart(ctx context.Context, pfk *entity.ProjectFlockKandang) error { + if pfk == nil || pfk.ProjectFlock.Id == 0 { + return nil + } + + standardID := pfk.ProjectFlock.ProductionStandardId + if standardID == 0 { + return nil + } + + category := strings.ToUpper(pfk.ProjectFlock.Category) + switch category { + case string(utils.ProjectFlockCategoryLaying): + detailRepo := rProductionStandard.NewProductionStandardDetailRepository(s.Repository.DB()) + details, err := detailRepo.GetByProductionStandardID(ctx, standardID) + if err != nil { + return err + } + startWeek := 0 + if len(details) > 0 { + startWeek = details[0].Week + } + if startWeek != 18 { + return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock") + } + case string(utils.ProjectFlockCategoryGrowing): + growthRepo := rProductionStandard.NewStandardGrowthDetailRepository(s.Repository.DB()) + details, err := growthRepo.GetByProductionStandardID(ctx, standardID) + if err != nil { + return err + } + startWeek := 0 + if len(details) > 0 { + startWeek = details[0].Week + } + if startWeek != 1 { + return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock") + } + } + + return nil +} diff --git a/internal/modules/production/recordings/validations/recording.validation.go b/internal/modules/production/recordings/validations/recording.validation.go index a1d6aaf7..8b4eab57 100644 --- a/internal/modules/production/recordings/validations/recording.validation.go +++ b/internal/modules/production/recordings/validations/recording.validation.go @@ -21,6 +21,7 @@ type ( type Create struct { ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` + RecordDate *string `json:"record_date,omitempty" validate:"omitempty,datetime=2006-01-02"` Stocks []Stock `json:"stocks" validate:"dive"` Depletions []Depletion `json:"depletions" validate:"dive"` Eggs []Egg `json:"eggs" validate:"omitempty,dive"` From ce7ce778fd62bcd72583ef75a3e19ad6658c4149 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 13 Jan 2026 14:39:59 +0700 Subject: [PATCH 2/6] [FIX/BE-US-281] add response validation if weeks not have production standart --- .../services/production-standard.service.go | 81 +++++++++++++++++++ .../modules/production/recordings/module.go | 13 +++ .../recordings/services/recording.service.go | 58 +++---------- 3 files changed, 107 insertions(+), 45 deletions(-) diff --git a/internal/modules/master/production-standards/services/production-standard.service.go b/internal/modules/master/production-standards/services/production-standard.service.go index e1470170..2ea95cf3 100644 --- a/internal/modules/master/production-standards/services/production-standard.service.go +++ b/internal/modules/master/production-standards/services/production-standard.service.go @@ -1,8 +1,10 @@ package service import ( + "context" "errors" "fmt" + "strings" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" @@ -22,6 +24,8 @@ type ProductionStandardService interface { CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProductionStandard, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductionStandard, error) DeleteOne(ctx *fiber.Ctx, id uint) error + EnsureWeekStart(ctx context.Context, standardID uint, category string) error + EnsureWeekAvailable(ctx context.Context, standardID uint, category string, day int) error } type productionStandardService struct { @@ -299,3 +303,80 @@ func (s productionStandardService) DeleteOne(c *fiber.Ctx, id uint) error { } return nil } + +func (s productionStandardService) EnsureWeekStart(ctx context.Context, standardID uint, category string) error { + if standardID == 0 || strings.TrimSpace(category) == "" { + return nil + } + + switch strings.ToUpper(category) { + case string(utils.ProjectFlockCategoryLaying): + details, err := s.ProductionStandardDetailRepo.GetByProductionStandardID(ctx, standardID) + if err != nil { + return err + } + startWeek := 0 + if len(details) > 0 { + startWeek = details[0].Week + } + if startWeek != 18 { + return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock") + } + case string(utils.ProjectFlockCategoryGrowing): + details, err := s.StandardGrowthDetailRepo.GetByProductionStandardID(ctx, standardID) + if err != nil { + return err + } + startWeek := 0 + if len(details) > 0 { + startWeek = details[0].Week + } + if startWeek != 1 { + return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock") + } + } + + return nil +} + +func (s productionStandardService) EnsureWeekAvailable(ctx context.Context, standardID uint, category string, day int) error { + if standardID == 0 || day <= 0 { + return nil + } + + upperCategory := strings.ToUpper(category) + weekBase := 1 + if upperCategory == string(utils.ProjectFlockCategoryLaying) { + weekBase = 18 + } + week := ((day - 1) / 7) + weekBase + if week <= 0 { + return nil + } + + if upperCategory == string(utils.ProjectFlockCategoryLaying) { + detail, err := s.ProductionStandardDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week)) + } + return err + } + if detail == nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week)) + } + } + + growthDetail, err := s.StandardGrowthDetailRepo.GetByStandardIDAndWeek(ctx, standardID, week) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week)) + } + return err + } + if growthDetail == nil { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Standart production tidak tersedia untuk week %d", week)) + } + + return nil +} diff --git a/internal/modules/production/recordings/module.go b/internal/modules/production/recordings/module.go index a19faa33..91b024ac 100644 --- a/internal/modules/production/recordings/module.go +++ b/internal/modules/production/recordings/module.go @@ -11,6 +11,8 @@ 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" + rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories" + sProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services" @@ -29,6 +31,16 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) stockAllocationRepo := commonRepo.NewStockAllocationRepository(db) + productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db) + productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db) + standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db) + + productionStandardService := sProductionStandard.NewProductionStandardService( + productionStandardRepo, + productionStandardDetailRepo, + standardGrowthDetailRepo, + validate, + ) fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log) if err := fifoService.RegisterUsable(fifo.UsableConfig{ @@ -63,6 +75,7 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate approvalRepo, approvalService, fifoService, + productionStandardService, validate, ) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 18c00966..ccf360b9 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -10,6 +10,7 @@ import ( m "gitlab.com/mbugroup/lti-api.git/internal/middleware" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories" + sProductionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations" @@ -53,6 +54,7 @@ type recordingService struct { ProjectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository ApprovalRepo commonRepo.ApprovalRepository ApprovalSvc commonSvc.ApprovalService + ProductionStandardSvc sProductionStandard.ProductionStandardService FifoSvc commonSvc.FifoService } @@ -64,6 +66,7 @@ func NewRecordingService( approvalRepo commonRepo.ApprovalRepository, approvalSvc commonSvc.ApprovalService, fifoSvc commonSvc.FifoService, + productionStandardSvc sProductionStandard.ProductionStandardService, validate *validator.Validate, ) RecordingService { return &recordingService{ @@ -75,6 +78,7 @@ func NewRecordingService( ProjectFlockPopulationRepo: projectFlockPopulationRepo, ApprovalRepo: approvalRepo, ApprovalSvc: approvalSvc, + ProductionStandardSvc: productionStandardSvc, FifoSvc: fifoSvc, } } @@ -196,8 +200,10 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent if err := s.ensureChickInExists(ctx, pfk.Id); err != nil { return nil, err } - if err := s.ensureProductionStandardWeekStart(ctx, pfk); err != nil { - return nil, err + if s.ProductionStandardSvc != nil { + if err := s.ProductionStandardSvc.EnsureWeekStart(ctx, pfk.ProjectFlock.ProductionStandardId, category); err != nil { + return nil, err + } } if !isLaying && len(req.Eggs) > 0 { @@ -221,6 +227,11 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent s.Log.Errorf("Failed to determine recording day: %+v", err) return err } + if s.ProductionStandardSvc != nil { + if err := s.ProductionStandardSvc.EnsureWeekAvailable(ctx, pfk.ProjectFlock.ProductionStandardId, category, nextDay); err != nil { + return err + } + } existsToday, err := s.Repository.ExistsOnDate(ctx, req.ProjectFlockKandangId, recordTime) if err != nil { @@ -1476,46 +1487,3 @@ func (s *recordingService) ensureChickInExists(ctx context.Context, projectFlock return fiber.NewError(fiber.StatusBadRequest, "Chick in project flock belum disetujui sehingga belum dapat membuat recording") } - -func (s *recordingService) ensureProductionStandardWeekStart(ctx context.Context, pfk *entity.ProjectFlockKandang) error { - if pfk == nil || pfk.ProjectFlock.Id == 0 { - return nil - } - - standardID := pfk.ProjectFlock.ProductionStandardId - if standardID == 0 { - return nil - } - - category := strings.ToUpper(pfk.ProjectFlock.Category) - switch category { - case string(utils.ProjectFlockCategoryLaying): - detailRepo := rProductionStandard.NewProductionStandardDetailRepository(s.Repository.DB()) - details, err := detailRepo.GetByProductionStandardID(ctx, standardID) - if err != nil { - return err - } - startWeek := 0 - if len(details) > 0 { - startWeek = details[0].Week - } - if startWeek != 18 { - return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock") - } - case string(utils.ProjectFlockCategoryGrowing): - growthRepo := rProductionStandard.NewStandardGrowthDetailRepository(s.Repository.DB()) - details, err := growthRepo.GetByProductionStandardID(ctx, standardID) - if err != nil { - return err - } - startWeek := 0 - if len(details) > 0 { - startWeek = details[0].Week - } - if startWeek != 1 { - return fiber.NewError(fiber.StatusBadRequest, "Week tidak sesuai dengan standart kategori project flock") - } - } - - return nil -} From 33e89d65ab0e76234a8c2295fb40dc7168d90f79 Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 13 Jan 2026 15:37:54 +0700 Subject: [PATCH 3/6] [FIX/BE-US-281] changes calculate fcr egg --- .../modules/production/recordings/services/recording.service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index ccf360b9..88ed4cf7 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -1178,7 +1178,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm var fcrValue float64 if usageInGrams > 0 && totalEggWeightGrams > 0 { - fcrValue = totalEggWeightGrams / usageInGrams + fcrValue = usageInGrams / totalEggWeightGrams updates["fcr_value"] = fcrValue recording.FcrValue = &fcrValue } else { From 3012d260ec00a782fa1dff96d3188a94a816e524 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Wed, 14 Jan 2026 11:57:56 +0700 Subject: [PATCH 4/6] FIX[BE]: fixing error on report marketing dto --- .../modules/repports/dto/repportMarketing.dto.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/modules/repports/dto/repportMarketing.dto.go b/internal/modules/repports/dto/repportMarketing.dto.go index 36df7a05..92ee9a77 100644 --- a/internal/modules/repports/dto/repportMarketing.dto.go +++ b/internal/modules/repports/dto/repportMarketing.dto.go @@ -302,6 +302,16 @@ func (p ProductRelationDTOFixed) MarshalJSON() ([]byte, error) { Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers"` } + suppliers := make([]supplierDTO.SupplierRelationDTO, len(p.ProductRelationDTO.Suppliers)) + for i, ps := range p.ProductRelationDTO.Suppliers { + suppliers[i] = supplierDTO.SupplierRelationDTO{ + Id: ps.Id, + Name: ps.Name, + Alias: ps.Alias, + Category: ps.Category, + } + } + return json.Marshal(&Alias{ Id: p.ProductRelationDTO.Id, Name: p.ProductRelationDTO.Name, @@ -310,6 +320,6 @@ func (p ProductRelationDTOFixed) MarshalJSON() ([]byte, error) { Uom: p.ProductRelationDTO.Uom, Flags: p.ProductRelationDTO.Flags, ProductCategory: p.ProductRelationDTO.ProductCategory, - Suppliers: p.ProductRelationDTO.Suppliers, + Suppliers: suppliers, }) } From e12c34db133a8deb086d4a147e01bbf0e71b89dd Mon Sep 17 00:00:00 2001 From: kris Date: Wed, 14 Jan 2026 06:18:47 +0000 Subject: [PATCH 5/6] Update .gitlab-ci.yml file --- .gitlab-ci.yml | 245 ++++++++++++++++--------------------------------- 1 file changed, 81 insertions(+), 164 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index b0e3883e..53f28b3e 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,173 +1,90 @@ stages: - - build - - migrate - deploy - - seed -default: - tags: - - self-hosted-stg - -workflow: - rules: - - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"' - when: always - - when: never - -variables: - DOCKER_BUILDKIT: "1" - - IMAGE_TAG: "staging_${CI_COMMIT_SHORT_SHA}" - IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}" - IMAGE_LATEST: "${CI_REGISTRY_IMAGE}:staging_latest" - - DEPLOY_DIR: "/opt/deploy/stg-lti-api" - COMPOSE_FILE: "docker-compose.yaml" - -# ========================= -# BUILD (AUTO) -# ========================= -build_staging: - stage: build - rules: - - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"' - script: | - set -e - docker info - - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" - - echo "✅ Build image: $IMAGE_NAME" - docker build -t "$IMAGE_NAME" -f Dockerfile . - - echo "✅ Push image: $IMAGE_NAME" - docker push "$IMAGE_NAME" - - echo "✅ Tag latest: $IMAGE_LATEST" - docker tag "$IMAGE_NAME" "$IMAGE_LATEST" - docker push "$IMAGE_LATEST" - - -# ========================= -# MIGRATE (AUTO) -# ========================= -migrate_staging: - stage: migrate - rules: - - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"' - needs: - - job: build_staging - artifacts: false - script: | - set -e - echo "✅ Running migrations (staging) ..." - - cd "$DEPLOY_DIR" - test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1) - test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1) - - # ✅ load env dari server - set -a - . ./.env - set +a - - # ✅ validasi - test -n "$DB_HOST" || (echo "❌ DB_HOST empty" && exit 1) - test -n "$DB_PORT" || (echo "❌ DB_PORT empty" && exit 1) - test -n "$DB_USER" || (echo "❌ DB_USER empty" && exit 1) - test -n "$DB_PASSWORD" || (echo "❌ DB_PASSWORD empty" && exit 1) - test -n "$DB_NAME" || (echo "❌ DB_NAME empty" && exit 1) - - export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE:-disable}" - echo "✅ DATABASE_URL=$DATABASE_URL" - - # ✅ Pastikan postgres & redis ON (sesuaikan nama service compose kamu!) - echo "✅ Ensuring postgres & redis running ..." - docker compose -f "$COMPOSE_FILE" up -d stg-postgres-lti stg-redis-lti || true - - # ✅ Ambil network key dari compose - COMPOSE_NETWORK_KEY="$(docker compose -f "$COMPOSE_FILE" config | awk '/networks:/ {getline; print $1}' | tr -d ':')" - echo "✅ Compose network key: $COMPOSE_NETWORK_KEY" - - # ✅ Cari network name yang dipakai docker - NETWORK_NAME="$(docker network ls --format '{{.Name}}' | grep "_${COMPOSE_NETWORK_KEY}$" | head -n 1)" - test -n "$NETWORK_NAME" || (echo "❌ Cannot find docker network for compose ($COMPOSE_NETWORK_KEY)" && exit 1) - - echo "✅ Docker network detected: $NETWORK_NAME" - - # ✅ Migrations dari repo (CI workspace) - echo "✅ Checking migrations from repo..." - ls -lah "$CI_PROJECT_DIR/internal/database/migrations" - - echo "✅ Running migrations via migrate/migrate container" - set +e - out=$(docker run --rm \ - --network "$NETWORK_NAME" \ - -v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \ - migrate/migrate:v4.15.2 \ - -path=/migrations -database "$DATABASE_URL" up 2>&1) - code=$? - set -e - - echo "$out" - - # ✅ Handle no change dengan benar (tidak false-success) - if echo "$out" | grep -qi "no change"; then - echo "✅ No change (already up to date)" - exit 0 - fi - - if [ $code -ne 0 ]; then - echo "❌ Migration failed with exit code $code" - exit $code - fi - - echo "✅ Migration applied successfully" - - -# ========================= -# DEPLOY (AUTO) -# ========================= -deploy_staging: +deploy-dev: stage: deploy - rules: - - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"' - needs: - - job: migrate_staging - artifacts: false - - job: build_staging - artifacts: false - script: | - set -e - docker info - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" + image: alpine:3.20 + variables: + DEPLOY_APP: "LTI-MBUGROUP" + # Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga + GIT_SUBMODULE_STRATEGY: recursive + GIT_DEPTH: "1" - cd "$DEPLOY_DIR" - test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1) - test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1) + before_script: + - echo "🧰 Installing dependencies..." + - apk update && apk add --no-cache openssh git curl bash - docker compose -f "$COMPOSE_FILE" pull - docker compose -f "$COMPOSE_FILE" up -d --force-recreate - docker image prune -f + # Setup SSH di runner + - mkdir -p ~/.ssh + - echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa + - chmod 600 ~/.ssh/id_rsa + - eval "$(ssh-agent -s)" + - ssh-add ~/.ssh/id_rsa + # Trust host keys (server + gitlab) biar SSH gak nanya interaktif + - ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts + - ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts -# ========================= -# SEED (MANUAL) -# ========================= -seed_staging: - stage: seed - rules: - - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"' - needs: - - job: deploy_staging - artifacts: false - when: manual - allow_failure: false - script: | - set -e - cd "$DEPLOY_DIR" - test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found" && exit 1) - test -f .env || (echo "❌ .env not found" && exit 1) + script: + - echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP" - docker compose -f "$COMPOSE_FILE" pull seed || true - docker compose -f "$COMPOSE_FILE" run --rm seed \ No newline at end of file + - > + if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" " + set -e + + cd /home/devops/docker/deployment/development/lti-api + + # Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS) + git remote set-url origin git@gitlab.com:mbugroup/lti-api.git + + # Pastikan server percaya gitlab.com juga (untuk git fetch via SSH) + mkdir -p ~/.ssh + ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts + + # Fetch/reset pakai SSH + GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development + git reset --hard origin/development + + docker compose restart dev-api-lti || docker compose up -d dev-api-lti + "; then + STATUS='success'; + else + STATUS='failed'; + fi; + + RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}"; + + if [ "$STATUS" = "success" ]; then + COLOR=3066993; + TITLE="✅ Deployment API Succeeded"; + DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully."; + else + COLOR=15158332; + TITLE="❌ Deployment API Failed Gaes"; + DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed."; + fi; + + echo "{ + \"username\": \"CI Bot\", + \"embeds\": [{ + \"title\": \"$TITLE\", + \"description\": \"$DESC\", + \"color\": $COLOR, + \"fields\": [ + {\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true}, + {\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true}, + {\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false}, + {\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false} + ] + }] + }" > payload.json; + + echo "📡 Sending notification to Discord..."; + curl -sS -H "Content-Type: application/json" \ + -d @payload.json "$DISCORD_WEBHOOK_URL"; + + only: + - development + + environment: + name: development \ No newline at end of file From c2a89910fbc832666c32de653bc3056ad1dbd7ca Mon Sep 17 00:00:00 2001 From: Adnan Zahir Date: Wed, 14 Jan 2026 13:46:15 +0700 Subject: [PATCH 6/6] chore: test staging from development --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index da8394f1..5b502da1 100644 --- a/README.md +++ b/README.md @@ -110,4 +110,4 @@ IT Development PT Mitra Berlian Unggas Group ## 📃 License -This project is private. All rights reserved. +> This project is private. All rights reserved.