From bb76d27f2514a559c7a9a262e746593992ada53a Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Sat, 27 Dec 2025 09:02:16 +0700 Subject: [PATCH] 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. --- ..._create_production_standards_tables.up.sql | 60 +++- internal/entities/production_standard.go | 19 ++ .../entities/production_standard_detail.go | 19 ++ internal/entities/standard_growth_detail.go | 19 ++ .../production-standard.controller.go | 145 +++++++++ .../dto/production-standard.dto.go | 155 +++++++++ .../master/production-standards/module.go | 33 ++ .../production_standard.repository.go | 103 ++++++ .../production_standard_detail.repository.go | 63 ++++ .../standard_growth_detail.repository.go | 63 ++++ .../master/production-standards/route.go | 23 ++ .../services/production-standard.service.go | 302 ++++++++++++++++++ .../production-standard.validation.go | 41 +++ internal/modules/master/route.go | 2 + 14 files changed, 1038 insertions(+), 9 deletions(-) create mode 100644 internal/entities/production_standard.go create mode 100644 internal/entities/production_standard_detail.go create mode 100644 internal/entities/standard_growth_detail.go create mode 100644 internal/modules/master/production-standards/controllers/production-standard.controller.go create mode 100644 internal/modules/master/production-standards/dto/production-standard.dto.go create mode 100644 internal/modules/master/production-standards/module.go create mode 100644 internal/modules/master/production-standards/repositories/production_standard.repository.go create mode 100644 internal/modules/master/production-standards/repositories/production_standard_detail.repository.go create mode 100644 internal/modules/master/production-standards/repositories/standard_growth_detail.repository.go create mode 100644 internal/modules/master/production-standards/route.go create mode 100644 internal/modules/master/production-standards/services/production-standard.service.go create mode 100644 internal/modules/master/production-standards/validations/production-standard.validation.go diff --git a/internal/database/migrations/20251226161036_create_production_standards_tables.up.sql b/internal/database/migrations/20251226161036_create_production_standards_tables.up.sql index 61aa3071..2af43d20 100644 --- a/internal/database/migrations/20251226161036_create_production_standards_tables.up.sql +++ b/internal/database/migrations/20251226161036_create_production_standards_tables.up.sql @@ -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); diff --git a/internal/entities/production_standard.go b/internal/entities/production_standard.go new file mode 100644 index 00000000..1214f6cf --- /dev/null +++ b/internal/entities/production_standard.go @@ -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"` +} diff --git a/internal/entities/production_standard_detail.go b/internal/entities/production_standard_detail.go new file mode 100644 index 00000000..cd50a572 --- /dev/null +++ b/internal/entities/production_standard_detail.go @@ -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"` +} diff --git a/internal/entities/standard_growth_detail.go b/internal/entities/standard_growth_detail.go new file mode 100644 index 00000000..a3d19fb8 --- /dev/null +++ b/internal/entities/standard_growth_detail.go @@ -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"` +} diff --git a/internal/modules/master/production-standards/controllers/production-standard.controller.go b/internal/modules/master/production-standards/controllers/production-standard.controller.go new file mode 100644 index 00000000..1635385d --- /dev/null +++ b/internal/modules/master/production-standards/controllers/production-standard.controller.go @@ -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", + }) +} diff --git a/internal/modules/master/production-standards/dto/production-standard.dto.go b/internal/modules/master/production-standards/dto/production-standard.dto.go new file mode 100644 index 00000000..9544732a --- /dev/null +++ b/internal/modules/master/production-standards/dto/production-standard.dto.go @@ -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), + } +} diff --git a/internal/modules/master/production-standards/module.go b/internal/modules/master/production-standards/module.go new file mode 100644 index 00000000..bd08ee88 --- /dev/null +++ b/internal/modules/master/production-standards/module.go @@ -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) +} + diff --git a/internal/modules/master/production-standards/repositories/production_standard.repository.go b/internal/modules/master/production-standards/repositories/production_standard.repository.go new file mode 100644 index 00000000..26330d63 --- /dev/null +++ b/internal/modules/master/production-standards/repositories/production_standard.repository.go @@ -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 +} diff --git a/internal/modules/master/production-standards/repositories/production_standard_detail.repository.go b/internal/modules/master/production-standards/repositories/production_standard_detail.repository.go new file mode 100644 index 00000000..ad680c01 --- /dev/null +++ b/internal/modules/master/production-standards/repositories/production_standard_detail.repository.go @@ -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 +} diff --git a/internal/modules/master/production-standards/repositories/standard_growth_detail.repository.go b/internal/modules/master/production-standards/repositories/standard_growth_detail.repository.go new file mode 100644 index 00000000..afe93d81 --- /dev/null +++ b/internal/modules/master/production-standards/repositories/standard_growth_detail.repository.go @@ -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 +} diff --git a/internal/modules/master/production-standards/route.go b/internal/modules/master/production-standards/route.go new file mode 100644 index 00000000..d2035bea --- /dev/null +++ b/internal/modules/master/production-standards/route.go @@ -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) +} diff --git a/internal/modules/master/production-standards/services/production-standard.service.go b/internal/modules/master/production-standards/services/production-standard.service.go new file mode 100644 index 00000000..b81faf8b --- /dev/null +++ b/internal/modules/master/production-standards/services/production-standard.service.go @@ -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 +} diff --git a/internal/modules/master/production-standards/validations/production-standard.validation.go b/internal/modules/master/production-standards/validations/production-standard.validation.go new file mode 100644 index 00000000..51aeecc7 --- /dev/null +++ b/internal/modules/master/production-standards/validations/production-standard.validation.go @@ -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"` +} diff --git a/internal/modules/master/route.go b/internal/modules/master/route.go index 44702e1a..26ae28ee 100644 --- a/internal/modules/master/route.go +++ b/internal/modules/master/route.go @@ -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 }