Merge branch 'dev/teguh' into 'feat/BE/Sprint-8'

feat(BE US#386): add standard_fcr column to production_standard_details and update existing API

See merge request mbugroup/lti-api!114
This commit is contained in:
Hafizh A. Y.
2025-12-30 09:36:00 +00:00
15 changed files with 115 additions and 74 deletions
@@ -0,0 +1,3 @@
-- Remove standard_fcr column from production_standard_details table
ALTER TABLE production_standard_details
DROP COLUMN IF EXISTS standard_fcr;
@@ -0,0 +1,3 @@
-- Add standard_fcr column to production_standard_details table
ALTER TABLE production_standard_details
ADD COLUMN standard_fcr NUMERIC(15, 3);
+22 -10
View File
@@ -891,14 +891,14 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error {
WarehouseName string
Quantity float64
}{
{ProductName: "DOC Broiler", WarehouseName: "Gudang Priangan", Quantity: 100},
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Singaparna", Quantity: 200},
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Banten", Quantity: 300},
{ProductName: "DOC Broiler", WarehouseName: "Gudang Singaparna 1", Quantity: 5000},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Singaparna 1", Quantity: 600},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Singaparna 1", Quantity: 80},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Cikaum 1", Quantity: 450},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Cikaum 1", Quantity: 60},
{ProductName: "DOC Broiler", WarehouseName: "Gudang Priangan", Quantity: 0},
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Singaparna", Quantity: 0},
{ProductName: "281 SPECIAL STARTER", WarehouseName: "Gudang Banten", Quantity: 0},
{ProductName: "DOC Broiler", WarehouseName: "Gudang Singaparna 1", Quantity: 0},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Singaparna 1", Quantity: 0},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Singaparna 1", Quantity: 0},
{ProductName: "Telur Konsumsi Baik", WarehouseName: "Gudang Cikaum 1", Quantity: 0},
{ProductName: "Telur Pecah", WarehouseName: "Gudang Cikaum 1", Quantity: 0},
}
for _, seed := range seeds {
@@ -962,12 +962,24 @@ func seedTransferStock(tx *gorm.DB) error {
{
StockTransferId: transfer.Id,
ProductId: 1,
// Quantity: 10,
SourceProductWarehouseID: func() *uint64 { id := uint64(1); return &id }(),
DestProductWarehouseID: func() *uint64 { id := uint64(2); return &id }(),
UsageQty: 10,
PendingQty: 0,
TotalQty: 10,
TotalUsed: 0,
},
{
StockTransferId: transfer.Id,
ProductId: 2,
// Quantity: 5,
SourceProductWarehouseID: func() *uint64 { id := uint64(1); return &id }(),
DestProductWarehouseID: func() *uint64 { id := uint64(2); return &id }(),
UsageQty: 5,
PendingQty: 0,
TotalQty: 5,
TotalUsed: 0,
},
}
for i := range details {
@@ -12,6 +12,7 @@ type ProductionStandardDetail struct {
TargetHenHouseProduction *float64 `gorm:"type:numeric(15,3)"`
TargetEggWeight *float64 `gorm:"type:numeric(15,3)"`
TargetEggMass *float64 `gorm:"type:numeric(15,3)"`
StandardFCR *float64 `gorm:"type:numeric(15,3)"`
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
@@ -117,39 +117,37 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
var createdLogId uint
isProductWarehouseExist, err := s.ProductWarehouseRepo.ProductWarehouseExistByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
if err != nil {
s.Log.Errorf("Failed to check product warehouse existence: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
var projectFlockKandangID *uint
pfk, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(req.WarehouseID))
if err == nil && pfk != nil {
idCopy := uint(pfk.Id)
projectFlockKandangID = &idCopy
}
if !isProductWarehouseExist {
projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.WarehouseID))
if err != nil {
return nil, err
pw, err := s.ProductWarehouseRepo.FindByProductWarehouseAndPfk(
ctx,
uint(req.ProductID),
uint(req.WarehouseID),
projectFlockKandangID,
)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to find product warehouse: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
}
newPW := &entity.ProductWarehouse{
ProductId: uint(req.ProductID),
WarehouseId: uint(req.WarehouseID),
Quantity: 0,
ProjectFlockKandangId: &projectFlockKandangID,
// CreatedBy: 1, // TODO: should Get from auth middleware
ProjectFlockKandangId: projectFlockKandangID,
}
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
s.Log.Errorf("Failed to create product warehouse: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse")
}
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
}
pw, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
ctx,
uint(req.ProductID),
uint(req.WarehouseID),
)
if err != nil {
s.Log.Errorf("Failed to get product warehouse for project flock check: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
pw = newPW
}
if err := common.EnsureProjectFlockNotClosedForProductWarehouses(
@@ -18,6 +18,7 @@ type ProductWarehouseRepository interface {
ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error)
ExistsByID(ctx context.Context, id uint) (bool, error)
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
FindByProductWarehouseAndPfk(ctx context.Context, productID uint, warehouseID uint, projectFlockKandangID *uint) (*entity.ProductWarehouse, error)
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint, db *gorm.DB) (*entity.ProductWarehouse, error)
GetByFlagAndWarehouseID(ctx context.Context, flagName string, warehouseId uint) ([]entity.ProductWarehouse, error)
@@ -107,6 +108,20 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
return &productWarehouse, nil
}
func (r *ProductWarehouseRepositoryImpl) FindByProductWarehouseAndPfk(ctx context.Context, productID uint, warehouseID uint, projectFlockKandangID *uint) (*entity.ProductWarehouse, error) {
var productWarehouse entity.ProductWarehouse
err := r.DB().WithContext(ctx).
Where("product_id = ? AND warehouse_id = ? AND project_flock_kandang_id IS NOT DISTINCT FROM ?", productID, warehouseID, projectFlockKandangID).
First(&productWarehouse).Error
if err != nil {
return nil, err
}
return &productWarehouse, nil
}
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
var productWarehouses []entity.ProductWarehouse
q := r.DB().WithContext(ctx).Model(&entity.ProductWarehouse{}).
@@ -106,23 +106,17 @@ func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entit
}
func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, error) {
s.Log.Infof("Attempting to get StockTransfer with ID: %d", id)
transferPtr, err := s.StockTransferRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
return s.withRelations(db)
})
if err != nil {
s.Log.Errorf("Error getting StockTransfer ID %d: %+v", id, err)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Transfer not found")
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get transfer")
}
if transferPtr != nil {
s.Log.Infof("StockTransfer %d has %d documents", transferPtr.Id, len(transferPtr.Documents))
}
return transferPtr, nil
}
@@ -336,7 +330,9 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Files: documentFiles,
})
if err != nil {
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d", idx+1))
s.Log.WithError(err).Errorf("Failed to upload document for delivery %d (delivery_id: %d, filename: %s)",
idx+1, deliveries[idx].Id, file.Filename)
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to upload document for delivery %d: %v", idx+1, err))
}
}
}
@@ -396,7 +392,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
})
if err != nil {
s.Log.Errorf("Transaction failed in CreateOne: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to process transfer transaction: %v", err))
}
@@ -247,11 +247,15 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
itemDeliveryDate = &parsedDate
}
// Hitung total_weight dan total_price otomatis
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
deliveryProduct.TotalWeight = requestedProduct.TotalWeight
deliveryProduct.TotalPrice = requestedProduct.TotalPrice
deliveryProduct.TotalWeight = totalWeight
deliveryProduct.TotalPrice = totalPrice
deliveryProduct.DeliveryDate = itemDeliveryDate
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
@@ -357,11 +361,15 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
// Hitung total_weight dan total_price otomatis
totalWeight := requestedProduct.Qty * requestedProduct.AvgWeight
totalPrice := requestedProduct.UnitPrice * requestedProduct.Qty
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
deliveryProduct.TotalWeight = requestedProduct.TotalWeight
deliveryProduct.TotalPrice = requestedProduct.TotalPrice
deliveryProduct.TotalWeight = totalWeight
deliveryProduct.TotalPrice = totalPrice
deliveryProduct.DeliveryDate = itemDeliveryDate
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
@@ -75,7 +75,6 @@ func (s salesOrdersService) getOne(c *fiber.Ctx, id uint) (*entity.Marketing, er
return nil, fiber.NewError(fiber.StatusNotFound, "SalesOrders not found")
}
if err != nil {
s.Log.Errorf("Failed get marketing by id: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order")
}
@@ -293,13 +292,17 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
for _, rp := range req.MarketingProducts {
if old, ok := oldByPW[rp.ProductWarehouseId]; ok {
// Hitung total_weight dan total_price otomatis
totalWeight := rp.Qty * rp.AvgWeight
totalPrice := rp.UnitPrice * rp.Qty
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,
"total_weight": totalWeight,
"total_price": totalPrice,
}
if err := marketingProductRepoTx.PatchOne(c.Context(), old.Id, updateBody, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update marketing product")
@@ -589,30 +592,34 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
func (s *salesOrdersService) createMarketingProductWithDelivery(ctx context.Context, marketingId uint, rp validation.CreateMarketingProduct, marketingProductRepo repository.MarketingProductRepository, invDeliveryRepo repository.MarketingDeliveryProductRepository) error {
// Hitung total_weight dan total_price otomatis
totalWeight := rp.Qty * rp.AvgWeight
totalPrice := rp.UnitPrice * rp.Qty
marketingProduct := &entity.MarketingProduct{
MarketingId: marketingId,
ProductWarehouseId: rp.ProductWarehouseId,
Qty: rp.Qty,
UnitPrice: rp.UnitPrice,
AvgWeight: rp.AvgWeight,
TotalWeight: rp.TotalWeight,
TotalPrice: rp.TotalPrice,
TotalWeight: totalWeight,
TotalPrice: totalPrice,
}
if err := marketingProductRepo.CreateOne(ctx, marketingProduct, nil); err != nil {
return err
}
marketingDeliveryProduct := &entity.MarketingDeliveryProduct{
MarketingProductId: marketingProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId,
UnitPrice: 0,
TotalWeight: 0,
AvgWeight: 0,
TotalPrice: 0,
DeliveryDate: nil,
VehicleNumber: rp.VehicleNumber,
UsageQty: 0,
PendingQty: 0,
MarketingProductId: marketingProduct.Id,
ProductWarehouseId: marketingProduct.ProductWarehouseId,
UnitPrice: 0,
TotalWeight: 0,
AvgWeight: 0,
TotalPrice: 0,
DeliveryDate: nil,
VehicleNumber: rp.VehicleNumber,
UsageQty: 0,
PendingQty: 0,
}
if err := invDeliveryRepo.CreateOne(ctx, marketingDeliveryProduct, nil); err != nil {
return err
@@ -5,8 +5,6 @@ type DeliveryProduct struct {
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"`
}
@@ -12,10 +12,8 @@ type CreateMarketingProduct struct {
VehicleNumber string `json:"vehicle_number" validate:"required,min=1,max=50"`
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 {
@@ -33,6 +33,7 @@ type EggProductionStandardDetailDTO struct {
TargetHenHouseProduction *float64 `json:"target_hen_house_production"`
TargetEggWeight *float64 `json:"target_egg_weight"`
TargetEggMass *float64 `json:"target_egg_mass"`
StandardFCR *float64 `json:"standard_fcr"`
}
type WeeklyProductionStandardDTO struct {
@@ -95,6 +96,7 @@ func ToWeeklyProductionStandardDTOWithDetails(growth entity.StandardGrowthDetail
TargetHenHouseProduction: detail.TargetHenHouseProduction,
TargetEggWeight: detail.TargetEggWeight,
TargetEggMass: detail.TargetEggMass,
StandardFCR: detail.StandardFCR,
}
return WeeklyProductionStandardDTO{
@@ -148,6 +150,7 @@ func ToEggProductionStandardDetailDTO(e entity.ProductionStandardDetail) EggProd
TargetHenHouseProduction: e.TargetHenHouseProduction,
TargetEggWeight: e.TargetEggWeight,
TargetEggMass: e.TargetEggMass,
StandardFCR: e.StandardFCR,
}
}
@@ -84,7 +84,6 @@ func (s productionStandardService) GetOne(c *fiber.Ctx, id uint) (*entity.Produc
return nil, fiber.NewError(fiber.StatusNotFound, "ProductionStandard not found")
}
if err != nil {
s.Log.Errorf("Failed get productionStandard by id: %+v", err)
return nil, err
}
return productionStandard, nil
@@ -111,6 +110,7 @@ func (s *productionStandardService) CreateOne(c *fiber.Ctx, req *validation.Crea
var createdStandard *entity.ProductionStandard
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
standardRepoTx := repository.NewProductionStandardRepository(tx)
productionStandardDetailRepoTx := repository.NewProductionStandardDetailRepository(tx)
standardGrowthDetailRepoTx := repository.NewStandardGrowthDetailRepository(tx)
@@ -142,6 +142,7 @@ func (s *productionStandardService) CreateOne(c *fiber.Ctx, req *validation.Crea
TargetHenHouseProduction: detailReq.ProductionStandardDetails.TargetHenHouseProduction,
TargetEggWeight: detailReq.ProductionStandardDetails.TargetEggWeight,
TargetEggMass: detailReq.ProductionStandardDetails.TargetEggMass,
StandardFCR: detailReq.ProductionStandardDetails.StandardFCR,
}
if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil {
@@ -206,7 +207,6 @@ func (s productionStandardService) UpdateOne(c *fiber.Ctx, req *validation.Updat
nameExists, err := s.Repository.NameExists(c.Context(), *req.Name, &id)
if err != nil {
s.Log.Errorf("Failed to check existing production standard: %+v", err)
return err
}
if nameExists {
@@ -255,6 +255,7 @@ func (s productionStandardService) UpdateOne(c *fiber.Ctx, req *validation.Updat
TargetHenHouseProduction: detailReq.ProductionStandardDetails.TargetHenHouseProduction,
TargetEggWeight: detailReq.ProductionStandardDetails.TargetEggWeight,
TargetEggMass: detailReq.ProductionStandardDetails.TargetEggMass,
StandardFCR: detailReq.ProductionStandardDetails.StandardFCR,
}
if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil {
@@ -283,7 +284,6 @@ func (s productionStandardService) UpdateOne(c *fiber.Ctx, req *validation.Updat
})
if err != nil {
s.Log.Errorf("Failed to update production standard: %+v", err)
return nil, err
}
@@ -295,7 +295,6 @@ func (s productionStandardService) DeleteOne(c *fiber.Ctx, id uint) error {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "ProductionStandard not found")
}
s.Log.Errorf("Failed to delete productionStandard: %+v", err)
return err
}
return nil
@@ -5,6 +5,7 @@ type ProductionStandardDetailItem struct {
TargetHenHouseProduction *float64 `json:"target_hen_house_production" validate:"omitempty,gte=0"`
TargetEggWeight *float64 `json:"target_egg_weight" validate:"omitempty,gte=0"`
TargetEggMass *float64 `json:"target_egg_mass" validate:"omitempty,gte=0"`
StandardFCR *float64 `json:"standard_fcr" validate:"omitempty,gte=0"`
}
type StandardGrowthDetailItem struct {
@@ -1,14 +1,14 @@
package validation
type Create struct {
FlockName string `json:"flock_name" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
Category string `json:"category" validate:"required_strict"`
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
ProductionStandardId uint `json:"production_standard_id" validate:"required_strict,number,gt=0"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"`
FlockName string `json:"flock_name" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
Category string `json:"category" validate:"required_strict"`
FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"`
ProductionStandardId uint `json:"production_standard_id" validate:"required_strict,number,gt=0"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"`
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"`
}
type Query struct {