[FIX/BE-US-281] response recording and add payload record_at only in createOne

This commit is contained in:
ragilap
2026-01-13 14:11:53 +07:00
parent f7b70d4b14
commit eaa208f733
5 changed files with 342 additions and 152 deletions
Vendored
BIN
View File
Binary file not shown.
@@ -14,44 +14,83 @@ import (
// === DTO Structs ===
type RecordingProjectFlockDTO struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
FlockName string `json:"flock_name"`
ProjectFlockCategory string `json:"project_flock_category"`
Period int `json:"period"`
ProductionStandart *RecordingProductionStandardDTO `json:"production_standart,omitempty"`
Fcr *RecordingFcrDTO `json:"fcr,omitempty"`
TotalChickQty float64 `json:"total_chick_qty"`
}
type RecordingProductionStandardDTO struct {
Id uint `json:"id"`
Week int `json:"week"`
Name string `json:"name"`
HenDayStd float64 `json:"hen_day_std"`
HenHouseStd float64 `json:"hen_house_std"`
FeedIntakeStd float64 `json:"feed_intake_std"`
MaxDepletionStd float64 `json:"max_depletion_std"`
EggMassStd float64 `json:"egg_mass_std"`
EggWeightStd float64 `json:"egg_weight_std"`
}
type RecordingFcrDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
FcrStd float64 `json:"fcr_std"`
}
type RecordingAreaDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type RecordingLocationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Address string `json:"address"`
}
type RecordingWarehouseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
Area *RecordingAreaDTO `json:"area,omitempty"`
Location *RecordingLocationDTO `json:"location,omitempty"`
}
type RecordingRelationDTO struct {
Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
RecordDatetime time.Time `json:"record_datetime"`
Day int `json:"day"`
ProjectFlockCategory string `json:"project_flock_category"`
TotalDepletionQty float64 `json:"total_depletion_qty"`
CumDepletionRate float64 `json:"cum_depletion_rate"`
CumIntake int `json:"cum_intake"`
FcrValue float64 `json:"fcr_value"`
TotalChickQty float64 `json:"total_chick_qty"`
HenDay float64 `json:"hen_day"`
HenHouse float64 `json:"hen_house"`
FeedIntake float64 `json:"feed_intake"`
EggMass float64 `json:"egg_mass"`
EggWeight float64 `json:"egg_weight"`
StandardHenDay *float64 `json:"hen_day_std,omitempty"`
StandardHenHouse *float64 `json:"hen_house_std,omitempty"`
StandardFeedIntake *float64 `json:"feed_intake_std,omitempty"`
StandardMaxDepletion *float64 `json:"max_depletion_std,omitempty"`
StandardEggMass *float64 `json:"egg_mass_std,omitempty"`
StandardEggWeight *float64 `json:"egg_weight_std,omitempty"`
StandardFcr *float64 `json:"fcr_std,omitempty"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
Id uint `json:"id"`
ProjectFlock RecordingProjectFlockDTO `json:"project_flock"`
RecordDatetime time.Time `json:"record_datetime"`
Day int `json:"day"`
TotalDepletionQty float64 `json:"total_depletion_qty"`
CumDepletionRate float64 `json:"cum_depletion_rate"`
CumIntake int `json:"cum_intake"`
FcrValue float64 `json:"fcr_value"`
HenDay float64 `json:"hen_day"`
HenHouse float64 `json:"hen_house"`
FeedIntake float64 `json:"feed_intake"`
EggMass float64 `json:"egg_mass"`
EggWeight float64 `json:"egg_weight"`
Approval approvalDTO.ApprovalRelationDTO `json:"approval"`
}
type RecordingListDTO struct {
RecordingRelationDTO
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type RecordingDetailDTO struct {
RecordingListDTO
Depletions []RecordingDepletionDTO `json:"depletions"`
Stocks []RecordingStockDTO `json:"stocks"`
Eggs []RecordingEggDTO `json:"eggs"`
Warehouse *RecordingWarehouseDTO `json:"warehouse,omitempty"`
ProductCategory string `json:"product_category"`
Depletions []RecordingDepletionDTO `json:"depletions"`
Stocks []RecordingStockDTO `json:"stocks"`
Eggs []RecordingEggDTO `json:"eggs"`
}
type RecordingDepletionDTO struct {
@@ -63,7 +102,7 @@ type RecordingDepletionDTO struct {
type RecordingStockDTO struct {
ProductWarehouseId uint `json:"product_warehouse_id"`
UsageAmount float64 `json:"usage_amount"`
PendingQty *float64 `json:"pending_qty,omitempty"`
PendingQty float64 `json:"pending_qty"`
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
}
@@ -75,117 +114,10 @@ type RecordingEggDTO struct {
ProductWarehouse productWarehouseDTO.ProductWarehouseDTO `json:"product_warehouse"`
}
type RecordingProductWarehouseDTO struct {
Id uint `json:"id"`
ProductId uint `json:"product_id"`
ProductName string `json:"product_name"`
WarehouseId uint `json:"warehouse_id"`
WarehouseName string `json:"warehouse_name"`
}
// === Mapper Functions ===
func ToRecordingRelationDTO(e entity.Recording) RecordingRelationDTO {
var (
projectFlockCategory string
day int
totalDepletionQty float64
cumDepletionRate float64
cumIntake int
fcrValue float64
totalChickQty float64
henDay float64
henHouse float64
feedIntake float64
eggMass float64
eggWeight float64
)
if e.Day != nil {
day = *e.Day
}
if e.TotalDepletionQty != nil {
totalDepletionQty = *e.TotalDepletionQty
}
if e.CumDepletionRate != nil {
cumDepletionRate = *e.CumDepletionRate
}
if e.CumIntake != nil {
cumIntake = *e.CumIntake
}
if e.FcrValue != nil {
fcrValue = *e.FcrValue
}
if e.TotalChickQty != nil {
totalChickQty = *e.TotalChickQty
}
if e.HenDay != nil {
henDay = *e.HenDay
}
if e.HenHouse != nil {
henHouse = *e.HenHouse
}
if e.FeedIntake != nil {
feedIntake = *e.FeedIntake
}
if e.EggMass != nil {
eggMass = *e.EggMass
}
if e.EggWeight != nil {
eggWeight = *e.EggWeight
}
if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.ProjectFlock.Id != 0 {
category := e.ProjectFlockKandang.ProjectFlock.Category
projectFlockCategory = category
}
latestApproval := defaultRecordingLatestApproval(e)
if e.LatestApproval != nil {
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
latestApproval = snapshot
}
return RecordingRelationDTO{
Id: e.Id,
ProjectFlockKandangId: e.ProjectFlockKandangId,
RecordDatetime: e.RecordDatetime,
Day: day,
ProjectFlockCategory: projectFlockCategory,
TotalDepletionQty: totalDepletionQty,
CumDepletionRate: cumDepletionRate,
CumIntake: cumIntake,
FcrValue: fcrValue,
TotalChickQty: totalChickQty,
HenDay: henDay,
HenHouse: henHouse,
FeedIntake: feedIntake,
EggMass: eggMass,
EggWeight: eggWeight,
StandardHenDay: e.StandardHenDay,
StandardHenHouse: e.StandardHenHouse,
StandardFeedIntake: e.StandardFeedIntake,
StandardMaxDepletion: e.StandardMaxDepletion,
StandardEggMass: e.StandardEggMass,
StandardEggWeight: e.StandardEggWeight,
StandardFcr: e.StandardFcr,
Approval: latestApproval,
}
}
func ToRecordingListDTO(e entity.Recording) RecordingListDTO {
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(*e.CreatedUser)
createdUser = &mapped
}
return RecordingListDTO{
RecordingRelationDTO: ToRecordingRelationDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
return toRecordingListDTO(e)
}
func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
@@ -197,20 +129,15 @@ func ToRecordingListDTOs(e []entity.Recording) []RecordingListDTO {
}
func ToRecordingDetailDTO(e entity.Recording) RecordingDetailDTO {
listDTO := ToRecordingListDTO(e)
var eggs []RecordingEggDTO
if strings.EqualFold(listDTO.ProjectFlockCategory, string(utils.ProjectFlockCategoryLaying)) {
eggs = ToRecordingEggDTOs(e.Eggs)
} else if len(e.Eggs) > 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{}
@@ -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) {
@@ -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
}
@@ -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"`