Merge branch 'development-before-sso' of https://gitlab.com/mbugroup/lti-api into dev/ragil-before-sso

This commit is contained in:
ragilap
2025-10-26 15:08:48 +07:00
19 changed files with 664 additions and 234 deletions
@@ -0,0 +1 @@
DROP TABLE IF EXISTS project_chickin_details;
@@ -0,0 +1,45 @@
CREATE TABLE IF NOT EXISTS project_chickin_details (
id BIGSERIAL PRIMARY KEY,
project_chickin_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
quantity NUMERIC(15, 3) NOT NULL,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
ALTER TABLE project_chickin_details
ADD CONSTRAINT fk_project_chickin_id
FOREIGN KEY (project_chickin_id)
REFERENCES project_chickins(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE project_chickin_details
ADD CONSTRAINT fk_product_warehouse_id
FOREIGN KEY (product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_chickin_details
ADD CONSTRAINT fk_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_project_chickin_id ON project_chickin_details (project_chickin_id);
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_product_warehouse_id ON project_chickin_details (product_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_project_chickin_details_created_by ON project_chickin_details (created_by);
+78 -1
View File
@@ -1170,7 +1170,6 @@ func seedChickin(tx *gorm.DB, createdBy uint) error {
return err
}
// Update/Insert ProjectFlockPopulation
var population entity.ProjectFlockPopulation
err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -1198,6 +1197,84 @@ func seedChickin(tx *gorm.DB, createdBy uint) error {
return err
}
}
var pfk entity.ProjectFlockKandang
if err := tx.Where("id = ?", seed.ProjectFlockKandangId).First(&pfk).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// no pivot found; skip creating details
continue
}
return err
}
var warehouse entity.Warehouse
if err := tx.Where("kandang_id = ?", pfk.KandangId).First(&warehouse).Error; err != nil {
// if warehouse not found, cannot create details
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return err
}
var productWarehouses []entity.ProductWarehouse
err = tx.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 = ?", "DOC", warehouse.Id).
Order("product_warehouses.created_at DESC").
Find(&productWarehouses).Error
if err != nil {
return err
}
// If no product warehouses found, keep existing chickin.Quantity and skip details
if len(productWarehouses) == 0 {
continue
}
// sum all pw quantities and set chickin.Quantity to that total (mimic CreateOne)
totalQty := 0.0
for _, pw := range productWarehouses {
totalQty += pw.Quantity
}
if chickin.Quantity != totalQty {
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Update("quantity", totalQty).Error; err != nil {
return err
}
chickin.Quantity = totalQty
}
for _, pw := range productWarehouses {
// ensure detail exists or create it with full pw.Quantity
var detail entity.ProjectChickinDetail
err = tx.Where("project_chickin_id = ? AND product_warehouse_id = ?", chickin.Id, pw.Id).First(&detail).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
detail = entity.ProjectChickinDetail{
ProjectChickinId: chickin.Id,
ProductWarehouseId: pw.Id,
Quantity: pw.Quantity,
CreatedBy: createdBy,
}
if err := tx.Create(&detail).Error; err != nil {
return err
}
} else if err != nil {
return err
} else {
if detail.Quantity != pw.Quantity {
if err := tx.Model(&entity.ProjectChickinDetail{}).Where("id = ?", detail.Id).Update("quantity", pw.Quantity).Error; err != nil {
return err
}
}
}
// zero out pw quantity
if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("quantity", 0).Error; err != nil {
return err
}
}
}
return nil
+22
View File
@@ -0,0 +1,22 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type ProjectChickinDetail struct {
Id uint `gorm:"primaryKey"`
ProjectChickinId uint `gorm:"column:project_chickin_id;not null"`
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
Quantity float64 `gorm:"type:numeric(15,3);not null"`
CreatedBy uint `gorm:"column:created_by;not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
-18
View File
@@ -1,18 +0,0 @@
package entities
import (
"time"
)
type AuditLog struct {
Id uint `gorm:"primaryKey"`
TableName string `gorm:"size:100;not null"`
RecordId uint `gorm:"not null"`
Action string `gorm:"size:30;not null"`
BeforeData string `gorm:"type:jsonb"`
AfterData string `gorm:"type:jsonb"`
ChangedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
User *User `gorm:"foreignKey:ChangedBy;references:Id"`
}
-26
View File
@@ -1,26 +0,0 @@
package entities
import (
"time"
"gorm.io/gorm"
)
const (
EntityTypeProjectFlockKandang = "PROJECT_FLOCK_KANDANG"
)
type StockAvailability struct {
Id uint `gorm:"primaryKey"`
EntityType string `gorm:"size:50;not null"`
EntityId uint `gorm:"not null"`
ProductId uint `gorm:"not null"`
Quantity float64 `gorm:"not null;default:0"`
ReservedQuantity float64 `gorm:"not null;default:0"`
Unit string `gorm:"size:20"`
LastUpdated time.Time `gorm:"autoUpdateTime"`
CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
}
@@ -266,6 +266,24 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
}
s.Log.Infof("Source product warehouse updated: %+v", sourcePW.Id)
// create stock log for decrease (source)
beforeQty := sourcePW.Quantity + product.ProductQty // sourcePW already decreased
decreaseLog := &entity.StockLog{
TransactionType: entity.TransactionTypeDecrease,
Quantity: product.ProductQty,
BeforeQuantity: beforeQty,
AfterQuantity: sourcePW.Quantity,
LogType: entity.LogTypeTransfer,
LogId: uint(entityTransfer.Id),
Note: "",
ProductWarehouseId: sourcePW.Id,
CreatedBy: 1,
}
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), decreaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log decrease: %+v", err)
return err
}
// Tambah stok di gudang tujuan
destPW, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
c.Context(), uint(product.ProductID), uint(req.DestinationWarehouseID),
@@ -296,6 +314,24 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
}
s.Log.Infof("Destination product warehouse updated: %+v", destPW.Id)
// create stock log for increase (destination)
beforeDestQty := destPW.Quantity - product.ProductQty
increaseLog := &entity.StockLog{
TransactionType: entity.TransactionTypeIncrease,
Quantity: product.ProductQty,
BeforeQuantity: beforeDestQty,
AfterQuantity: destPW.Quantity,
LogType: entity.LogTypeTransfer,
LogId: uint(entityTransfer.Id),
Note: "",
ProductWarehouseId: destPW.Id,
CreatedBy: 1,
}
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), increaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log increase: %+v", err)
return err
}
}
return nil
@@ -10,7 +10,6 @@ import (
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services"
rAuditLog "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
@@ -22,8 +21,9 @@ type ChickinModule struct{}
func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
chickinRepo := rChickin.NewChickinRepository(db)
chickinDetailRepo := rChickin.NewChickinDetailRepository(db)
kandangRepo := rKandang.NewKandangRepository(db)
auditlogrepo := rAuditLog.NewAuditLogRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectflockkandangrepo := rProjectFlock.NewProjectFlockKandangRepository(db)
projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
@@ -32,7 +32,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
userRepo := rUser.NewUserRepository(db)
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, auditlogrepo, projectflockkandangrepo, projectflockpopulationrepo, validate)
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ChickinRoutes(router, userService, chickinService)
@@ -0,0 +1,21 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ProjectChickinDetailRepository interface {
repository.BaseRepository[entity.ProjectChickinDetail]
}
type ChickinDetailRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProjectChickinDetail]
}
func NewChickinDetailRepository(db *gorm.DB) ProjectChickinDetailRepository {
return &ChickinDetailRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](db),
}
}
@@ -8,11 +8,8 @@ import (
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
AuditLogRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
@@ -38,12 +35,12 @@ type chickinService struct {
WarehouseRepo rWarehouse.WarehouseRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
ProjectFlockRepo rProjectFlock.ProjectflockRepository
AuditLogRepo AuditLogRepo.AuditLogRepository
ProjectflockKandangRepo rProjectFlockKandang.ProjectFlockKandangRepository
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
ProjectChickinDetailRepo repository.ProjectChickinDetailRepository
}
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, auditLogRepo AuditLogRepo.AuditLogRepository, projectflockkandangRepo rProjectFlockKandang.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, validate *validator.Validate) ChickinService {
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate) ChickinService {
return &chickinService{
Log: utils.Log,
Validate: validate,
@@ -52,9 +49,9 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan
WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo,
ProjectFlockRepo: projectFlockRepo,
AuditLogRepo: auditLogRepo,
ProjectflockKandangRepo: projectflockkandangRepo,
ProjectflockPopulationRepo: projectflockpopulationRepo,
ProjectChickinDetailRepo: projectChickinDetailRepo,
}
}
@@ -112,7 +109,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err
}
projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), 1)
projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
if err != nil {
s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
return nil, err
@@ -124,23 +121,12 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err
}
projectFlock, err := s.ProjectFlockRepo.GetByID(
c.Context(),
projectflockkandang.ProjectFlockId,
func(db *gorm.DB) *gorm.DB {
return db
},
)
if err != nil {
s.Log.Errorf("Failed to get project flock: %+v", err)
return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found")
}
var productWarehouses []entity.ProductWarehouse
err = s.ProductWarehouseRepo.DB().
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 = ?", projectFlock.Category, warehouse.Id).
Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id).
Order("created_at DESC").
Find(&productWarehouses).Error
if err != nil {
@@ -168,7 +154,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format")
}
newChickin := &entity.ProjectChickin{
ProjectFlockKandangId: projectflockkandang.ProjectFlockId,
ProjectFlockKandangId: projectflockkandang.Id,
ChickInDate: chickinDate,
Quantity: totalQuantity,
Note: "",
@@ -189,6 +175,19 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return nil, err
}
// add ke detail chickin
newChickinDetail := &entity.ProjectChickinDetail{
ProjectChickinId: newChickin.Id,
ProductWarehouseId: pw.Id,
Quantity: pw.Quantity,
CreatedBy: 1, // todo: ganti dengan user login
}
err = s.ProjectChickinDetailRepo.CreateOne(c.Context(), newChickinDetail, nil)
if err != nil {
s.Log.Errorf("Failed to create chickin detail: %+v", err)
return nil, err
}
}
existingPopulation, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId)
@@ -249,85 +248,142 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
}
func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
// todo: cek apakah chickin sudah di approve atau belum
db := s.Repository.DB()
chickin, err := s.Repository.GetByID(c.Context(), id, nil)
tx := db.WithContext(c.Context()).Begin()
if tx.Error != nil {
s.Log.Errorf("Failed to begin transaction: %+v", tx.Error)
return tx.Error
}
rollback := func(err error) error {
if rerr := tx.Rollback().Error; rerr != nil {
s.Log.Errorf("Rollback failed: %+v", rerr)
}
return err
}
chickinRepoTx := s.Repository.WithTx(tx)
pfkRepoTx := s.ProjectflockKandangRepo.WithTx(tx)
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(tx)
chickin, err := chickinRepoTx.GetByID(c.Context(), id, nil)
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found"))
}
if err != nil {
s.Log.Errorf("Failed get chickin by id: %+v", err)
return err
return rollback(err)
}
population, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId)
if err != nil {
s.Log.Errorf("Failed to get project flock population: %+v", err)
return err
}
err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), population.Id, map[string]any{
"reserved_quantity": population.ReservedQuantity - chickin.Quantity,
}, nil)
if err != nil {
s.Log.Errorf("Failed to update project flock population: %+v", err)
return err
}
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
var population entity.ProjectFlockPopulation
if err := tx.WithContext(c.Context()).Where("project_flock_kandang_id = ?", chickin.ProjectFlockKandangId).First(&population).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
return rollback(fiber.NewError(fiber.StatusNotFound, "Project flock population not found"))
}
s.Log.Errorf("Failed to get project flock population: %+v", err)
return rollback(err)
}
newReserved := population.ReservedQuantity - chickin.Quantity
if newReserved < 0 {
newReserved = 0
}
if err := tx.WithContext(c.Context()).Model(&entity.ProjectFlockPopulation{}).Where("id = ?", population.Id).Updates(map[string]any{"reserved_quantity": newReserved}).Error; err != nil {
s.Log.Errorf("Failed to update project flock population: %+v", err)
return rollback(err)
}
// helper: restore quantities from details; returns (restored bool, error)
restoreFromDetails := func() (bool, error) {
var details []entity.ProjectChickinDetail
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil {
return false, err
}
if len(details) == 0 {
return false, nil
}
for _, d := range details {
var pw entity.ProductWarehouse
if err := tx.WithContext(c.Context()).Where("id = ?", d.ProductWarehouseId).First(&pw).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
continue
}
return false, err
}
updatedQuantity := pw.Quantity + d.Quantity
if err := productWarehouseRepoTx.PatchOne(c.Context(), pw.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
return false, err
}
}
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Delete(&entity.ProjectChickinDetail{}).Error; err != nil {
return false, err
}
return true, nil
}
restored, err := restoreFromDetails()
if err != nil {
s.Log.Errorf("Failed to restore from chickin details: %+v", err)
return rollback(err)
}
if !restored {
projectflockkandang, err := pfkRepoTx.GetByID(c.Context(), population.ProjectFlockKandangId)
if err != nil {
s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
return rollback(err)
}
var warehouse entity.Warehouse
if err := tx.WithContext(c.Context()).Where("kandang_id = ?", projectflockkandang.KandangId).First(&warehouse).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Warehouse not found for kandang"))
}
s.Log.Errorf("Failed to get warehouse: %+v", err)
return rollback(err)
}
var productWarehouse entity.ProductWarehouse
err = tx.WithContext(c.Context()).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 = ?", "DOC", warehouse.Id).
Order("product_warehouses.created_at DESC").
First(&productWarehouse).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse"))
}
s.Log.Errorf("Failed to get product warehouse: %+v", err)
return rollback(err)
}
updatedQuantity := productWarehouse.Quantity + chickin.Quantity
if err := productWarehouseRepoTx.PatchOne(c.Context(), productWarehouse.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return rollback(err)
}
}
// delete chickin (single place)
if err := chickinRepoTx.DeleteOne(c.Context(), id); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found"))
}
s.Log.Errorf("Failed to delete chickin: %+v", err)
return err
return rollback(err)
}
projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), population.ProjectFlockKandangId)
if err != nil {
s.Log.Errorf("Failed to get projectflock kandang: %+v", err)
return err
}
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId)
if err != nil {
s.Log.Errorf("Failed to get warehouse: %+v", err)
return err
}
projectFlock, err := s.ProjectFlockRepo.GetByID(
c.Context(),
projectflockkandang.ProjectFlockId,
func(db *gorm.DB) *gorm.DB {
return db
},
)
if err != nil {
s.Log.Errorf("Failed to get project flock: %+v", err)
return fiber.NewError(fiber.StatusNotFound, "Project Flock not found")
}
var productWarehouse entity.ProductWarehouse
err = s.ProductWarehouseRepo.DB().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 = ?", projectFlock.Category, warehouse.Id).
Order("created_at DESC").
First(&productWarehouse).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")
}
s.Log.Errorf("Failed to get product warehouse: %+v", err)
return err
}
updatedQuantity := productWarehouse.Quantity + chickin.Quantity
err = s.ProductWarehouseRepo.PatchOne(c.Context(), productWarehouse.Id, map[string]any{
"quantity": updatedQuantity,
}, nil)
if err != nil {
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return err
if err := tx.Commit().Error; err != nil {
s.Log.Errorf("Failed to commit transaction: %+v", err)
return rollback(err)
}
return nil
@@ -191,29 +191,33 @@ func (u *ProjectflockController) DeleteOne(c *fiber.Ctx) error {
}
func (u *ProjectflockController) Approval(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
req := new(validation.Approve)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
}
result, err := u.ProjectflockService.Approval(c, uint(id), req)
results, err := u.ProjectflockService.Approval(c, req)
if err != nil {
return err
}
var (
data interface{}
message = "Submit projectflock approval successfully"
)
if len(results) == 1 {
data = dto.ToProjectFlockListDTO(results[0])
} else {
message = "Submit projectflock approvals successfully"
data = dto.ToProjectFlockListDTOs(results)
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Submit projectflock approval successfully",
Data: dto.ToProjectFlockListDTO(*result),
Message: message,
Data: data,
})
}
@@ -240,3 +244,19 @@ func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error {
Data: responseBody,
})
}
func (u *ProjectflockController) LookupProjectFlockKandang(c *fiber.Ctx) error {
projectFlockIdStr := c.Query("project_flock_id", "")
kandangIdStr := c.Query("kandang_id", "")
result, err := u.ProjectflockService.GetProjectFlockKandangByParams(c, "", projectFlockIdStr, kandangIdStr)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{Code: fiber.StatusOK,
Status: "success",
Message: "Get projectflock kandang successfully",
Data: dto.ToProjectFlockKandangDTO(*result)})
}
@@ -58,6 +58,30 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
}
}
var flockSummary *flockDTO.FlockBaseDTO
if e.Flock.Id != 0 {
mapped := flockDTO.ToFlockBaseDTO(e.Flock)
flockSummary = &mapped
}
var areaSummary *areaDTO.AreaBaseDTO
if e.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.Area)
areaSummary = &mapped
}
var fcrSummary *fcrDTO.FcrBaseDTO
if e.Fcr.Id != 0 {
mapped := fcrDTO.ToFcrBaseDTO(e.Fcr)
fcrSummary = &mapped
}
var locationSummary *locationDTO.LocationBaseDTO
if e.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.Location)
locationSummary = &mapped
}
latestApproval := defaultProjectFlockLatestApproval(e)
if e.LatestApproval != nil {
snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval)
@@ -66,8 +90,12 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO {
return ProjectFlockListDTO{
ProjectFlockBaseDTO: createProjectFlockBaseDTO(e),
Flock: flockSummary,
Area: areaSummary,
Kandangs: kandangSummaries,
Category: e.Category,
Fcr: fcrSummary,
Location: locationSummary,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
@@ -101,13 +129,6 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv
result.StepName = label
}
}
if result.StepName == "" {
result.StepName = "Pengajuan"
}
if !e.CreatedAt.IsZero() {
result.ActionAt = e.CreatedAt
}
if e.CreatedUser.Id != 0 {
result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser)
@@ -0,0 +1,108 @@
package dto
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto"
fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto"
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/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 {
kandangDTO.KandangBaseDTO
ProjectFlockKandangId *uint `json:"project_flock_kandang_id,omitempty"`
}
type ProjectFlockWithPivotDTO struct {
ProjectFlockBaseDTO
Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"`
Area *areaDTO.AreaBaseDTO `json:"area,omitempty"`
Category string `json:"category"`
Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"`
Location *locationDTO.LocationBaseDTO `json:"location,omitempty"`
Kandangs []KandangWithPivotDTO `json:"kandangs,omitempty"`
CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"`
}
type ProjectFlockKandangDTO struct {
Id uint `json:"id"`
ProjectFlockId uint `json:"project_flock_id"`
KandangId uint `json:"kandang_id"`
Kandang *kandangDTO.KandangBaseDTO `json:"kandang,omitempty"`
ProjectFlock *ProjectFlockWithPivotDTO `json:"project_flock,omitempty"`
}
func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO {
var kandang *kandangDTO.KandangBaseDTO
if e.Kandang.Id != 0 {
mapped := kandangDTO.ToKandangBaseDTO(e.Kandang)
kandang = &mapped
}
var pf *ProjectFlockWithPivotDTO
if e.ProjectFlock.Id != 0 {
// build project flock with kandangs that include pivot ids
pfLocal := ProjectFlockWithPivotDTO{
ProjectFlockBaseDTO: ProjectFlockBaseDTO{
Id: e.ProjectFlock.Id,
Period: e.ProjectFlock.Period,
},
Category: e.ProjectFlock.Category,
}
// fill related small summaries
if e.ProjectFlock.Flock.Id != 0 {
mapped := ToFlockSummaryDTO(e.ProjectFlock.Flock)
pfLocal.Flock = &mapped
}
if e.ProjectFlock.Area.Id != 0 {
mapped := areaDTO.ToAreaBaseDTO(e.ProjectFlock.Area)
pfLocal.Area = &mapped
}
if e.ProjectFlock.Fcr.Id != 0 {
mapped := fcrDTO.ToFcrBaseDTO(e.ProjectFlock.Fcr)
pfLocal.Fcr = &mapped
}
if e.ProjectFlock.Location.Id != 0 {
mapped := locationDTO.ToLocationBaseDTO(e.ProjectFlock.Location)
pfLocal.Location = &mapped
}
if e.ProjectFlock.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(e.ProjectFlock.CreatedUser)
pfLocal.CreatedUser = &mapped
}
// build pivot map
pivotMap := make(map[uint]uint)
for _, ph := range e.ProjectFlock.KandangHistory {
pivotMap[ph.KandangId] = ph.Id
}
// populate kandangs with pivot ids
for _, k := range e.ProjectFlock.Kandangs {
kb := kandangDTO.ToKandangBaseDTO(k)
var pid *uint
if v, ok := pivotMap[k.Id]; ok {
vv := v
pid = &vv
}
pfLocal.Kandangs = append(pfLocal.Kandangs, KandangWithPivotDTO{
KandangBaseDTO: kb,
ProjectFlockKandangId: pid,
})
}
pf = &pfLocal
}
return ProjectFlockKandangDTO{
Id: e.Id,
ProjectFlockId: e.ProjectFlockId,
KandangId: e.KandangId,
Kandang: kandang,
ProjectFlock: pf,
}
}
@@ -9,6 +9,7 @@ import (
type ProjectFlockKandangRepository interface {
GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error)
GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error)
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error)
@@ -45,6 +46,12 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context) ([]entit
if err := r.db.WithContext(ctx).
Preload("ProjectFlock").
Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location").
Preload("ProjectFlock.CreatedUser").
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
Order("project_flock_id ASC, created_at ASC").
Find(&records).Error; err != nil {
@@ -66,9 +73,34 @@ func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint
if err := r.db.WithContext(ctx).
Preload("ProjectFlock").
Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location").
Preload("ProjectFlock.CreatedUser").
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
First(record, id).Error; err != nil {
return nil, err
}
return record, nil
}
func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error) {
record := new(entity.ProjectFlockKandang)
if err := r.db.WithContext(ctx).
Where("project_flock_id = ? AND kandang_id = ?", projectFlockID, kandangID).
Preload("ProjectFlock").
Preload("ProjectFlock.Flock").
Preload("ProjectFlock.Fcr").
Preload("ProjectFlock.Area").
Preload("ProjectFlock.Location").
Preload("ProjectFlock.CreatedUser").
Preload("ProjectFlock.Kandangs").
Preload("ProjectFlock.KandangHistory").
Preload("Kandang").
First(record).Error; err != nil {
return nil, err
}
return record, nil
}
@@ -25,6 +25,7 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
route.Post("/:id/approvals", ctrl.Approval)
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
route.Post("/approvals", ctrl.Approval)
route.Get("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary)
}
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strconv"
"strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
@@ -28,8 +29,9 @@ type ProjectflockService interface {
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, error)
GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error)
Approval(ctx *fiber.Ctx, id uint, req *validation.Approve) (*entity.ProjectFlock, error)
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error)
}
type projectflockService struct {
@@ -75,7 +77,7 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB {
Preload("Area").
Preload("Fcr").
Preload("Location").
Preload("Kandangs.Location")
Preload("Kandangs")
}
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
@@ -501,17 +503,11 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
return s.GetOne(c, id)
}
func (s projectflockService) Approval(c *fiber.Ctx, id uint, req *validation.Approve) (*entity.ProjectFlock, error) {
func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
project, err := s.GetOne(c, id)
if err != nil {
s.Log.Errorf("Failed to fetch projectflock %d before approval: %+v", id, err)
return nil, err
}
actorID := uint(1) // TODO: change from auth context
var action entity.ApprovalAction
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
@@ -523,42 +519,58 @@ func (s projectflockService) Approval(c *fiber.Ctx, id uint, req *validation.App
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
}
approvableIDs := uniqueUintSlice(req.ApprovableIds)
if len(approvableIDs) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
}
step := utils.ProjectFlockStepPengajuan
if action == entity.ApprovalActionApproved {
step = utils.ProjectFlockStepAktif
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
if _, err := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlock,
project.Id,
step,
&action,
actorID,
req.Notes,
); err != nil {
return err
}
kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
switch action {
case entity.ApprovalActionApproved:
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
for _, approvableID := range approvableIDs {
if _, err := projectRepoTx.GetByID(c.Context(), approvableID, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Projectflock %d not found", approvableID))
}
return err
}
if _, err := approvalSvc.CreateApproval(
c.Context(),
project.Id,
utils.KandangStatusActive,
utils.ApprovalWorkflowProjectFlock,
approvableID,
step,
&action,
actorID,
req.Notes,
); err != nil {
return err
}
case entity.ApprovalActionRejected:
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
c.Context(),
project.Id,
utils.KandangStatusNonActive,
); err != nil {
return err
switch action {
case entity.ApprovalActionApproved:
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
c.Context(),
approvableID,
utils.KandangStatusActive,
); err != nil {
return err
}
case entity.ApprovalActionRejected:
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
c.Context(),
approvableID,
utils.KandangStatusNonActive,
); err != nil {
return err
}
}
}
@@ -566,14 +578,26 @@ func (s projectflockService) Approval(c *fiber.Ctx, id uint, req *validation.App
})
if err != nil {
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
}
s.Log.Errorf("Failed to record approval for projectflock %d: %+v", id, err)
s.Log.Errorf("Failed to record approval for projectflocks %+v: %+v", approvableIDs, err)
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
}
return s.GetOne(c, id)
updated := make([]entity.ProjectFlock, 0, len(approvableIDs))
for _, approvableID := range approvableIDs {
project, err := s.GetOne(c, approvableID)
if err != nil {
return nil, err
}
updated = append(updated, *project)
}
return updated, nil
}
func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
@@ -617,6 +641,57 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
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) {
pfk, err := s.PivotRepo.GetByProjectFlockAndKandang(ctx.Context(), projectFlockID, kandangID)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
}
return nil, err
}
return pfk, nil
}
func (s projectflockService) GetProjectFlockKandangByParams(ctx *fiber.Ctx, idStr string, projectFlockIdStr string, kandangIdStr string) (*entity.ProjectFlockKandang, error) {
idStr = strings.TrimSpace(idStr)
projectFlockIdStr = strings.TrimSpace(projectFlockIdStr)
kandangIdStr = strings.TrimSpace(kandangIdStr)
if idStr != "" {
id, err := strconv.Atoi(idStr)
if err != nil || id <= 0 {
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 == "" {
return nil, fiber.NewError(fiber.StatusBadRequest, "Missing lookup parameters")
}
pfid, err := strconv.Atoi(projectFlockIdStr)
if err != nil || pfid <= 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid project_flock_id")
}
kid, err := strconv.Atoi(kandangIdStr)
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) {
flock, err := s.FlockRepo.GetByID(c.Context(), flockID, func(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
@@ -31,6 +31,7 @@ type Query struct {
}
type Approve struct {
Action string `json:"action" validate:"required_strict"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
Action string `json:"action" validate:"required_strict"`
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
}
@@ -1,21 +0,0 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type AuditLogRepository interface {
repository.BaseRepository[entity.AuditLog]
}
type AuditLogRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.AuditLog]
}
func NewAuditLogRepository(db *gorm.DB) AuditLogRepository {
return &AuditLogRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.AuditLog](db),
}
}
@@ -1,21 +0,0 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type StockAvailabilityRepository interface {
repository.BaseRepository[entity.StockAvailability]
}
type StockAvailabilityRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.StockAvailability]
}
func NewStockAvailabilityRepository(db *gorm.DB) StockAvailabilityRepository {
return &StockAvailabilityRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.StockAvailability](db),
}
}