From 69ded31eb17f3e3faf62a038ba6cf89f6d61c6da Mon Sep 17 00:00:00 2001 From: ragilap Date: Thu, 23 Oct 2025 15:23:28 +0700 Subject: [PATCH] feat(BE): recording --- internal/entities/recording.go | 3 +- .../projectflock_kandang.repository.go | 1 - .../controllers/recording.controller.go | 6 +- .../recordings/dto/recording.dto.go | 81 +++++++++- .../modules/production/recordings/module.go | 7 +- .../recordings/services/recording.service.go | 143 ++++++++++++------ .../validations/recording.validation.go | 12 +- 7 files changed, 186 insertions(+), 67 deletions(-) diff --git a/internal/entities/recording.go b/internal/entities/recording.go index f0923439..a3142e1d 100644 --- a/internal/entities/recording.go +++ b/internal/entities/recording.go @@ -11,8 +11,7 @@ type Recording struct { ProjectFlockKandangId uint `gorm:"column:project_flock_id;not null;index"` RecordDatetime time.Time `gorm:"column:record_datetime;not null"` RecordDate *time.Time `gorm:"column:record_date"` - Status int `gorm:"column:status;not null;default:0"` - Ontime bool `gorm:"column:ontime;not null;default:false"` + Ontime int `gorm:"column:ontime;not null;default:0"` Day *int `gorm:"column:day"` TotalDepletion *int `gorm:"column:total_depletion"` CumDepletionRate *float64 `gorm:"column:cum_depletion_rate"` 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 a5ceaf7f..fa6864fa 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -67,7 +67,6 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint Preload("ProjectFlock"). Preload("ProjectFlock.Flock"). Preload("Kandang"). - Preload("CreatedUser"). First(record, id).Error; err != nil { return nil, err } diff --git a/internal/modules/production/recordings/controllers/recording.controller.go b/internal/modules/production/recordings/controllers/recording.controller.go index ac5d7ffe..47f82068 100644 --- a/internal/modules/production/recordings/controllers/recording.controller.go +++ b/internal/modules/production/recordings/controllers/recording.controller.go @@ -71,7 +71,7 @@ func (u *RecordingController) GetOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Get recording successfully", - Data: dto.ToRecordingListDTO(*result), + Data: dto.ToRecordingDetailDTO(*result), }) } @@ -92,7 +92,7 @@ func (u *RecordingController) CreateOne(c *fiber.Ctx) error { Code: fiber.StatusCreated, Status: "success", Message: "Create recording successfully", - Data: dto.ToRecordingListDTO(*result), + Data: dto.ToRecordingDetailDTO(*result), }) } @@ -119,7 +119,7 @@ func (u *RecordingController) UpdateOne(c *fiber.Ctx) error { Code: fiber.StatusOK, Status: "success", Message: "Update recording successfully", - Data: dto.ToRecordingListDTO(*result), + Data: dto.ToRecordingDetailDTO(*result), }) } diff --git a/internal/modules/production/recordings/dto/recording.dto.go b/internal/modules/production/recordings/dto/recording.dto.go index f6b6b1bd..4a6b4818 100644 --- a/internal/modules/production/recordings/dto/recording.dto.go +++ b/internal/modules/production/recordings/dto/recording.dto.go @@ -14,7 +14,6 @@ type RecordingBaseDTO struct { ProjectFlockKandangId uint `json:"project_flock_kandang_id"` RecordDatetime time.Time `json:"record_datetime"` RecordDate *time.Time `json:"record_date,omitempty"` - Status int `json:"status"` Ontime bool `json:"ontime"` Day *int `json:"day,omitempty"` TotalDepletion *int `json:"total_depletion,omitempty"` @@ -37,18 +36,51 @@ type RecordingListDTO struct { type RecordingDetailDTO struct { RecordingListDTO + BodyWeights []RecordingBodyWeightDTO `json:"body_weights"` + Depletions []RecordingDepletionDTO `json:"depletions"` + Stocks []RecordingStockDTO `json:"stocks"` +} + +type RecordingBodyWeightDTO struct { + Weight float64 `json:"weight"` + Qty int `json:"qty"` + Notes *string `json:"notes,omitempty"` +} + +type RecordingDepletionDTO struct { + ProductWarehouseId uint `json:"product_warehouse_id"` + Total int64 `json:"total"` + Notes *string `json:"notes,omitempty"` +} + +type RecordingStockDTO struct { + ProductWarehouseId uint `json:"product_warehouse_id"` + Increase *float64 `json:"increase,omitempty"` + Decrease *float64 `json:"decrease,omitempty"` + UsageAmount *int64 `json:"usage_amount,omitempty"` + Notes *string `json:"notes,omitempty"` } // === Mapper Functions === func ToRecordingBaseDTO(e entity.Recording) RecordingBaseDTO { + recordDate := e.RecordDate + if recordDate == nil { + rd := time.Date( + e.RecordDatetime.Year(), + e.RecordDatetime.Month(), + e.RecordDatetime.Day(), + 0, 0, 0, 0, + e.RecordDatetime.Location(), + ) + recordDate = &rd + } return RecordingBaseDTO{ Id: e.Id, ProjectFlockKandangId: e.ProjectFlockKandangId, RecordDatetime: e.RecordDatetime, - RecordDate: e.RecordDate, - Status: e.Status, - Ontime: e.Ontime, + RecordDate: recordDate, + Ontime: e.Ontime == 1, Day: e.Day, TotalDepletion: e.TotalDepletion, CumDepletionRate: e.CumDepletionRate, @@ -88,5 +120,46 @@ func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO { func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO { return RecordingDetailDTO{ RecordingListDTO: ToRecordingListDTO(e), + BodyWeights: ToRecordingBodyWeightDTOs(e.BodyWeights), + Depletions: ToRecordingDepletionDTOs(e.Depletions), + Stocks: ToRecordingStockDTOs(e.Stocks), } } + +func ToRecordingBodyWeightDTOs(bodyWeights []entity.RecordingBW) []RecordingBodyWeightDTO { + result := make([]RecordingBodyWeightDTO, len(bodyWeights)) + for i, bw := range bodyWeights { + result[i] = RecordingBodyWeightDTO{ + Weight: bw.Weight, + Qty: bw.Qty, + Notes: bw.Notes, + } + } + return result +} + +func ToRecordingDepletionDTOs(depletions []entity.RecordingDepletion) []RecordingDepletionDTO { + result := make([]RecordingDepletionDTO, len(depletions)) + for i, d := range depletions { + result[i] = RecordingDepletionDTO{ + ProductWarehouseId: d.ProductWarehouseId, + Total: d.Total, + Notes: d.Notes, + } + } + return result +} + +func ToRecordingStockDTOs(stocks []entity.RecordingStock) []RecordingStockDTO { + result := make([]RecordingStockDTO, len(stocks)) + for i, s := range stocks { + result[i] = RecordingStockDTO{ + ProductWarehouseId: s.ProductWarehouseId, + Increase: s.Increase, + Decrease: s.Decrease, + UsageAmount: s.UsageAmount, + Notes: s.Notes, + } + } + return result +} diff --git a/internal/modules/production/recordings/module.go b/internal/modules/production/recordings/module.go index 36ae8dd7..91151a9c 100644 --- a/internal/modules/production/recordings/module.go +++ b/internal/modules/production/recordings/module.go @@ -5,6 +5,8 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rProjectFlockKandang "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" @@ -16,11 +18,12 @@ type RecordingModule struct{} func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { recordingRepo := rRecording.NewRecordingRepository(db) + projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) + productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) userRepo := rUser.NewUserRepository(db) - recordingService := sRecording.NewRecordingService(recordingRepo, validate) + recordingService := sRecording.NewRecordingService(recordingRepo, projectFlockKandangRepo, productWarehouseRepo, validate) userService := sUser.NewUserService(userRepo, validate) RecordingRoutes(router, userService, recordingService) } - diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 2a5fdecb..6deea620 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -9,6 +9,8 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" + rProjectFlockKandang "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" "gitlab.com/mbugroup/lti-api.git/internal/utils" @@ -28,16 +30,25 @@ type RecordingService interface { } type recordingService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.RecordingRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.RecordingRepository + ProjectFlockKandangRepo rProjectFlockKandang.ProjectFlockKandangRepository + ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository } -func NewRecordingService(repo repository.RecordingRepository, validate *validator.Validate) RecordingService { +func NewRecordingService( + repo repository.RecordingRepository, + projectFlockKandangRepo rProjectFlockKandang.ProjectFlockKandangRepository, + productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, + validate *validator.Validate, +) RecordingService { return &recordingService{ - Log: utils.Log, - Validate: validate, - Repository: repo, + Log: utils.Log, + Validate: validate, + Repository: repo, + ProjectFlockKandangRepo: projectFlockKandangRepo, + ProductWarehouseRepo: productWarehouseRepo, } } @@ -98,9 +109,16 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent return nil, err } - recordTime, err := time.Parse(time.RFC3339, req.RecordDatetime) - if err != nil { - return nil, fiber.NewError(fiber.StatusBadRequest, "record_datetime must be in RFC3339 format") + if _, err := s.ProjectFlockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock kandang not found") + } + s.Log.Errorf("Failed to get project flock kandang: %+v", err) + return nil, err + } + + if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions); err != nil { + return nil, err } tx := s.Repository.DB().WithContext(c.Context()).Begin() @@ -122,20 +140,22 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent return nil, err } - status := 0 - if req.Status != nil { - status = *req.Status - } - ontime := false - if req.Ontime != nil { - ontime = *req.Ontime - } + currentTime := time.Now().UTC() + recordTime := currentTime + recordDate := time.Date( + recordTime.Year(), + recordTime.Month(), + recordTime.Day(), + 0, 0, 0, 0, + recordTime.Location(), + ) + ontimeFlag := computeOntime(recordTime, currentTime) recording := &entity.Recording{ ProjectFlockKandangId: req.ProjectFlockKandangId, RecordDatetime: recordTime, - Status: status, - Ontime: ontime, + RecordDate: &recordDate, + Ontime: boolToInt(ontimeFlag), Day: &nextDay, CreatedBy: 1, // TODO: replace with authenticated user } @@ -203,33 +223,13 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin return nil, err } - updateBody := make(map[string]any) - - if req.RecordDatetime != nil { - parsed, err := time.Parse(time.RFC3339, *req.RecordDatetime) - if err != nil { - _ = tx.Rollback() - return nil, fiber.NewError(fiber.StatusBadRequest, "record_datetime must be in RFC3339 format") - } - updateBody["record_datetime"] = parsed - recording.RecordDatetime = parsed - } - if req.Status != nil { - updateBody["status"] = *req.Status - recording.Status = *req.Status - } - if req.Ontime != nil { - updateBody["ontime"] = *req.Ontime - recording.Ontime = *req.Ontime - } - - if len(updateBody) > 0 { - if err := tx.Model(&entity.Recording{}).Where("id = ?", id).Updates(updateBody).Error; err != nil { - _ = tx.Rollback() - s.Log.Errorf("Failed to update recording: %+v", err) - return nil, err - } + ontimeValue := boolToInt(computeOntime(recording.RecordDatetime, time.Now().UTC())) + if err := tx.Model(&entity.Recording{}).Where("id = ?", id).Update("ontime", ontimeValue).Error; err != nil { + _ = tx.Rollback() + s.Log.Errorf("Failed to refresh ontime flag: %+v", err) + return nil, err } + recording.Ontime = ontimeValue if req.BodyWeights != nil { if err := s.replaceBodyWeights(tx, recording.Id, req.BodyWeights); err != nil { @@ -239,6 +239,10 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } } if req.Stocks != nil { + if err := s.ensureProductWarehousesExist(c, req.Stocks, nil); err != nil { + _ = tx.Rollback() + return nil, err + } if err := s.replaceStocks(tx, recording.Id, req.Stocks); err != nil { _ = tx.Rollback() s.Log.Errorf("Failed to update stocks: %+v", err) @@ -246,6 +250,10 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin } } if req.Depletions != nil { + if err := s.ensureProductWarehousesExist(c, nil, req.Depletions); err != nil { + _ = tx.Rollback() + return nil, err + } if err := s.replaceDepletions(tx, recording.Id, req.Depletions); err != nil { _ = tx.Rollback() s.Log.Errorf("Failed to update depletions: %+v", err) @@ -280,6 +288,38 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error { // === Persistence Helpers === +func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []validation.Stock, depletions []validation.Depletion) error { + idSet := make(map[uint]struct{}) + + for _, stock := range stocks { + if stock.ProductWarehouseId != 0 { + idSet[stock.ProductWarehouseId] = struct{}{} + } + } + for _, dep := range depletions { + if dep.ProductWarehouseId != 0 { + idSet[dep.ProductWarehouseId] = struct{}{} + } + } + + if len(idSet) == 0 { + return nil + } + + for id := range idSet { + ok, err := s.ProductWarehouseRepo.ExistsByID(c.Context(), id) + if err != nil { + s.Log.Errorf("Failed to validate product warehouse %d: %+v", id, err) + return err + } + if !ok { + return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product warehouse %d not found", id)) + } + } + + return nil +} + func (s *recordingService) generateNextDay(tx *gorm.DB, projectFlockKandangId uint) (int, error) { var days []int if err := tx.Model(&entity.Recording{}). @@ -319,6 +359,17 @@ func nextRecordingDay(days []int) int { return len(normalized) + 1 } +func computeOntime(recordDatetime, reference time.Time) bool { + return !recordDatetime.Before(reference) +} + +func boolToInt(v bool) int { + if v { + return 1 + } + return 0 +} + func (s *recordingService) persistBodyWeights(tx *gorm.DB, recordingID uint, payload []validation.BodyWeight) error { if len(payload) == 0 { return nil diff --git a/internal/modules/production/recordings/validations/recording.validation.go b/internal/modules/production/recordings/validations/recording.validation.go index ae409586..d143de4b 100644 --- a/internal/modules/production/recordings/validations/recording.validation.go +++ b/internal/modules/production/recordings/validations/recording.validation.go @@ -24,21 +24,15 @@ type ( type Create struct { ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` - RecordDatetime string `json:"record_datetime" validate:"required,datetime=2006-01-02T15:04:05Z07:00"` - Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1 2 3"` - Ontime *bool `json:"ontime,omitempty" validate:"omitempty"` BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"` Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"` Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"` } type Update struct { - RecordDatetime *string `json:"record_datetime,omitempty" validate:"omitempty,datetime=2006-01-02T15:04:05Z07:00"` - Status *int `json:"status,omitempty" validate:"omitempty,oneof=0 1 2 3"` - Ontime *bool `json:"ontime,omitempty" validate:"omitempty"` - BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"` - Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"` - Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"` + BodyWeights []BodyWeight `json:"body_weights,omitempty" validate:"omitempty,dive"` + Stocks []Stock `json:"stocks,omitempty" validate:"omitempty,dive"` + Depletions []Depletion `json:"depletions,omitempty" validate:"omitempty,dive"` } type Query struct {