feat[BE#US386]: add production standards module with CRUD operations

- Created database migration for production standards and related tables.
- Implemented entities for ProductionStandard, ProductionStandardDetail, and StandardGrowthDetail.
- Developed controller for handling production standard requests.
- Added DTOs for data transfer between layers.
- Implemented service layer for business logic related to production standards.
- Created repository interfaces and implementations for data access.
- Added validation for production standard requests.
- Registered routes for production standards in the main application.
This commit is contained in:
aguhh18
2025-12-27 09:02:16 +07:00
committed by Hafizh A. Y
parent dbb13da7c4
commit bb76d27f25
14 changed files with 1038 additions and 9 deletions
@@ -6,12 +6,25 @@ CREATE TABLE IF NOT EXISTS production_standards (
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ,
created_by BIGINT NOT NULL
created_by BIGINT
);
-- Create index for deleted_at (soft delete)
CREATE INDEX idx_production_standards_deleted_at ON production_standards(deleted_at);
-- Tambahkan Foreign Key ke users
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE production_standards
ADD CONSTRAINT fk_production_standards_created_by
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;
-- Index
CREATE INDEX idx_production_standards_created_by ON production_standards(created_by);
-- Create production_standard_details table
CREATE TABLE IF NOT EXISTS production_standard_details (
id BIGSERIAL PRIMARY KEY,
@@ -22,11 +35,19 @@ CREATE TABLE IF NOT EXISTS production_standard_details (
target_egg_weight NUMERIC(15, 3),
target_egg_mass NUMERIC(15, 3),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
CONSTRAINT fk_production_standard_details_standard FOREIGN KEY (production_standard_id)
REFERENCES production_standards(id) ON DELETE CASCADE
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tambahkan Foreign Key ke production_standards
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
ALTER TABLE production_standard_details
ADD CONSTRAINT fk_production_standard_details_standard
FOREIGN KEY (production_standard_id) REFERENCES production_standards(id) ON DELETE CASCADE;
END IF;
END $$;
-- Create unique constraint for standard_id + week
CREATE UNIQUE INDEX idx_production_standard_details_standard_week
ON production_standard_details(production_standard_id, week);
@@ -35,20 +56,41 @@ CREATE UNIQUE INDEX idx_production_standard_details_standard_week
CREATE TABLE IF NOT EXISTS standard_growth_details (
id BIGSERIAL PRIMARY KEY,
production_standard_id BIGINT NOT NULL,
target_mean_bw INT,
target_mean_bw NUMERIC(15, 3),
max_depletion NUMERIC(15, 3),
min_uniformity NUMERIC(15, 3) NOT NULL,
week INT NOT NULL,
feed_intake INT,
feed_intake NUMERIC(15, 3),
created_at TIMESTAMPTZ DEFAULT NOW(),
created_by BIGINT NOT NULL,
CONSTRAINT fk_standard_growth_details_standard FOREIGN KEY (production_standard_id)
REFERENCES production_standards(id) ON DELETE CASCADE
created_by BIGINT
);
-- Tambahkan Foreign Key ke production_standards
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'production_standards') THEN
ALTER TABLE standard_growth_details
ADD CONSTRAINT fk_standard_growth_details_standard
FOREIGN KEY (production_standard_id) REFERENCES production_standards(id) ON DELETE CASCADE;
END IF;
END $$;
-- Tambahkan Foreign Key ke users
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE standard_growth_details
ADD CONSTRAINT fk_standard_growth_details_created_by
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL ON UPDATE CASCADE;
END IF;
END $$;
-- Create unique constraint for standard_id + week
CREATE UNIQUE INDEX idx_standard_growth_details_standard_week
ON standard_growth_details(production_standard_id, week);
-- Index
CREATE INDEX idx_standard_growth_details_created_by ON standard_growth_details(created_by);
-- Create index for project_category
CREATE INDEX idx_production_standards_project_category ON production_standards(project_category);
+19
View File
@@ -0,0 +1,19 @@
package entities
import (
"time"
)
type ProductionStandard struct {
Id uint `gorm:"primaryKey;autoIncrement"`
Name string `gorm:"type:varchar(100);uniqueIndex;not null"`
ProjectCategory string `gorm:"type:varchar(20);not null"`
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
DeletedAt *time.Time `gorm:"type:timestamptz"`
CreatedBy uint `gorm:"not null"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"`
ProductionStandardDetails []ProductionStandardDetail `gorm:"foreignKey:ProductionStandardId;references:Id"`
StandardGrowthDetails []StandardGrowthDetail `gorm:"foreignKey:ProductionStandardId;references:Id"`
}
@@ -0,0 +1,19 @@
package entities
import (
"time"
)
type ProductionStandardDetail struct {
Id uint `gorm:"primaryKey;autoIncrement"`
ProductionStandardId uint `gorm:"not null"`
Week int `gorm:"not null"`
TargetHenDayProduction *float64 `gorm:"type:numeric(15,3)"`
TargetHenHouseProduction *float64 `gorm:"type:numeric(15,3)"`
TargetEggWeight *float64 `gorm:"type:numeric(15,3)"`
TargetEggMass *float64 `gorm:"type:numeric(15,3)"`
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
UpdatedAt time.Time `gorm:"type:timestamptz;not null"`
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
}
@@ -0,0 +1,19 @@
package entities
import (
"time"
)
type StandardGrowthDetail struct {
Id uint `gorm:"primaryKey;autoIncrement"`
ProductionStandardId uint `gorm:"not null"`
TargetMeanBw *float64 `gorm:"type:numeric(15,3)"`
MaxDepletion *float64 `gorm:"type:numeric(15,3)"`
MinUniformity float64 `gorm:"type:numeric(15,3);not null"`
Week int `gorm:"not null"`
FeedIntake *float64 `gorm:"type:numeric(15,3)"`
CreatedAt time.Time `gorm:"type:timestamptz;not null"`
CreatedBy uint `gorm:"not null"`
ProductionStandard ProductionStandard `gorm:"foreignKey:ProductionStandardId;references:Id"`
}
@@ -0,0 +1,145 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type ProductionStandardController struct {
ProductionStandardService service.ProductionStandardService
}
func NewProductionStandardController(productionStandardService service.ProductionStandardService) *ProductionStandardController {
return &ProductionStandardController{
ProductionStandardService: productionStandardService,
}
}
func (u *ProductionStandardController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
ProjectCategory: c.Query("project_category", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.ProductionStandardService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ProductionStandardListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all productionStandards successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToProductionStandardListDTOs(result),
})
}
func (u *ProductionStandardController) 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.ProductionStandardService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get productionStandard successfully",
Data: dto.ToProductionStandardDetailDTO(*result, result.StandardGrowthDetails, result.ProductionStandardDetails),
})
}
func (u *ProductionStandardController) 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.ProductionStandardService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create productionStandard successfully",
Data: dto.ToProductionStandardDetailDTO(*result, result.StandardGrowthDetails, result.ProductionStandardDetails),
})
}
func (u *ProductionStandardController) 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.ProductionStandardService.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 productionStandard successfully",
Data: dto.ToProductionStandardDetailDTO(*result, result.StandardGrowthDetails, result.ProductionStandardDetails),
})
}
func (u *ProductionStandardController) 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.ProductionStandardService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete productionStandard successfully",
})
}
@@ -0,0 +1,155 @@
package dto
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type ProductionStandardListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
ProjectCategory string `json:"project_category"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
}
type ProductionStandardDetailDTO struct {
ProductionStandardListDTO
Details []WeeklyProductionStandardDTO `json:"details"`
}
type GrowthStandardDetailDTO struct {
Id uint `json:"id"`
TargetMeanBW *float64 `json:"target_mean_bw"`
MaxDepletion *float64 `json:"max_depletion"`
MinUniformity float64 `json:"min_uniformity"`
FeedIntake *float64 `json:"feed_intake"`
}
type EggProductionStandardDetailDTO struct {
Id uint `json:"id"`
TargetHenDayProduction *float64 `json:"target_hen_day_production"`
TargetHenHouseProduction *float64 `json:"target_hen_house_production"`
TargetEggWeight *float64 `json:"target_egg_weight"`
TargetEggMass *float64 `json:"target_egg_mass"`
}
type WeeklyProductionStandardDTO struct {
Week int `json:"week"`
GrowthStandardDetail GrowthStandardDetailDTO `json:"growth_standard_detail"`
EggProductionStandardDetailDTO *EggProductionStandardDetailDTO `json:"egg_production_standard_detail,omitempty"`
}
// === Mapper Functions ===
func ToProductionStandardListDTO(e entity.ProductionStandard) ProductionStandardListDTO {
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
createdUser = &mapped
}
return ProductionStandardListDTO{
Id: e.Id,
Name: e.Name,
ProjectCategory: e.ProjectCategory,
CreatedUser: createdUser,
}
}
func ToProductionStandardListDTOs(e []entity.ProductionStandard) []ProductionStandardListDTO {
result := make([]ProductionStandardListDTO, len(e))
for i, r := range e {
result[i] = ToProductionStandardListDTO(r)
}
return result
}
func ToWeeklyProductionStandardDTO(e entity.StandardGrowthDetail) WeeklyProductionStandardDTO {
return WeeklyProductionStandardDTO{
Week: e.Week,
GrowthStandardDetail: GrowthStandardDetailDTO{
Id: e.Id,
TargetMeanBW: e.TargetMeanBw,
MaxDepletion: e.MaxDepletion,
MinUniformity: e.MinUniformity,
FeedIntake: e.FeedIntake,
},
EggProductionStandardDetailDTO: nil, // GROWING category - no egg production details
}
}
func ToWeeklyProductionStandardDTOWithDetails(growth entity.StandardGrowthDetail, detail entity.ProductionStandardDetail) WeeklyProductionStandardDTO {
eggDetail := &EggProductionStandardDetailDTO{
Id: detail.Id,
TargetHenDayProduction: detail.TargetHenDayProduction,
TargetHenHouseProduction: detail.TargetHenHouseProduction,
TargetEggWeight: detail.TargetEggWeight,
TargetEggMass: detail.TargetEggMass,
}
return WeeklyProductionStandardDTO{
Week: growth.Week,
GrowthStandardDetail: GrowthStandardDetailDTO{
Id: growth.Id,
TargetMeanBW: growth.TargetMeanBw,
MaxDepletion: growth.MaxDepletion,
MinUniformity: growth.MinUniformity,
FeedIntake: growth.FeedIntake,
},
EggProductionStandardDetailDTO: eggDetail, // LAYING category - with egg production details
}
}
func ToWeeklyProductionStandardDTOs(e []entity.StandardGrowthDetail) []WeeklyProductionStandardDTO {
result := make([]WeeklyProductionStandardDTO, len(e))
for i, r := range e {
result[i] = ToWeeklyProductionStandardDTO(r)
}
return result
}
func ToWeeklyProductionStandardDTOsWithDetails(
growthDetails []entity.StandardGrowthDetail,
productionStandardDetails []entity.ProductionStandardDetail,
) []WeeklyProductionStandardDTO {
result := make([]WeeklyProductionStandardDTO, len(growthDetails))
// Create map for production standard details by week
prodDetailMap := make(map[int]entity.ProductionStandardDetail)
for _, detail := range productionStandardDetails {
prodDetailMap[detail.Week] = detail
}
// Map growth details and combine with production standard details
for i, growth := range growthDetails {
if prodDetail, exists := prodDetailMap[growth.Week]; exists {
result[i] = ToWeeklyProductionStandardDTOWithDetails(growth, prodDetail)
} else {
result[i] = ToWeeklyProductionStandardDTO(growth)
}
}
return result
}
func ToEggProductionStandardDetailDTO(e entity.ProductionStandardDetail) EggProductionStandardDetailDTO {
return EggProductionStandardDetailDTO{
TargetHenDayProduction: e.TargetHenDayProduction,
TargetHenHouseProduction: e.TargetHenHouseProduction,
TargetEggWeight: e.TargetEggWeight,
TargetEggMass: e.TargetEggMass,
}
}
func ToProductionStandardDetailDTO(
standard entity.ProductionStandard,
growthDetails []entity.StandardGrowthDetail,
productionStandardDetails []entity.ProductionStandardDetail,
) ProductionStandardDetailDTO {
return ProductionStandardDetailDTO{
ProductionStandardListDTO: ToProductionStandardListDTO(standard),
Details: ToWeeklyProductionStandardDTOsWithDetails(growthDetails, productionStandardDetails),
}
}
@@ -0,0 +1,33 @@
package productionstandards
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
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"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ProductionStandardModule struct{}
func (ProductionStandardModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
productionStandardRepo := rProductionStandard.NewProductionStandardRepository(db)
productionStandardDetailRepo := rProductionStandard.NewProductionStandardDetailRepository(db)
standardGrowthDetailRepo := rProductionStandard.NewStandardGrowthDetailRepository(db)
userRepo := rUser.NewUserRepository(db)
productionStandardService := sProductionStandard.NewProductionStandardService(
productionStandardRepo,
productionStandardDetailRepo,
standardGrowthDetailRepo,
validate,
)
userService := sUser.NewUserService(userRepo, validate)
ProductionStandardRoutes(router, userService, productionStandardService)
}
@@ -0,0 +1,103 @@
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 ProductionStandardRepository interface {
repository.BaseRepository[entity.ProductionStandard]
GetAll(ctx context.Context, offset, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProductionStandard, int64, error)
GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.ProductionStandard, error)
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
IdExists(ctx context.Context, id uint) (bool, error)
GetByProjectCategory(ctx context.Context, projectCategory string) ([]entity.ProductionStandard, error)
}
type ProductionStandardRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProductionStandard]
db *gorm.DB
}
func NewProductionStandardRepository(db *gorm.DB) ProductionStandardRepository {
return &ProductionStandardRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductionStandard](db),
db: db,
}
}
func (r *ProductionStandardRepositoryImpl) GetAll(ctx context.Context, offset, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProductionStandard, int64, error) {
var standards []entity.ProductionStandard
var total int64
// Build base query
q := r.db.WithContext(ctx).Model(&entity.ProductionStandard{})
// Apply modifier for filters
if modifier != nil {
q = modifier(q)
}
// Count total
if err := q.Count(&total).Error; err != nil {
return nil, 0, err
}
// Re-apply modifier and add preloads for Find
q = r.db.WithContext(ctx).Model(&entity.ProductionStandard{})
if modifier != nil {
q = modifier(q)
}
q = q.Preload("CreatedUser")
// Find with offset and limit
if err := q.Offset(offset).Limit(limit).Find(&standards).Error; err != nil {
return nil, 0, err
}
return standards, total, nil
}
func (r *ProductionStandardRepositoryImpl) GetByID(ctx context.Context, id uint, modifier func(*gorm.DB) *gorm.DB) (*entity.ProductionStandard, error) {
var standard entity.ProductionStandard
q := r.db.WithContext(ctx).Model(&entity.ProductionStandard{})
// Apply modifier
if modifier != nil {
q = modifier(q)
}
// Ensure CreatedUser is preloaded
q = q.Preload("CreatedUser")
if err := q.First(&standard, id).Error; err != nil {
return nil, err
}
return &standard, nil
}
func (r *ProductionStandardRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.ProductionStandard](ctx, r.db, name, excludeID)
}
func (r *ProductionStandardRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProductionStandard](ctx, r.db, id)
}
func (r *ProductionStandardRepositoryImpl) GetByProjectCategory(ctx context.Context, projectCategory string) ([]entity.ProductionStandard, error) {
var standards []entity.ProductionStandard
err := r.db.WithContext(ctx).
Preload("CreatedUser").
Where("project_category = ?", projectCategory).
Where("deleted_at IS NULL").
Find(&standards).Error
if err != nil {
return nil, err
}
return standards, nil
}
@@ -0,0 +1,63 @@
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 ProductionStandardDetailRepository interface {
repository.BaseRepository[entity.ProductionStandardDetail]
IdExists(ctx context.Context, id uint) (bool, error)
GetByProductionStandardID(ctx context.Context, productionStandardId uint) ([]entity.ProductionStandardDetail, error)
GetByStandardIDAndWeek(ctx context.Context, standardId uint, week int) (*entity.ProductionStandardDetail, error)
DeleteByProductionStandardID(ctx context.Context, productionStandardId uint) error
}
type ProductionStandardDetailRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProductionStandardDetail]
db *gorm.DB
}
func NewProductionStandardDetailRepository(db *gorm.DB) ProductionStandardDetailRepository {
return &ProductionStandardDetailRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductionStandardDetail](db),
db: db,
}
}
func (r *ProductionStandardDetailRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProductionStandardDetail](ctx, r.db, id)
}
func (r *ProductionStandardDetailRepositoryImpl) GetByProductionStandardID(ctx context.Context, productionStandardId uint) ([]entity.ProductionStandardDetail, error) {
var details []entity.ProductionStandardDetail
err := r.db.WithContext(ctx).
Where("production_standard_id = ?", productionStandardId).
Order("week ASC").
Find(&details).Error
if err != nil {
return nil, err
}
return details, nil
}
func (r *ProductionStandardDetailRepositoryImpl) GetByStandardIDAndWeek(ctx context.Context, standardId uint, week int) (*entity.ProductionStandardDetail, error) {
var detail entity.ProductionStandardDetail
err := r.db.WithContext(ctx).
Where("production_standard_id = ?", standardId).
Where("week = ?", week).
First(&detail).Error
if err != nil {
return nil, err
}
return &detail, nil
}
func (r *ProductionStandardDetailRepositoryImpl) DeleteByProductionStandardID(ctx context.Context, productionStandardId uint) error {
return r.db.WithContext(ctx).
Where("production_standard_id = ?", productionStandardId).
Delete(&entity.ProductionStandardDetail{}).Error
}
@@ -0,0 +1,63 @@
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 StandardGrowthDetailRepository interface {
repository.BaseRepository[entity.StandardGrowthDetail]
IdExists(ctx context.Context, id uint) (bool, error)
GetByProductionStandardID(ctx context.Context, productionStandardId uint) ([]entity.StandardGrowthDetail, error)
GetByStandardIDAndWeek(ctx context.Context, standardId uint, week int) (*entity.StandardGrowthDetail, error)
DeleteByProductionStandardID(ctx context.Context, productionStandardId uint) error
}
type StandardGrowthDetailRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StandardGrowthDetail]
db *gorm.DB
}
func NewStandardGrowthDetailRepository(db *gorm.DB) StandardGrowthDetailRepository {
return &StandardGrowthDetailRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StandardGrowthDetail](db),
db: db,
}
}
func (r *StandardGrowthDetailRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.StandardGrowthDetail](ctx, r.db, id)
}
func (r *StandardGrowthDetailRepositoryImpl) GetByProductionStandardID(ctx context.Context, productionStandardId uint) ([]entity.StandardGrowthDetail, error) {
var details []entity.StandardGrowthDetail
err := r.db.WithContext(ctx).
Where("production_standard_id = ?", productionStandardId).
Order("week ASC").
Find(&details).Error
if err != nil {
return nil, err
}
return details, nil
}
func (r *StandardGrowthDetailRepositoryImpl) GetByStandardIDAndWeek(ctx context.Context, standardId uint, week int) (*entity.StandardGrowthDetail, error) {
var detail entity.StandardGrowthDetail
err := r.db.WithContext(ctx).
Where("production_standard_id = ?", standardId).
Where("week = ?", week).
First(&detail).Error
if err != nil {
return nil, err
}
return &detail, nil
}
func (r *StandardGrowthDetailRepositoryImpl) DeleteByProductionStandardID(ctx context.Context, productionStandardId uint) error {
return r.db.WithContext(ctx).
Where("production_standard_id = ?", productionStandardId).
Delete(&entity.StandardGrowthDetail{}).Error
}
@@ -0,0 +1,23 @@
package productionstandards
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/controllers"
productionStandard "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func ProductionStandardRoutes(v1 fiber.Router, u user.UserService, s productionStandard.ProductionStandardService) {
ctrl := controller.NewProductionStandardController(s)
route := v1.Group("/production-standards")
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,302 @@
package service
import (
"errors"
"fmt"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards/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 ProductionStandardService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProductionStandard, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProductionStandard, error)
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
}
type productionStandardService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ProductionStandardRepository
ProductionStandardDetailRepo repository.ProductionStandardDetailRepository
StandardGrowthDetailRepo repository.StandardGrowthDetailRepository
}
func NewProductionStandardService(
repo repository.ProductionStandardRepository,
productionStandardDetailRepo repository.ProductionStandardDetailRepository,
standardGrowthDetailRepo repository.StandardGrowthDetailRepository,
validate *validator.Validate,
) ProductionStandardService {
return &productionStandardService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ProductionStandardDetailRepo: productionStandardDetailRepo,
StandardGrowthDetailRepo: standardGrowthDetailRepo,
}
}
func (s productionStandardService) withRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("CreatedUser").
Preload("ProductionStandardDetails").
Preload("StandardGrowthDetails")
}
func (s productionStandardService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductionStandard, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
productionStandards, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
if params.ProjectCategory != "" {
return db.Where("project_category = ?", params.ProjectCategory)
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get productionStandards: %+v", err)
return nil, 0, err
}
return productionStandards, total, nil
}
func (s productionStandardService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductionStandard, error) {
productionStandard, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
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
}
func (s *productionStandardService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProductionStandard, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
nameExists, err := s.Repository.NameExists(c.Context(), req.Name, nil)
if err != nil {
return nil, err
}
if nameExists {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Production standard with name '%s' already exists", req.Name))
}
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)
newStandard := &entity.ProductionStandard{
Name: req.Name,
ProjectCategory: req.ProjectCategory,
CreatedBy: actorID,
}
if err := standardRepoTx.CreateOne(c.Context(), newStandard, nil); err != nil {
return fmt.Errorf("failed to create production standard: %w", err)
}
for _, detailReq := range req.Details {
if detailReq.ProductionStandardUniformityDetails == nil {
return fmt.Errorf("production_standard_uniformity_details is required in week %d", detailReq.Week)
}
if req.ProjectCategory == string(utils.ProjectFlockCategoryLaying) {
if detailReq.ProductionStandardDetails == nil {
return fmt.Errorf("production_standard_details is required for LAYING category in week %d", detailReq.Week)
}
productionStandardDetail := &entity.ProductionStandardDetail{
ProductionStandardId: newStandard.Id,
Week: detailReq.Week,
TargetHenDayProduction: detailReq.ProductionStandardDetails.TargetHenDayProduction,
TargetHenHouseProduction: detailReq.ProductionStandardDetails.TargetHenHouseProduction,
TargetEggWeight: detailReq.ProductionStandardDetails.TargetEggWeight,
TargetEggMass: detailReq.ProductionStandardDetails.TargetEggMass,
}
if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil {
return fmt.Errorf("failed to create production standard detail for week %d: %w", detailReq.Week, err)
}
}
standardGrowthDetail := &entity.StandardGrowthDetail{
ProductionStandardId: newStandard.Id,
Week: detailReq.Week,
TargetMeanBw: detailReq.ProductionStandardUniformityDetails.TargetMeanBw,
MaxDepletion: detailReq.ProductionStandardUniformityDetails.MaxDepletion,
MinUniformity: detailReq.ProductionStandardUniformityDetails.MinUniformity,
FeedIntake: detailReq.ProductionStandardUniformityDetails.FeedIntake,
CreatedBy: actorID,
}
if err := standardGrowthDetailRepoTx.CreateOne(c.Context(), standardGrowthDetail, nil); err != nil {
return fmt.Errorf("failed to create standard growth detail for week %d: %w", detailReq.Week, err)
}
}
createdStandard = newStandard
return nil
})
if err != nil {
s.Log.Errorf("Failed to create production standard: %+v", err)
return nil, err
}
return s.GetOne(c, createdStandard.Id)
}
func (s productionStandardService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProductionStandard, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
var updatedStandard *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)
existingStandard, err := standardRepoTx.GetByID(c.Context(), id, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "ProductionStandard not found")
}
return fmt.Errorf("failed to get production standard: %w", err)
}
updateBody := make(map[string]any)
if req.Name != nil {
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 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Production standard with name '%s' already exists", *req.Name))
}
updateBody["name"] = *req.Name
}
if req.ProjectCategory != nil {
updateBody["project_category"] = *req.ProjectCategory
}
if len(updateBody) > 0 {
if err := standardRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
return fmt.Errorf("failed to update production standard: %w", err)
}
}
if req.Details != nil && len(req.Details) > 0 {
projectCategory := existingStandard.ProjectCategory
if req.ProjectCategory != nil {
projectCategory = *req.ProjectCategory
}
if err := productionStandardDetailRepoTx.DeleteByProductionStandardID(c.Context(), id); err != nil {
return fmt.Errorf("failed to delete old production standard details: %w", err)
}
if err := standardGrowthDetailRepoTx.DeleteByProductionStandardID(c.Context(), id); err != nil {
return fmt.Errorf("failed to delete old standard growth details: %w", err)
}
for _, detailReq := range req.Details {
if detailReq.ProductionStandardUniformityDetails == nil {
return fmt.Errorf("production_standard_uniformity_details is required in week %d", detailReq.Week)
}
if projectCategory == "LAYING" {
if detailReq.ProductionStandardDetails == nil {
return fmt.Errorf("production_standard_details is required for LAYING category in week %d", detailReq.Week)
}
productionStandardDetail := &entity.ProductionStandardDetail{
ProductionStandardId: id,
Week: detailReq.Week,
TargetHenDayProduction: detailReq.ProductionStandardDetails.TargetHenDayProduction,
TargetHenHouseProduction: detailReq.ProductionStandardDetails.TargetHenHouseProduction,
TargetEggWeight: detailReq.ProductionStandardDetails.TargetEggWeight,
TargetEggMass: detailReq.ProductionStandardDetails.TargetEggMass,
}
if err := productionStandardDetailRepoTx.CreateOne(c.Context(), productionStandardDetail, nil); err != nil {
return fmt.Errorf("failed to create production standard detail for week %d: %w", detailReq.Week, err)
}
}
standardGrowthDetail := &entity.StandardGrowthDetail{
ProductionStandardId: id,
Week: detailReq.Week,
TargetMeanBw: detailReq.ProductionStandardUniformityDetails.TargetMeanBw,
MaxDepletion: detailReq.ProductionStandardUniformityDetails.MaxDepletion,
MinUniformity: detailReq.ProductionStandardUniformityDetails.MinUniformity,
FeedIntake: detailReq.ProductionStandardUniformityDetails.FeedIntake,
CreatedBy: actorID,
}
if err := standardGrowthDetailRepoTx.CreateOne(c.Context(), standardGrowthDetail, nil); err != nil {
return fmt.Errorf("failed to create standard growth detail for week %d: %w", detailReq.Week, err)
}
}
}
updatedStandard = existingStandard
return nil
})
if err != nil {
s.Log.Errorf("Failed to update production standard: %+v", err)
return nil, err
}
return s.GetOne(c, updatedStandard.Id)
}
func (s productionStandardService) 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, "ProductionStandard not found")
}
s.Log.Errorf("Failed to delete productionStandard: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,41 @@
package validation
type ProductionStandardDetailItem struct {
TargetHenDayProduction *float64 `json:"target_hen_day_production" validate:"omitempty,gte=0"`
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"`
}
type StandardGrowthDetailItem struct {
TargetMeanBw *float64 `json:"target_mean_bw" validate:"omitempty,gte=0"`
MaxDepletion *float64 `json:"max_depletion" validate:"omitempty,gte=0,lte=100"`
MinUniformity float64 `json:"min_uniformity" validate:"required,gte=0,lte=100"`
FeedIntake *float64 `json:"feed_intake" validate:"omitempty,gte=0"`
}
type DetailItem struct {
Week int `json:"week" validate:"required,gte=1"`
ProductionStandardDetails *ProductionStandardDetailItem `json:"production_standard_details,omitempty"`
ProductionStandardUniformityDetails *StandardGrowthDetailItem `json:"production_standard_uniformity_details" validate:"required"`
}
type Create struct {
Name string `json:"name" validate:"required,min=3"`
ProjectCategory string `json:"project_category" validate:"required,oneof=GROWING LAYING"`
Details []DetailItem `json:"details" validate:"required,min=1,dive"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3"`
ProjectCategory *string `json:"project_category,omitempty" validate:"omitempty,oneof=GROWING LAYING"`
Details []DetailItem `json:"details,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"`
ProjectCategory string `query:"project_category" validate:"omitempty,oneof=GROWING LAYING"`
}
+2
View File
@@ -20,6 +20,7 @@ import (
suppliers "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers"
uoms "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms"
warehouses "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses"
productionStandards "gitlab.com/mbugroup/lti-api.git/internal/modules/master/production-standards"
// MODULE IMPORTS
)
@@ -40,6 +41,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
products.ProductModule{},
banks.BankModule{},
flocks.FlockModule{},
productionStandards.ProductionStandardModule{},
// MODULE REGISTRY
}