merge: ragil-before-sso from development-before-sso

This commit is contained in:
ragilap
2025-10-28 12:22:08 +07:00
parent 8ae614540f
commit 614da067f7
30 changed files with 309 additions and 110 deletions
@@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"fmt"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -32,3 +33,21 @@ func ExistsByName[T any](ctx context.Context, db *gorm.DB, name string, excludeI
} }
return count > 0, nil return count > 0, nil
} }
func ExistsByField[T any](ctx context.Context, db *gorm.DB, field string, value any, excludeID *uint) (bool, error) {
if field == "" {
return false, fmt.Errorf("field is required")
}
var count int64
q := db.WithContext(ctx).
Model(new(T)).
Where(fmt.Sprintf("%s = ?", field), value).
Where("deleted_at IS NULL")
if excludeID != nil {
q = q.Where("id <> ?", *excludeID)
}
if err := q.Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
@@ -28,6 +28,11 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error {
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
ProductId: uint(c.QueryInt("product_id", 0)), ProductId: uint(c.QueryInt("product_id", 0)),
WarehouseId: uint(c.QueryInt("warehouse_id", 0)), WarehouseId: uint(c.QueryInt("warehouse_id", 0)),
Flags: c.Query("flags", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
} }
result, totalResults, err := u.ProductWarehouseService.GetAll(c, query) result, totalResults, err := u.ProductWarehouseService.GetAll(c, query)
@@ -71,5 +76,3 @@ func (u *ProductWarehouseController) GetOne(c *fiber.Ctx) error {
Data: dto.ToProductWarehouseListDTO(*result), Data: dto.ToProductWarehouseListDTO(*result),
}) })
} }
@@ -16,6 +16,7 @@ type ProductWarehouseRepository interface {
ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error)
ExistsByID(ctx context.Context, id uint) (bool, error) ExistsByID(ctx context.Context, id uint) (bool, error)
GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (*entity.ProductWarehouse, error)
GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
} }
type ProductWarehouseRepositoryImpl struct { type ProductWarehouseRepositoryImpl struct {
@@ -30,6 +31,17 @@ func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository {
} }
} }
func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) {
return repository.Exists[entity.Product](ctx, r.db, productId)
}
func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) {
return repository.Exists[entity.Warehouse](ctx, r.db, warehouseId)
}
func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProductWarehouse](ctx, r.db, id)
}
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) { func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Context, productId, warehouseId uint, excludeID *uint) (bool, error) {
var count int64 var count int64
query := r.db.WithContext(ctx).Model(&entity.ProductWarehouse{}). query := r.db.WithContext(ctx).Model(&entity.ProductWarehouse{}).
@@ -43,17 +55,6 @@ func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExists(ctx context.Cont
return count > 0, nil return count > 0, nil
} }
func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) {
return repository.Exists[entity.Product](ctx, r.db, productId)
}
func (r *ProductWarehouseRepositoryImpl) IsWarehouseExist(ctx context.Context, warehouseId uint) (bool, error) {
return repository.Exists[entity.Warehouse](ctx, r.db, warehouseId)
}
func (r *ProductWarehouseRepositoryImpl) ExistsByID(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProductWarehouse](ctx, r.db, id)
}
func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) { func (r *ProductWarehouseRepositoryImpl) ProductWarehouseExistByProductAndWarehouseID(ctx context.Context, productId, warehouseId uint) (bool, error) {
var count int64 var count int64
if err := r.db.WithContext(ctx). if err := r.db.WithContext(ctx).
@@ -72,3 +73,19 @@ func (r *ProductWarehouseRepositoryImpl) GetProductWarehouseByProductAndWarehous
} }
return &productWarehouse, nil return &productWarehouse, nil
} }
func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error) {
var productWarehouses []entity.ProductWarehouse
err := r.db.WithContext(ctx).
Table("product_warehouses").
Select("product_warehouses.*").
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", categoryCode, warehouseId).
Order("product_warehouses.created_at DESC").
Find(&productWarehouses).Error
if err != nil {
return nil, err
}
return productWarehouses, nil
}
@@ -49,8 +49,30 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
return nil, 0, err return nil, 0, err
} }
if params.ProductId > 0 {
isProductExist, err := s.Repository.IsProductExist(c.Context(), params.ProductId)
if err != nil {
return nil, 0, err
}
if !isProductExist {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found")
}
}
if params.WarehouseId > 0 {
isWarehouseExist, err := s.Repository.IsWarehouseExist(c.Context(), params.WarehouseId)
if err != nil {
return nil, 0, err
}
if !isWarehouseExist {
return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
}
}
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
cleanFlags := utils.ParseFlags(params.Flags)
productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { productWarehouses, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
@@ -62,6 +84,12 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
db = db.Where("warehouse_id = ?", params.WarehouseId) db = db.Where("warehouse_id = ?", params.WarehouseId)
} }
if len(cleanFlags) > 0 {
db = db.Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products").
Where("flags.name IN ?", cleanFlags)
}
return db.Order("created_at DESC").Order("updated_at DESC") return db.Order("created_at DESC").Order("updated_at DESC")
}) })
@@ -13,8 +13,9 @@ type Update struct {
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1"` Page int `query:"page" validate:"omitempty,number,min=1"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
ProductId uint `query:"product_id" validate:"omitempty,number,min=1"` ProductId uint `query:"product_id" validate:"omitempty,number,min=1"`
WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"` WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"`
Flags string `query:"flags" validate:"omitempty"`
} }
@@ -29,6 +29,10 @@ func (u *AreaController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), Search: c.Query("search", ""),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.AreaService.GetAll(c, query) result, totalResults, err := u.AreaService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -29,6 +29,10 @@ func (u *BankController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), Search: c.Query("search", ""),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.BankService.GetAll(c, query) result, totalResults, err := u.BankService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -11,6 +11,7 @@ import (
type BankRepository interface { type BankRepository interface {
repository.BaseRepository[entity.Bank] repository.BaseRepository[entity.Bank]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
AccountNumberExists(ctx context.Context, accountNumber string, excludeID *uint) (bool, error)
} }
type BankRepositoryImpl struct { type BankRepositoryImpl struct {
@@ -28,3 +29,7 @@ func NewBankRepository(db *gorm.DB) BankRepository {
func (r *BankRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) { func (r *BankRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Bank](ctx, r.db, name, excludeID) return repository.ExistsByName[entity.Bank](ctx, r.db, name, excludeID)
} }
func (r *BankRepositoryImpl) AccountNumberExists(ctx context.Context, accountNumber string, excludeID *uint) (bool, error) {
return repository.ExistsByField[entity.Bank](ctx, r.db, "account_number", accountNumber, excludeID)
}
@@ -87,6 +87,13 @@ func (s *bankService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.B
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with name %s already exists", req.Name)) return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with name %s already exists", req.Name))
} }
if exists, err := s.Repository.AccountNumberExists(c.Context(), req.AccountNumber, nil); err != nil {
s.Log.Errorf("Failed to check bank account number: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check bank account number")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Bank with account number %s already exists", req.AccountNumber))
}
createBody := &entity.Bank{ createBody := &entity.Bank{
Name: req.Name, Name: req.Name,
Alias: req.Alias, Alias: req.Alias,
@@ -29,6 +29,10 @@ func (u *CustomerController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), Search: c.Query("search", ""),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.CustomerService.GetAll(c, query) result, totalResults, err := u.CustomerService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -29,6 +29,10 @@ func (u *FcrController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), Search: c.Query("search", ""),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.FcrService.GetAll(c, query) result, totalResults, err := u.FcrService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -29,6 +29,10 @@ func (u *FlockController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), Search: c.Query("search", ""),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.FlockService.GetAll(c, query) result, totalResults, err := u.FlockService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -31,6 +31,10 @@ func (u *KandangController) GetAll(c *fiber.Ctx) error {
PicId: c.QueryInt("pic_id", 0), PicId: c.QueryInt("pic_id", 0),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.KandangService.GetAll(c, query) result, totalResults, err := u.KandangService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -30,6 +30,10 @@ func (u *LocationController) GetAll(c *fiber.Ctx) error {
AreaId: c.QueryInt("area_id", 0), AreaId: c.QueryInt("area_id", 0),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.LocationService.GetAll(c, query) result, totalResults, err := u.LocationService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -29,6 +29,10 @@ func (u *NonstockController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), Search: c.Query("search", ""),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.NonstockService.GetAll(c, query) result, totalResults, err := u.NonstockService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -29,6 +29,10 @@ func (u *ProductCategoryController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), Search: c.Query("search", ""),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.ProductCategoryService.GetAll(c, query) result, totalResults, err := u.ProductCategoryService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -30,6 +30,10 @@ func (u *ProductController) GetAll(c *fiber.Ctx) error {
ProductCategoryID: c.QueryInt("product_category_id", 0), ProductCategoryID: c.QueryInt("product_category_id", 0),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.ProductService.GetAll(c, query) result, totalResults, err := u.ProductService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -29,6 +29,10 @@ func (u *SupplierController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), Search: c.Query("search", ""),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.SupplierService.GetAll(c, query) result, totalResults, err := u.SupplierService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -11,7 +11,7 @@ import (
type SupplierRepository interface { type SupplierRepository interface {
repository.BaseRepository[entity.Supplier] repository.BaseRepository[entity.Supplier]
NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error)
AliasExists(ctx context.Context, alias string, excludeID *uint) (bool, error)
} }
type SupplierRepositoryImpl struct { type SupplierRepositoryImpl struct {
@@ -29,3 +29,7 @@ func NewSupplierRepository(db *gorm.DB) SupplierRepository {
func (r *SupplierRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) { func (r *SupplierRepositoryImpl) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) {
return repository.ExistsByName[entity.Supplier](ctx, r.db, name, excludeID) return repository.ExistsByName[entity.Supplier](ctx, r.db, name, excludeID)
} }
func (r *SupplierRepositoryImpl) AliasExists(ctx context.Context, alias string, excludeID *uint) (bool, error) {
return repository.ExistsByField[entity.Supplier](ctx, r.db, "alias", alias, excludeID)
}
@@ -88,6 +88,13 @@ func (s *supplierService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with name %s already exists", req.Name)) return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with name %s already exists", req.Name))
} }
if exists, err := s.Repository.AliasExists(c.Context(), strings.TrimSpace(strings.ToUpper(req.Alias)), nil); err != nil {
s.Log.Errorf("Failed to check supplier alias: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check supplier alias")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with alias %s already exists", strings.TrimSpace(strings.ToUpper(req.Alias))))
}
typ := strings.ToUpper(req.Type) typ := strings.ToUpper(req.Type)
if !utils.IsValidCustomerSupplierType(typ) { if !utils.IsValidCustomerSupplierType(typ) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid supplier type") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid supplier type")
@@ -143,6 +150,12 @@ func (s supplierService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint
} }
if req.Alias != nil { if req.Alias != nil {
if exists, err := s.Repository.AliasExists(c.Context(), strings.TrimSpace(strings.ToUpper(*req.Alias)), &id); err != nil {
s.Log.Errorf("Failed to check supplier alias: %+v", err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check supplier alias")
} else if exists {
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Supplier with alias %s already exists", strings.TrimSpace(strings.ToUpper(*req.Alias))))
}
updateBody["alias"] = strings.TrimSpace(strings.ToUpper(*req.Alias)) updateBody["alias"] = strings.TrimSpace(strings.ToUpper(*req.Alias))
} }
@@ -29,6 +29,10 @@ func (u *UomController) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), Search: c.Query("search", ""),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.UomService.GetAll(c, query) result, totalResults, err := u.UomService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -30,6 +30,10 @@ func (u *WarehouseController) GetAll(c *fiber.Ctx) error {
AreaId: c.QueryInt("area_id", 0), AreaId: c.QueryInt("area_id", 0),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.WarehouseService.GetAll(c, query) result, totalResults, err := u.WarehouseService.GetAll(c, query)
if err != nil { if err != nil {
return err return err
@@ -121,14 +121,8 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err return nil, err
} }
var productWarehouses []entity.ProductWarehouse // move complex DB query into repository for cleaner service
err = s.ProductWarehouseRepo.DB(). productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id)
WithContext(c.Context()).
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id).
Order("created_at DESC").
Find(&productWarehouses).Error
if err != nil { if err != nil {
s.Log.Errorf("Failed to get product warehouses: %+v", err) s.Log.Errorf("Failed to get product warehouses: %+v", err)
return nil, err return nil, err
@@ -136,8 +130,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
if len(productWarehouses) == 0 { if len(productWarehouses) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse") return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")
} }
// Jumlahkan semua quantity DOC
totalQuantity := 0.0 totalQuantity := 0.0
for _, pw := range productWarehouses { for _, pw := range productWarehouses {
totalQuantity += pw.Quantity totalQuantity += pw.Quantity
@@ -147,7 +139,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses") return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses")
} }
// Buat satu chickin dengan total quantity
chickinDate, err := utils.ParseDateString(req.ChickInDate) chickinDate, err := utils.ParseDateString(req.ChickInDate)
if err != nil { if err != nil {
s.Log.Errorf("Failed to parse chickin date: %+v", err) s.Log.Errorf("Failed to parse chickin date: %+v", err)
@@ -157,7 +148,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
ProjectFlockKandangId: projectflockkandang.Id, ProjectFlockKandangId: projectflockkandang.Id,
ChickInDate: chickinDate, ChickInDate: chickinDate,
Quantity: totalQuantity, Quantity: totalQuantity,
Note: "", Note: req.Note,
CreatedBy: 1, //todo: ganti dengan user login CreatedBy: 1, //todo: ganti dengan user login
} }
err = s.Repository.CreateOne(c.Context(), newChickin, nil) err = s.Repository.CreateOne(c.Context(), newChickin, nil)
@@ -176,7 +167,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err return nil, err
} }
// add ke detail chickin
newChickinDetail := &entity.ProjectChickinDetail{ newChickinDetail := &entity.ProjectChickinDetail{
ProjectChickinId: newChickin.Id, ProjectChickinId: newChickin.Id,
ProductWarehouseId: pw.Id, ProductWarehouseId: pw.Id,
@@ -232,6 +222,9 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if req.ChickInDate != "" { if req.ChickInDate != "" {
updateBody["chick_in_date"] = req.ChickInDate updateBody["chick_in_date"] = req.ChickInDate
} }
if req.Note != "" {
updateBody["note"] = req.Note
}
if len(updateBody) == 0 { if len(updateBody) == 0 {
return s.GetOne(c, id) return s.GetOne(c, id)
} }
@@ -293,7 +286,6 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
return rollback(err) return rollback(err)
} }
// helper: restore quantities from details; returns (restored bool, error)
restoreFromDetails := func() (bool, error) { restoreFromDetails := func() (bool, error) {
var details []entity.ProjectChickinDetail var details []entity.ProjectChickinDetail
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil { if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil {
@@ -3,10 +3,12 @@ package validation
type Create struct { type Create struct {
ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"`
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"` ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
Note string `json:"note" validate:"omitempty`
} }
type Update struct { type Update struct {
ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"` ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"`
Note string `json:"note" validate:"omitempty"`
} }
type Query struct { type Query struct {
@@ -246,17 +246,39 @@ func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error {
} }
func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error { func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
projectFlockIdStr := c.Query("project_flock_id", "") projectFlockId := c.QueryInt("project_flock_id", 0)
kandangIdStr := c.Query("kandang_id", "") kandangId := c.QueryInt("kandang_id", 0)
result, err := u.ProjectflockService.GetProjectFlockKandangByParams(c, "", projectFlockIdStr, kandangIdStr) if projectFlockId == 0 || kandangId == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id or kandang_id")
}
result, availableStock, err := u.ProjectflockService.GetProjectFlockKandangByProjectAndKandang(c, uint(projectFlockId), uint(kandangId))
if err != nil { if err != nil {
return err return err
} }
dtoResult := dto.ToProjectFlockKandangDTO(*result)
dtoResult.AvailableQuantity = float64(availableStock)
// populate available quantity for each kandang inside project_flock
if dtoResult.ProjectFlock != nil {
for i := range dtoResult.ProjectFlock.Kandangs {
kand := &dtoResult.ProjectFlock.Kandangs[i]
if kand.Id == 0 {
continue
}
if q, qerr := u.ProjectflockService.GetAvailableDocQuantity(c, kand.Id); qerr == nil {
kand.AvailableQuantity = q
}
}
// remove inner kandangs from project_flock to avoid duplication
dtoResult.ProjectFlock.Kandangs = nil
}
return c.Status(fiber.StatusOK). return c.Status(fiber.StatusOK).
JSON(response.Success{Code: fiber.StatusOK, JSON(response.Success{Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Get projectflock kandang successfully", Message: "Get projectflock kandang successfully",
Data: dto.ToProjectFlockKandangDTO(*result)}) Data: dtoResult})
} }
@@ -10,10 +10,9 @@ import (
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
// internal DTO used only for lookup response: project flock with kandangs carrying pivot ids
type KandangWithPivotDTO struct { type KandangWithPivotDTO struct {
kandangDTO.KandangBaseDTO kandangDTO.KandangBaseDTO
ProjectFlockKandangId *uint `json:"project_flock_kandang_id,omitempty"` AvailableQuantity float64 `json:"available_quantity"`
} }
type ProjectFlockWithPivotDTO struct { type ProjectFlockWithPivotDTO struct {
@@ -28,11 +27,13 @@ type ProjectFlockWithPivotDTO struct {
} }
type ProjectFlockKandangDTO struct { type ProjectFlockKandangDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
ProjectFlockId uint `json:"project_flock_id"` ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
KandangId uint `json:"kandang_id"` ProjectFlockId uint `json:"project_flock_id"`
Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"` KandangId uint `json:"kandang_id"`
ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"` Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"`
ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"`
AvailableQuantity float64 `json:"available_quantity"`
} }
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
@@ -44,7 +45,7 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
var pf *ProjectFlockWithPivotDTO var pf *ProjectFlockWithPivotDTO
if e.ProjectFlock.Id != 0 { if e.ProjectFlock.Id != 0 {
// build project flock with kandangs that include pivot ids
pfLocal := ProjectFlockWithPivotDTO{ pfLocal := ProjectFlockWithPivotDTO{
ProjectFlockBaseDTO: ProjectFlockBaseDTO{ ProjectFlockBaseDTO: ProjectFlockBaseDTO{
Id: e.ProjectFlock.Id, Id: e.ProjectFlock.Id,
@@ -53,7 +54,6 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
Category: e.ProjectFlock.Category, Category: e.ProjectFlock.Category,
} }
// fill related small summaries
if e.ProjectFlock.Flock.Id != 0 { if e.ProjectFlock.Flock.Id != 0 {
mapped := ToFlockSummaryDTO(e.ProjectFlock.Flock) mapped := ToFlockSummaryDTO(e.ProjectFlock.Flock)
pfLocal.Flock = &mapped pfLocal.Flock = &mapped
@@ -75,23 +75,16 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
pfLocal.CreatedUser = &mapped pfLocal.CreatedUser = &mapped
} }
// build pivot map
pivotMap := make(map[uint]uint) pivotMap := make(map[uint]uint)
for _, ph := range e.ProjectFlock.KandangHistory { for _, ph := range e.ProjectFlock.KandangHistory {
pivotMap[ph.KandangId] = ph.Id pivotMap[ph.KandangId] = ph.Id
} }
// populate kandangs with pivot ids
for _, k := range e.ProjectFlock.Kandangs { for _, k := range e.ProjectFlock.Kandangs {
kb := kandangDTO.ToKandangBaseDTO(k) kb := kandangDTO.ToKandangBaseDTO(k)
var pid *uint
if v, ok := pivotMap[k.Id]; ok {
vv := v
pid = &vv
}
pfLocal.Kandangs = append(pfLocal.Kandangs, KandangWithPivotDTO{ pfLocal.Kandangs = append(pfLocal.Kandangs, KandangWithPivotDTO{
KandangBaseDTO: kb, KandangBaseDTO: kb,
ProjectFlockKandangId: pid, AvailableQuantity: 0,
}) })
} }
@@ -99,10 +92,12 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
} }
return ProjectFlockKandangDTO{ return ProjectFlockKandangDTO{
Id: e.Id, Id: e.Id,
ProjectFlockId: e.ProjectFlockId, ProjectFlockKandangId: e.Id,
KandangId: e.KandangId, ProjectFlockId: e.ProjectFlockId,
Kandang: kandang, KandangId: e.KandangId,
ProjectFlock: pf, Kandang: kandang,
ProjectFlock: pf,
AvailableQuantity: 0,
} }
} }
@@ -9,8 +9,10 @@ import (
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
"gorm.io/gorm" "gorm.io/gorm"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services"
@@ -27,6 +29,8 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
kandangRepo := rKandang.NewKandangRepository(db) kandangRepo := rKandang.NewKandangRepository(db)
projectflockRepo := rProjectflock.NewProjectflockRepository(db) projectflockRepo := rProjectflock.NewProjectflockRepository(db)
projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db) projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
@@ -35,7 +39,7 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid
panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err)) panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err))
} }
projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, approvalService, validate) projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, warehouseRepo, productWarehouseRepo, approvalService, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
ProjectflockRoutes(router, userService, projectflockService) ProjectflockRoutes(router, userService, projectflockService)
@@ -4,14 +4,15 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"strconv"
"strings" "strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
warehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils" utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -29,20 +30,23 @@ type ProjectflockService interface {
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, error) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error)
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error)
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
} }
type projectflockService struct { type projectflockService struct {
Log *logrus.Logger Log *logrus.Logger
Validate *validator.Validate Validate *validator.Validate
Repository repository.ProjectflockRepository Repository repository.ProjectflockRepository
FlockRepo flockRepository.FlockRepository FlockRepo flockRepository.FlockRepository
KandangRepo kandangRepository.KandangRepository KandangRepo kandangRepository.KandangRepository
WarehouseRepo warehouseRepository.WarehouseRepository
ProductWarehouseRepo productWarehouseRepository.ProductWarehouseRepository
PivotRepo repository.ProjectFlockKandangRepository PivotRepo repository.ProjectFlockKandangRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
approvalWorkflow approvalutils.ApprovalWorkflowKey approvalWorkflow approvalutils.ApprovalWorkflowKey
} }
type FlockPeriodSummary struct { type FlockPeriodSummary struct {
@@ -55,18 +59,22 @@ func NewProjectflockService(
flockRepo flockRepository.FlockRepository, flockRepo flockRepository.FlockRepository,
kandangRepo kandangRepository.KandangRepository, kandangRepo kandangRepository.KandangRepository,
pivotRepo repository.ProjectFlockKandangRepository, pivotRepo repository.ProjectFlockKandangRepository,
warehouseRepo warehouseRepository.WarehouseRepository,
productWarehouseRepo productWarehouseRepository.ProductWarehouseRepository,
approvalSvc commonSvc.ApprovalService, approvalSvc commonSvc.ApprovalService,
validate *validator.Validate, validate *validator.Validate,
) ProjectflockService { ) ProjectflockService {
return &projectflockService{ return &projectflockService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
Repository: repo, Repository: repo,
FlockRepo: flockRepo, FlockRepo: flockRepo,
KandangRepo: kandangRepo, KandangRepo: kandangRepo,
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
PivotRepo: pivotRepo, PivotRepo: pivotRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
approvalWorkflow: utils.ApprovalWorkflowProjectFlock, approvalWorkflow: utils.ApprovalWorkflowProjectFlock,
} }
} }
@@ -648,11 +656,6 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
return nil return nil
} }
func (s projectflockService) GetProjectFlockKandang(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, error) {
// keep for backward compatibility; delegate to new consolidated method
return s.GetProjectFlockKandangByParams(ctx, fmt.Sprintf("%d", id), "", "")
}
func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error) { func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error) {
pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID) pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
@@ -665,38 +668,30 @@ func (s projectflockService) GetProjectFlockKandangByProjectAndKandang(ctx *fibe
return pfk, nil return pfk, nil
} }
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, error) { func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error) {
idStr = strings.TrimSpace(idStr)
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
kandangIdStr = strings.TrimSpace(kandangIdStr)
if idStr != "" { wh, err := s.WarehouseRepo.GetByKandangID(ctx.Context(), kandangID)
id, err := strconv.Atoi(idStr) if err != nil {
if err != nil || id <= 0 { return 0, err
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
pfk, err := s.PivotRepo.GetByID(ctx.Context(), uint(id))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
return nil, err
}
return pfk, nil
} }
if projectFlockIdStr == "" || kandangIdStr == "" { var productWarehouses []entity.ProductWarehouse
return nil, fiber.NewError(fiber.StatusBadRequest, "Missing lookup parameters") err = s.ProductWarehouseRepo.DB().
WithContext(ctx.Context()).
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_id").
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", wh.Id).
Order("created_at DESC").
Find(&productWarehouses).Error
if err != nil {
return 0, err
} }
pfid, err := strconv.Atoi(projectFlockIdStr)
if err != nil || pfid <= 0 { total := 0.0
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id") for _, pw := range productWarehouses {
total += pw.Quantity
} }
kid, err := strconv.Atoi(kandangIdStr) return total, nil
if err != nil || kid <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid kandang_id")
}
return s.GetProjectFlockKandangByProjectAndKandang(ctx, uint(pfid), uint(kid))
} }
func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) { func (s projectflockService) GetFlockPeriodSummary(c *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) {
@@ -853,7 +848,6 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
return nil return nil
} }
func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository { func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository {
if s.PivotRepo == nil { if s.PivotRepo == nil {
return repository.NewProjectFlockKandangRepository(dbTransaction) return repository.NewProjectFlockKandangRepository(dbTransaction)
@@ -874,3 +868,4 @@ func (s projectflockService) anyKandangLinkedToOtherProject(ctx context.Context,
} }
return count > 0, nil return count > 0, nil
} }
+37 -1
View File
@@ -1,6 +1,9 @@
package utils package utils
import "strings" import (
"sort"
"strings"
)
// NormalizeTrim returns the input string without leading/trailing whitespace. // NormalizeTrim returns the input string without leading/trailing whitespace.
func NormalizeTrim(value string) string { func NormalizeTrim(value string) string {
@@ -11,3 +14,36 @@ func NormalizeTrim(value string) string {
func NormalizeUpper(value string) string { func NormalizeUpper(value string) string {
return strings.ToUpper(NormalizeTrim(value)) return strings.ToUpper(NormalizeTrim(value))
} }
// NormalizeFlag trims whitespace, removes surrounding brackets/quotes and returns upper-case flag
func NormalizeFlag(value string) string {
v := NormalizeTrim(value)
v = strings.Trim(v, "[]\"'")
return strings.ToUpper(v)
}
// ParseFlags parses a raw flags string like "[DOC, PAKAN]" or "DOC,PAKAN"
// and returns a deduplicated, sorted slice of normalized flags (upper-case, trimmed).
func ParseFlags(raw string) []string {
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
set := make(map[string]struct{}, len(parts))
for _, p := range parts {
f := NormalizeFlag(p)
if f == "" {
continue
}
set[f] = struct{}{}
}
if len(set) == 0 {
return nil
}
res := make([]string, 0, len(set))
for k := range set {
res = append(res, k)
}
sort.Strings(res)
return res
}
+4
View File
@@ -29,6 +29,10 @@ func (u *{{Pascal .Entity}}Controller) GetAll(c *fiber.Ctx) error {
Search: c.Query("search", ""), Search: c.Query("search", ""),
} }
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.{{Pascal .Entity}}Service.GetAll(c, query) result, totalResults, err := u.{{Pascal .Entity}}Service.GetAll(c, query)
if err != nil { if err != nil {
return err return err