Merge branch 'dev/teguh' into 'feat/BE/US-75/chick-in-doc'

[FIX/BE][US#75] Adjust "chickin delete one" code to match backend standard

See merge request mbugroup/lti-api!49
This commit is contained in:
Hafizh A. Y.
2025-10-31 09:48:27 +00:00
38 changed files with 1287 additions and 424 deletions
@@ -0,0 +1 @@
DROP TABLE IF EXISTS laying_transfers;
@@ -0,0 +1,46 @@
CREATE TABLE IF NOT EXISTS laying_transfers (
id BIGSERIAL PRIMARY KEY,
from_project_flock_id BIGINT NOT NULL,
to_project_flock_id BIGINT NOT NULL,
transfer_date DATE NOT NULL,
total_qty INTEGER,
notes TEXT,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ,
created_by BIGINT
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_from_project_flock
FOREIGN KEY (from_project_flock_id)
REFERENCES project_flocks(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_to_project_flock
FOREIGN KEY (to_project_flock_id)
REFERENCES project_flocks(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE laying_transfers
ADD CONSTRAINT fk_laying_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_laying_transfers_from_project_flock_id ON laying_transfers (from_project_flock_id);
CREATE INDEX IF NOT EXISTS idx_laying_transfers_to_project_flock_id ON laying_transfers (to_project_flock_id);
CREATE INDEX IF NOT EXISTS idx_laying_transfers_created_by ON laying_transfers (created_by);
CREATE INDEX IF NOT EXISTS idx_laying_transfers_deleted_at ON laying_transfers (deleted_at);
@@ -0,0 +1 @@
DROP TABLE IF EXISTS laying_kandang_transfers;
@@ -0,0 +1,40 @@
CREATE TABLE IF NOT EXISTS laying_kandang_transfers (
id BIGSERIAL PRIMARY KEY,
kandang_id BIGINT,
product_warehouse_id BIGINT,
qty NUMERIC(15,3),
laying_transfer_id BIGINT NOT NULL
);
-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada)
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'kandangs') THEN
ALTER TABLE laying_kandang_transfers
ADD CONSTRAINT fk_laying_kandang_transfers_kandang
FOREIGN KEY (kandang_id)
REFERENCES kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE laying_kandang_transfers
ADD CONSTRAINT fk_laying_kandang_transfers_product_warehouse
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 = 'laying_transfers') THEN
ALTER TABLE laying_kandang_transfers
ADD CONSTRAINT fk_laying_kandang_transfers_laying_transfer
FOREIGN KEY (laying_transfer_id)
REFERENCES laying_transfers(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- INDEXES
CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_kandang_id ON laying_kandang_transfers(kandang_id);
CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_product_warehouse_id ON laying_kandang_transfers(product_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_laying_kandang_transfers_laying_transfer_id ON laying_kandang_transfers(laying_transfer_id);
@@ -0,0 +1,5 @@
DROP TABLE IF EXISTS project_chickin_details;
DROP TABLE IF EXISTS project_chickins;
DROP TABLE IF EXISTS project_flock_populations;
@@ -0,0 +1,58 @@
-- ============================================
-- MIGRATION: project_chickins
-- ============================================
-- STEP 1: Hapus tabel jika sudah ada
-- STEP 2: Buat tabel project_chickins
CREATE TABLE IF NOT EXISTS project_chickins (
id BIGSERIAL PRIMARY KEY,
project_flock_kandang_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
chick_in_date DATE NOT NULL,
usage_qty NUMERIC(15, 3) NOT NULL,
pending_usage_qty NUMERIC(15, 3) DEFAULT 0,
notes TEXT,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- STEP 3: FOREIGN KEYS
DO $$
BEGIN
-- Relasi ke project_flock_kandangs
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_kandang
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
-- Relasi ke product_warehouses
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_warehouse
FOREIGN KEY (product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
-- Relasi ke users
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_chickins
ADD CONSTRAINT fk_project_chickins_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- STEP 4: INDEXES
CREATE INDEX IF NOT EXISTS idx_chickins_kandang_id ON project_chickins (project_flock_kandang_id);
CREATE INDEX IF NOT EXISTS idx_chickins_warehouse_id ON project_chickins (product_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_chickins_created_by ON project_chickins (created_by);
@@ -0,0 +1,57 @@
-- ============================================
-- MIGRATION: project_flock_populations
-- ============================================
-- STEP 1: Hapus tabel jika sudah ada
-- STEP 2: Buat tabel project_flock_populations
CREATE TABLE IF NOT EXISTS project_flock_populations (
id BIGSERIAL PRIMARY KEY,
project_chickin_id BIGINT NOT NULL,
product_warehouse_id BIGINT NOT NULL,
total_qty NUMERIC(15, 3) NOT NULL,
total_used_qty NUMERIC(15, 3) DEFAULT 0,
notes TEXT,
created_by BIGINT NOT NULL,
created_at TIMESTAMPTZ DEFAULT now(),
updated_at TIMESTAMPTZ DEFAULT now(),
deleted_at TIMESTAMPTZ
);
-- STEP 3: FOREIGN KEYS
DO $$
BEGIN
-- Relasi ke project_chickins
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_chickins') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_populations_chickin
FOREIGN KEY (project_chickin_id)
REFERENCES project_chickins(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
-- Relasi ke product_warehouses
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'product_warehouses') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_populations_warehouse
FOREIGN KEY (product_warehouse_id)
REFERENCES product_warehouses(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
-- Relasi ke users
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN
ALTER TABLE project_flock_populations
ADD CONSTRAINT fk_project_flock_populations_created_by
FOREIGN KEY (created_by)
REFERENCES users(id)
ON DELETE RESTRICT ON UPDATE CASCADE;
END IF;
END $$;
-- STEP 4: INDEXES
CREATE INDEX IF NOT EXISTS idx_populations_chickin_id ON project_flock_populations (project_chickin_id);
CREATE INDEX IF NOT EXISTS idx_populations_warehouse_id ON project_flock_populations (product_warehouse_id);
CREATE INDEX IF NOT EXISTS idx_populations_created_by ON project_flock_populations (created_by);
+136 -136
View File
@@ -93,9 +93,9 @@ func Run(db *gorm.DB) error {
if err := seedTransferStock(tx, adminID); err != nil { if err := seedTransferStock(tx, adminID); err != nil {
return err return err
} }
if err := seedChickin(tx, adminID); err != nil { // if err := seedChickin(tx, adminID); err != nil {
return err // return err
} // }
fmt.Println("✅ Master data seeding completed") fmt.Println("✅ Master data seeding completed")
return nil return nil
@@ -1134,151 +1134,151 @@ func seedTransferStock(tx *gorm.DB, createdBy uint) error {
return nil return nil
} }
func seedChickin(tx *gorm.DB, createdBy uint) error { // func seedChickin(tx *gorm.DB, createdBy uint) error {
seeds := []struct { // seeds := []struct {
ProjectFlockKandangId uint // ProjectFlockKandangId uint
ChickInDate string // ChickInDate string
Quantity float64 // Quantity float64
Note string // Note string
}{ // }{
{ProjectFlockKandangId: 1, ChickInDate: "2025-10-20", Quantity: 100, Note: "Seeder chickin 1"}, // {ProjectFlockKandangId: 1, ChickInDate: "2025-10-20", Quantity: 100, Note: "Seeder chickin 1"},
{ProjectFlockKandangId: 2, ChickInDate: "2025-10-21", Quantity: 200, Note: "Seeder chickin 2"}, // {ProjectFlockKandangId: 2, ChickInDate: "2025-10-21", Quantity: 200, Note: "Seeder chickin 2"},
} // }
for _, seed := range seeds { // for _, seed := range seeds {
chickinDate, err := time.Parse("2006-01-02", seed.ChickInDate) // chickinDate, err := time.Parse("2006-01-02", seed.ChickInDate)
if err != nil { // if err != nil {
return err // return err
} // }
// Insert ProjectChickin jika belum ada // // Insert ProjectChickin jika belum ada
var chickin entity.ProjectChickin // var chickin entity.ProjectChickin
err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", seed.ProjectFlockKandangId, chickinDate). // err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", seed.ProjectFlockKandangId, chickinDate).
First(&chickin).Error // First(&chickin).Error
if errors.Is(err, gorm.ErrRecordNotFound) { // if errors.Is(err, gorm.ErrRecordNotFound) {
chickin = entity.ProjectChickin{ // chickin = entity.ProjectChickin{
ProjectFlockKandangId: seed.ProjectFlockKandangId, // ProjectFlockKandangId: seed.ProjectFlockKandangId,
ChickInDate: chickinDate, // ChickInDate: chickinDate,
Quantity: seed.Quantity, // Quantity: seed.Quantity,
Note: seed.Note, // Note: seed.Note,
CreatedBy: createdBy, // CreatedBy: createdBy,
} // }
if err := tx.Create(&chickin).Error; err != nil { // if err := tx.Create(&chickin).Error; err != nil {
return err // return err
} // }
} else if err != nil { // } else if err != nil {
return err // return err
} // }
var population entity.ProjectFlockPopulation // var population entity.ProjectFlockPopulation
err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error // err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error
if errors.Is(err, gorm.ErrRecordNotFound) { // if errors.Is(err, gorm.ErrRecordNotFound) {
population = entity.ProjectFlockPopulation{ // population = entity.ProjectFlockPopulation{
ProjectFlockKandangId: seed.ProjectFlockKandangId, // ProjectFlockKandangId: seed.ProjectFlockKandangId,
InitialQuantity: seed.Quantity, // InitialQuantity: seed.Quantity,
CurrentQuantity: seed.Quantity, // CurrentQuantity: seed.Quantity,
ReservedQuantity: 0, // ReservedQuantity: 0,
CreatedBy: createdBy, // CreatedBy: createdBy,
} // }
if err := tx.Create(&population).Error; err != nil { // if err := tx.Create(&population).Error; err != nil {
return err // return err
} // }
} else if err != nil { // } else if err != nil {
return err // return err
} else { // } else {
// Update population quantities // // Update population quantities
if err := tx.Model(&entity.ProjectFlockPopulation{}). // if err := tx.Model(&entity.ProjectFlockPopulation{}).
Where("id = ?", population.Id). // Where("id = ?", population.Id).
Updates(map[string]any{ // Updates(map[string]any{
"initial_quantity": population.InitialQuantity + seed.Quantity, // "initial_quantity": population.InitialQuantity + seed.Quantity,
"current_quantity": population.CurrentQuantity + seed.Quantity, // "current_quantity": population.CurrentQuantity + seed.Quantity,
"reserved_quantity": 0, // "reserved_quantity": 0,
}).Error; err != nil { // }).Error; err != nil {
return err // return err
} // }
} // }
var pfk entity.ProjectFlockKandang // var pfk entity.ProjectFlockKandang
if err := tx.Where("id = ?", seed.ProjectFlockKandangId).First(&pfk).Error; err != nil { // if err := tx.Where("id = ?", seed.ProjectFlockKandangId).First(&pfk).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { // if errors.Is(err, gorm.ErrRecordNotFound) {
// no pivot found; skip creating details // // no pivot found; skip creating details
continue // continue
} // }
return err // return err
} // }
var warehouse entity.Warehouse // var warehouse entity.Warehouse
if err := tx.Where("kandang_id = ?", pfk.KandangId).First(&warehouse).Error; err != nil { // if err := tx.Where("kandang_id = ?", pfk.KandangId).First(&warehouse).Error; err != nil {
// if warehouse not found, cannot create details // // if warehouse not found, cannot create details
if errors.Is(err, gorm.ErrRecordNotFound) { // if errors.Is(err, gorm.ErrRecordNotFound) {
continue // continue
} // }
return err // return err
} // }
var productWarehouses []entity.ProductWarehouse // var productWarehouses []entity.ProductWarehouse
err = tx.Table("product_warehouses"). // err = tx.Table("product_warehouses").
Select("product_warehouses.*"). // Select("product_warehouses.*").
Joins("JOIN products ON products.id = product_warehouses.product_id"). // Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN product_categories ON product_categories.id = products.product_category_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). // Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", "DOC", warehouse.Id).
Order("product_warehouses.created_at DESC"). // Order("product_warehouses.created_at DESC").
Find(&productWarehouses).Error // Find(&productWarehouses).Error
if err != nil { // if err != nil {
return err // return err
} // }
// If no product warehouses found, keep existing chickin.Quantity and skip details // // If no product warehouses found, keep existing chickin.Quantity and skip details
if len(productWarehouses) == 0 { // if len(productWarehouses) == 0 {
continue // continue
} // }
// sum all pw quantities and set chickin.Quantity to that total (mimic CreateOne) // // sum all pw quantities and set chickin.Quantity to that total (mimic CreateOne)
totalQty := 0.0 // totalQty := 0.0
for _, pw := range productWarehouses { // for _, pw := range productWarehouses {
totalQty += pw.Quantity // totalQty += pw.Quantity
} // }
if chickin.Quantity != totalQty { // if chickin.Quantity != totalQty {
if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Update("quantity", totalQty).Error; err != nil { // if err := tx.Model(&entity.ProjectChickin{}).Where("id = ?", chickin.Id).Update("quantity", totalQty).Error; err != nil {
return err // return err
} // }
chickin.Quantity = totalQty // chickin.Quantity = totalQty
} // }
for _, pw := range productWarehouses { // for _, pw := range productWarehouses {
// ensure detail exists or create it with full pw.Quantity // // ensure detail exists or create it with full pw.Quantity
var detail entity.ProjectChickinDetail // var detail entity.ProjectChickinDetail
err = tx.Where("project_chickin_id = ? AND product_warehouse_id = ?", chickin.Id, pw.Id).First(&detail).Error // err = tx.Where("project_chickin_id = ? AND product_warehouse_id = ?", chickin.Id, pw.Id).First(&detail).Error
if errors.Is(err, gorm.ErrRecordNotFound) { // if errors.Is(err, gorm.ErrRecordNotFound) {
detail = entity.ProjectChickinDetail{ // detail = entity.ProjectChickinDetail{
ProjectChickinId: chickin.Id, // ProjectChickinId: chickin.Id,
ProductWarehouseId: pw.Id, // ProductWarehouseId: pw.Id,
Quantity: pw.Quantity, // Quantity: pw.Quantity,
CreatedBy: createdBy, // CreatedBy: createdBy,
} // }
if err := tx.Create(&detail).Error; err != nil { // if err := tx.Create(&detail).Error; err != nil {
return err // return err
} // }
} else if err != nil { // } else if err != nil {
return err // return err
} else { // } else {
if detail.Quantity != pw.Quantity { // if detail.Quantity != pw.Quantity {
if err := tx.Model(&entity.ProjectChickinDetail{}).Where("id = ?", detail.Id).Update("quantity", pw.Quantity).Error; err != nil { // if err := tx.Model(&entity.ProjectChickinDetail{}).Where("id = ?", detail.Id).Update("quantity", pw.Quantity).Error; err != nil {
return err // return err
} // }
} // }
} // }
// zero out pw quantity // // zero out pw quantity
if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("quantity", 0).Error; err != nil { // if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("quantity", 0).Error; err != nil {
return err // return err
} // }
} // }
} // }
return nil // return nil
} // }
func ptr[T any](v T) *T { func ptr[T any](v T) *T {
return &v return &v
@@ -0,0 +1,22 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingKandangTransfer struct {
Id uint `gorm:"primaryKey"`
KandangId uint
ProductWarehouseId uint
Qty float64 `gorm:"type:numeric(15,3)"`
LayingTransferId uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
LayingTransfer *LayingTransfer `gorm:"foreignKey:LayingTransferId;references:Id"`
}
+24
View File
@@ -0,0 +1,24 @@
package entities
import (
"time"
"gorm.io/gorm"
)
type LayingTransfer struct {
Id uint `gorm:"primaryKey"`
FromProjectFlockId uint `gorm:"not null"`
ToProjectFlockId uint `gorm:"not null"`
TotalQty float64 `gorm:"type:numeric(15,3)"`
TransferDate time.Time `gorm:"type:date;not null"`
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
FromProjectFlock *ProjectFlock `gorm:"foreignKey:FromProjectFlockId;references:Id"`
ToProjectFlock *ProjectFlock `gorm:"foreignKey:ToProjectFlockId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
}
+7 -4
View File
@@ -12,13 +12,16 @@ type ProjectChickin struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null"` ProjectFlockKandangId uint `gorm:"not null"`
ChickInDate time.Time `gorm:"not null"` ChickInDate time.Time `gorm:"not null"`
Quantity float64 `gorm:"not null"` ProductWarehouseId uint `gorm:"not null"`
Note string `gorm:"type:text"` UsageQty float64 `gorm:"type:numeric(15,3);not null"`
PendingUsageQty float64 `gorm:"type:numeric(15,3);default:0"`
Notes string `gorm:"type:text"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
} }
+13 -11
View File
@@ -7,16 +7,18 @@ import (
) )
type ProjectFlockPopulation struct { type ProjectFlockPopulation struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
ProjectFlockKandangId uint `gorm:"not null"` ProjectChickinId uint `gorm:"not null"`
InitialQuantity float64 `gorm:"type:numeric(15,3);not null"` ProductWarehouseId uint `gorm:"not null"`
CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"` TotalQty float64 `gorm:"type:numeric(15,3);not null"`
ReservedQuantity float64 `gorm:"type:numeric(15,3)"` TotalUsedQty float64 `gorm:"type:numeric(15,3);not null"`
CreatedBy uint `gorm:"not null"` Notes string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedBy uint `gorm:"not null"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
} }
+1
View File
@@ -28,3 +28,4 @@ type ProjectFlock struct {
KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"` KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"`
LatestApproval *Approval `gorm:"-" json:"-"` LatestApproval *Approval `gorm:"-" json:"-"`
} }
+8 -6
View File
@@ -3,10 +3,12 @@ package entities
import "time" import "time"
type ProjectFlockKandang struct { type ProjectFlockKandang struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"` ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"` KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
} }
@@ -82,6 +82,10 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} {
"LOKASI", "LOKASI",
"KANDANG", "KANDANG",
}, },
"stock_log": map[string][]string{
"log_types": []string{"TRANSFER", "ADJUSTMENT"},
"transaction_types": []string{"INCREASE", "DECREASE"},
},
"supplier_categories": []string{ "supplier_categories": []string{
"BOP", "BOP",
"SAPRONAK", "SAPRONAK",
@@ -17,6 +17,9 @@ type ProductWarehouseRepository interface {
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) GetByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) ([]entity.ProductWarehouse, error)
GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) (*entity.ProductWarehouse, error)
WithTxRepo(tx *gorm.DB) ProductWarehouseRepository
DB() *gorm.DB
} }
type ProductWarehouseRepositoryImpl struct { type ProductWarehouseRepositoryImpl struct {
@@ -31,6 +34,15 @@ func NewProductWarehouseRepository(db *gorm.DB) ProductWarehouseRepository {
} }
} }
func (r *ProductWarehouseRepositoryImpl) WithTxRepo(tx *gorm.DB) ProductWarehouseRepository {
return &ProductWarehouseRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProductWarehouse](tx),
db: tx,
}
}
func (r *ProductWarehouseRepositoryImpl) DB() *gorm.DB {
return r.db
}
func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) { func (r *ProductWarehouseRepositoryImpl) IsProductExist(ctx context.Context, productId uint) (bool, error) {
return repository.Exists[entity.Product](ctx, r.db, productId) return repository.Exists[entity.Product](ctx, r.db, productId)
} }
@@ -89,3 +101,20 @@ func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx con
} }
return productWarehouses, nil return productWarehouses, nil
} }
func (r *ProductWarehouseRepositoryImpl) GetLatestByCategoryCodeAndWarehouseID(ctx context.Context, categoryCode string, warehouseId uint) (*entity.ProductWarehouse, error) {
var productWarehouse 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").
Limit(1).
First(&productWarehouse).Error
if err != nil {
return nil, err
}
return &productWarehouse, nil
}
@@ -139,23 +139,33 @@ func (u *ChickinController) DeleteOne(c *fiber.Ctx) error {
}) })
} }
func (u *ChickinController) Approve(c *fiber.Ctx) error { func (u *ChickinController) Approval(c *fiber.Ctx) error {
param := c.Params("id") req := new(validation.Approve)
if err := c.BodyParser(req); err != nil {
id, err := strconv.Atoi(param) return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
} }
if err := u.ChickinService.Approve(c, uint(id)); err != nil { results, err := u.ChickinService.Approval(c, req)
if err != nil {
return err return err
} }
var (
data interface{}
message = "Submit chickin approval successfully"
)
if len(results) == 1 {
data = dto.ToChickinListDTO(results[0])
} else {
message = "Submit chickin approvals successfully"
data = dto.ToChickinListDTOs(results)
}
return c.Status(fiber.StatusOK). return c.Status(fiber.StatusOK).
JSON(response.Success{ JSON(response.Success{
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Approve chickin successfully", Message: message,
Data: nil, Data: data,
}) })
} }
@@ -18,8 +18,10 @@ type ChickinBaseDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"` ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"`
ChickInDate time.Time `json:"chick_in_date"` ChickInDate time.Time `json:"chick_in_date"`
Quantity float64 `json:"quantity"` ProductWarehouseId uint `json:"product_warehouse_id"`
Note string `json:"note"` UsageQty float64 `json:"usage_qty"`
PendingUsageQty float64 `json:"pending_usage_qty"`
Notes string `json:"notes"`
} }
type ProjectFlockDTO struct { type ProjectFlockDTO struct {
@@ -44,8 +46,10 @@ type ChickinSimpleDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
ProjectFlockKandangId uint `json:"project_flock_kandang_id"` ProjectFlockKandangId uint `json:"project_flock_kandang_id"`
ChickInDate time.Time `json:"chick_in_date"` ChickInDate time.Time `json:"chick_in_date"`
Quantity float64 `json:"quantity"` ProductWarehouseId uint `json:"product_warehouse_id"`
Note string `json:"note"` UsageQty float64 `json:"usage_qty"`
PendingUsageQty float64 `json:"pending_usage_qty"`
Notes string `json:"notes"`
CreatedBy uint `json:"created_by"` CreatedBy uint `json:"created_by"`
} }
@@ -138,16 +142,18 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD
func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO {
var pfk *ProjectFlockKandangDTO var pfk *ProjectFlockKandangDTO
if e.ProjectFlockKandang.Id != 0 { if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 {
mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang) mapped := ToProjectFlockKandangDTO(*e.ProjectFlockKandang)
pfk = &mapped pfk = &mapped
} }
return ChickinBaseDTO{ return ChickinBaseDTO{
Id: e.Id, Id: e.Id,
ProjectFlockKandang: pfk, ProjectFlockKandang: pfk,
ChickInDate: e.ChickInDate, ChickInDate: e.ChickInDate,
Quantity: e.Quantity, ProductWarehouseId: e.ProductWarehouseId,
Note: e.Note, UsageQty: e.UsageQty,
PendingUsageQty: e.PendingUsageQty,
Notes: e.Notes,
} }
} }
@@ -156,21 +162,23 @@ func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO {
Id: e.Id, Id: e.Id,
ProjectFlockKandangId: e.ProjectFlockKandangId, ProjectFlockKandangId: e.ProjectFlockKandangId,
ChickInDate: e.ChickInDate, ChickInDate: e.ChickInDate,
Quantity: e.Quantity, ProductWarehouseId: e.ProductWarehouseId,
Note: e.Note, UsageQty: e.UsageQty,
PendingUsageQty: e.PendingUsageQty,
Notes: e.Notes,
CreatedBy: e.CreatedBy, CreatedBy: e.CreatedBy,
} }
} }
func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO { func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO {
var createdUser *userBaseDTO.UserBaseDTO var createdUser *userBaseDTO.UserBaseDTO
if e.CreatedUser.Id != 0 { if e.CreatedUser != nil && e.CreatedUser.Id != 0 {
mapped := userBaseDTO.ToUserBaseDTO(e.CreatedUser) mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped createdUser = &mapped
} }
var pfk *ProjectFlockKandangDTO var pfk *ProjectFlockKandangDTO
if e.ProjectFlockKandang.Id != 0 { if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 {
mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang) mapped := ToProjectFlockKandangDTO(*e.ProjectFlockKandang)
pfk = &mapped pfk = &mapped
} }
return ChickinListDTO{ return ChickinListDTO{
@@ -1,10 +1,15 @@
package chickins package chickins
import ( import (
"fmt"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
"gorm.io/gorm" "gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/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" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
@@ -15,6 +20,8 @@ import (
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
) )
type ChickinModule struct{} type ChickinModule struct{}
@@ -32,6 +39,12 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
}
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate) chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
@@ -1,6 +1,8 @@
package repository package repository
import ( import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm" "gorm.io/gorm"
@@ -8,6 +10,10 @@ import (
type ProjectChickinDetailRepository interface { type ProjectChickinDetailRepository interface {
repository.BaseRepository[entity.ProjectChickinDetail] repository.BaseRepository[entity.ProjectChickinDetail]
CreateOne(ctx context.Context, entity *entity.ProjectChickinDetail, modifier func(*gorm.DB) *gorm.DB) error
DeleteMany(ctx context.Context, modifier func(*gorm.DB) *gorm.DB) error
GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error)
WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository
} }
type ChickinDetailRepositoryImpl struct { type ChickinDetailRepositoryImpl struct {
@@ -19,3 +25,22 @@ func NewChickinDetailRepository(db *gorm.DB) ProjectChickinDetailRepository {
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](db), BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](db),
} }
} }
func (r *ChickinDetailRepositoryImpl) WithTxRepo(tx *gorm.DB) ProjectChickinDetailRepository {
return &ChickinDetailRepositoryImpl{BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickinDetail](tx)}
}
func (r *ChickinDetailRepositoryImpl) DB() *gorm.DB {
return r.BaseRepositoryImpl.DB()
}
func (r *ChickinDetailRepositoryImpl) GetByProjectChickinID(ctx context.Context, projectChickinID uint) ([]entity.ProjectChickinDetail, error) {
var records []entity.ProjectChickinDetail
if err := r.DB().WithContext(ctx).Where("project_chickin_id = ?", projectChickinID).Find(&records).Error; err != nil {
return nil, err
}
if len(records) == 0 {
return nil, gorm.ErrRecordNotFound
}
return records, nil
}
@@ -25,5 +25,5 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService
route.Get("/:id", ctrl.GetOne) route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne) route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne) route.Delete("/:id", ctrl.DeleteOne)
route.Post("/:id/approve", ctrl.Approve) route.Post("/approvals", ctrl.Approval)
} }
@@ -2,7 +2,11 @@ package service
import ( import (
"errors" "errors"
"fmt"
"strings"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
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"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
@@ -24,7 +28,7 @@ type ChickinService interface {
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error)
DeleteOne(ctx *fiber.Ctx, id uint) error DeleteOne(ctx *fiber.Ctx, id uint) error
Approve(ctx *fiber.Ctx, id uint) error Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error)
} }
type chickinService struct { type chickinService struct {
@@ -77,6 +81,7 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
} }
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
chickins, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { chickins, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db) db = s.withRelations(db)
if params.ProjectFlockKandangId != 0 { if params.ProjectFlockKandangId != 0 {
@@ -109,107 +114,98 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
return nil, err return nil, err
} }
projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId) projectFlockKandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId)
if err != nil { if err != nil {
s.Log.Errorf("Failed to get projectflock kandang: %+v", err) return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found")
return nil, err
} }
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId) warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.KandangId)
if err != nil { if err != nil {
s.Log.Errorf("Failed to get warehouse: %+v", err) return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found")
return nil, err
} }
// move complex DB query into repository for cleaner service var productWarehouses []entity.ProductWarehouse
productWarehouses, err := s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id)
if err != nil {
s.Log.Errorf("Failed to get product warehouses: %+v", err)
return nil, err
}
if len(productWarehouses) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse")
}
totalQuantity := 0.0
for _, pw := range productWarehouses {
totalQuantity += pw.Quantity
}
if totalQuantity < 1 { if strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category)) == string(utils.ProjectFlockCategoryGrowing) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses")
productWarehouses, err = s.ProductWarehouseRepo.GetByCategoryCodeAndWarehouseID(c.Context(), "DOC", warehouse.Id)
if err != nil || len(productWarehouses) == 0 {
return nil, fiber.NewError(fiber.StatusNotFound, "Product for growing category in the Kandang's warehouse not found")
}
} }
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)
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format")
} }
newChickin := &entity.ProjectChickin{
ProjectFlockKandangId: projectflockkandang.Id, actorID := uint(1) // todo nanti ambil dari auth context
ChickInDate: chickinDate, newChikins := make([]*entity.ProjectChickin, 0)
Quantity: totalQuantity, for _, productWarehouse := range productWarehouses {
Note: req.Note,
CreatedBy: 1, //todo: ganti dengan user login if productWarehouse.Quantity > 0 {
newChickin := &entity.ProjectChickin{
ProjectFlockKandangId: req.ProjectFlockKandangId,
ChickInDate: chickinDate,
UsageQty: 0,
PendingUsageQty: productWarehouse.Quantity,
ProductWarehouseId: productWarehouse.Id,
Notes: req.Note,
CreatedBy: actorID,
}
newChikins = append(newChikins, newChickin)
}
} }
err = s.Repository.CreateOne(c.Context(), newChickin, nil)
if len(newChikins) == 0 {
return nil, fiber.NewError(fiber.StatusBadRequest, "No chickins to create")
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
if err := s.Repository.WithTx(dbTransaction).CreateMany(c.Context(), newChikins, nil); err != nil {
return err
}
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
if err != nil {
return err
}
for _, chickin := range newChikins {
updates := map[string]any{"quantity": 0}
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fmt.Errorf("failed to update product warehouse quantity for id %d", chickin.ProductWarehouseId)
}
return err
}
}
if latest == nil {
action := entity.ApprovalActionCreated
if _, err := approvalSvcTx.CreateApproval(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, utils.ProjectFlockKandangStepPengajuan, &action, actorID, nil); err != nil {
lower := strings.ToLower(err.Error())
if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) {
return err
}
}
}
return nil
})
if err != nil { if err != nil {
s.Log.Errorf("Failed to create chickin: %+v", err) return nil, fiber.NewError(fiber.StatusBadRequest, err.Error())
return nil, err
} }
// Update semua product warehouse: set quantity jadi 0 return newChikins[0], nil
for _, pw := range productWarehouses {
err = s.ProductWarehouseRepo.PatchOne(c.Context(), pw.Id, map[string]any{
"quantity": 0,
}, nil)
if err != nil {
s.Log.Errorf("Failed to update product warehouse quantity: %+v", err)
return nil, err
}
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)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
s.Log.Errorf("Failed to get project flock population: %+v", err)
return nil, err
}
if existingPopulation != nil {
err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), existingPopulation.Id, map[string]any{
"reserved_quantity": newChickin.Quantity + existingPopulation.ReservedQuantity,
}, nil)
if err != nil {
s.Log.Errorf("Failed to update project flock population: %+v", err)
return nil, err
}
} else {
newPopulation := &entity.ProjectFlockPopulation{
ProjectFlockKandangId: req.ProjectFlockKandangId,
InitialQuantity: 0,
CurrentQuantity: 0,
ReservedQuantity: newChickin.Quantity,
CreatedBy: 1, // todo: ganti dengan user login
}
err = s.ProjectflockPopulationRepo.CreateOne(c.Context(), newPopulation, nil)
if err != nil {
s.Log.Errorf("Failed to create project flock population: %+v", err)
return nil, err
}
}
return s.GetOne(c, newChickin.Id)
} }
func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) { func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) {
@@ -223,7 +219,8 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
updateBody["chick_in_date"] = req.ChickInDate updateBody["chick_in_date"] = req.ChickInDate
} }
if req.Note != "" { if req.Note != "" {
updateBody["note"] = req.Note // entity uses `Notes` => column `notes`
updateBody["notes"] = req.Note
} }
if len(updateBody) == 0 { if len(updateBody) == 0 {
return s.GetOne(c, id) return s.GetOne(c, id)
@@ -241,178 +238,141 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
} }
func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
db := s.Repository.DB()
tx := db.WithContext(c.Context()).Begin() // Simplified delete: directly call repository delete. Complex restore logic removed for now.
if tx.Error != nil { if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
s.Log.Errorf("Failed to begin transaction: %+v", tx.Error) if errors.Is(err, gorm.ErrRecordNotFound) {
return tx.Error return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
}
rollback := func(err error) error {
if rerr := tx.Rollback().Error; rerr != nil {
s.Log.Errorf("Rollback failed: %+v", rerr)
} }
return err return err
} }
chickinRepoTx := s.Repository.WithTx(tx) return nil
pfkRepoTx := s.ProjectflockKandangRepo.WithTx(tx) }
productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(tx)
chickin, err := chickinRepoTx.GetByID(c.Context(), id, nil) func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) {
if errors.Is(err, gorm.ErrRecordNotFound) { if err := s.Validate.Struct(req); err != nil {
return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found")) return nil, err
}
if err != nil {
s.Log.Errorf("Failed get chickin by id: %+v", err)
return rollback(err)
} }
var population entity.ProjectFlockPopulation actorID := uint(1) // todo nanti ambil dari auth context
if err := tx.WithContext(c.Context()).Where("project_flock_kandang_id = ?", chickin.ProjectFlockKandangId).First(&population).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { var action entity.ApprovalAction
return rollback(fiber.NewError(fiber.StatusNotFound, "Project flock population not found")) switch strings.ToUpper(strings.TrimSpace(req.Action)) {
} case string(entity.ApprovalActionRejected):
s.Log.Errorf("Failed to get project flock population: %+v", err) action = entity.ApprovalActionRejected
return rollback(err) case string(entity.ApprovalActionApproved):
action = entity.ApprovalActionApproved
default:
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
} }
newReserved := population.ReservedQuantity - chickin.Quantity approvableIDs := uniqueUintSlice(req.ApprovableIds)
if newReserved < 0 { if len(approvableIDs) == 0 {
newReserved = 0 return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
}
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)
} }
restoreFromDetails := func() (bool, error) { step := utils.ProjectFlockKandangStepPengajuan
var details []entity.ProjectChickinDetail if action == entity.ApprovalActionApproved {
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Find(&details).Error; err != nil { step = utils.ProjectFlockKandangStepDisetujui
return false, err }
}
if len(details) == 0 {
return false, nil
}
for _, d := range details { err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
var pw entity.ProductWarehouse approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
if err := tx.WithContext(c.Context()).Where("id = ?", d.ProductWarehouseId).First(&pw).Error; err != nil { ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
if errors.Is(err, gorm.ErrRecordNotFound) { chickinRepoTx := s.Repository.WithTx(dbTransaction)
continue for _, approvableID := range approvableIDs {
exists, err := s.ProjectflockKandangRepo.WithTx(dbTransaction).IdExists(c.Context(), approvableID)
if err != nil {
return err
}
if !exists {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("ProjectFlockKandang %d not found", approvableID))
}
if _, err := approvalSvc.CreateApproval(
c.Context(),
utils.ApprovalWorkflowProjectFlockKandang,
approvableID,
step,
&action,
actorID,
req.Notes,
); err != nil {
lower := strings.ToLower(err.Error())
if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) {
return err
} }
return false, err s.Log.Infof("ignored duplicate approval for kandang %d: %v", approvableID, err)
} }
updatedQuantity := pw.Quantity + d.Quantity if action == entity.ApprovalActionApproved {
if err := productWarehouseRepoTx.PatchOne(c.Context(), pw.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil {
return false, err var chickins []entity.ProjectChickin
if err := chickinRepoTx.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", approvableID).Find(&chickins).Error; err != nil {
return err
}
for _, chickin := range chickins {
population := &entity.ProjectFlockPopulation{
ProjectChickinId: chickin.Id,
ProductWarehouseId: chickin.ProductWarehouseId,
TotalQty: chickin.PendingUsageQty,
TotalUsedQty: 0,
Notes: chickin.Notes,
CreatedBy: actorID,
}
if err := ProjectFlockPopulationRepotx.CreateOne(c.Context(), population, nil); err != nil {
lower := strings.ToLower(err.Error())
if !(strings.Contains(lower, "duplicate") || strings.Contains(lower, "unique constraint") || strings.Contains(lower, "23505")) {
return err
}
s.Log.Infof("ignored duplicate population for chickin %d: %v", chickin.Id, err)
}
}
} }
} }
if err := tx.WithContext(c.Context()).Where("project_chickin_id = ?", chickin.Id).Delete(&entity.ProjectChickinDetail{}).Error; err != nil { return nil
return false, err })
}
return true, nil
}
restored, err := restoreFromDetails()
if err != nil { if err != nil {
s.Log.Errorf("Failed to restore from chickin details: %+v", err) if fiberErr, ok := err.(*fiber.Error); ok {
return rollback(err) return nil, fiberErr
}
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) { if errors.Is(err, gorm.ErrRecordNotFound) {
return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found")) return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found")
} }
s.Log.Errorf("Failed to delete chickin: %+v", err) s.Log.Errorf("Failed to record approval for chickins %+v: %+v", approvableIDs, err)
return rollback(err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
} }
if err := tx.Commit().Error; err != nil { updated := make([]entity.ProjectChickin, 0)
s.Log.Errorf("Failed to commit transaction: %+v", err) for _, kandangID := range approvableIDs {
return rollback(err) var chickins []entity.ProjectChickin
if err := s.Repository.DB().WithContext(c.Context()).Where("project_flock_kandang_id = ?", kandangID).Find(&chickins).Error; err != nil {
return nil, err
}
updated = append(updated, chickins...)
} }
return nil return updated, nil
} }
func (s *chickinService) Approve(c *fiber.Ctx, id uint) error { func uniqueUintSlice(values []uint) []uint {
seen := make(map[uint]struct{}, len(values))
// todo: ini contoh akhir jika sudah approved result := make([]uint, 0, len(values))
for _, v := range values {
chickin, err := s.Repository.GetByID( if _, ok := seen[v]; ok {
c.Context(), continue
id, }
nil, seen[v] = struct{}{}
) result = append(result, v)
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Chickin not found")
} }
if err != nil { return result
s.Log.Errorf("Failed get chickin by id: %+v", err)
return 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,
"initial_quantity": population.InitialQuantity + chickin.Quantity,
"current_quantity": population.CurrentQuantity + chickin.Quantity,
}, nil)
if err != nil {
s.Log.Errorf("Failed to update project flock population: %+v", err)
return err
}
return nil
} }
@@ -3,7 +3,7 @@ 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` Note string `json:"note" validate:"omitempty"`
} }
type Update struct { type Update struct {
@@ -16,3 +16,9 @@ type Query struct {
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"` ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"`
} }
type Approve struct {
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"`
}
@@ -9,8 +9,16 @@ import (
) )
type ProjectFlockPopulationRepository interface { type ProjectFlockPopulationRepository interface {
repository.BaseRepository[entity.ProjectFlockPopulation] // domain-specific
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error)
// subset of base repository methods used by services
CreateOne(ctx context.Context, entity *entity.ProjectFlockPopulation, modifier func(*gorm.DB) *gorm.DB) error
PatchOne(ctx context.Context, id uint, updates map[string]any, modifier func(*gorm.DB) *gorm.DB) error
// transaction helpers
WithTx(tx *gorm.DB) ProjectFlockPopulationRepository
DB() *gorm.DB
} }
type projectFlockPopulationRepositoryImpl struct { type projectFlockPopulationRepositoryImpl struct {
@@ -23,6 +31,16 @@ func NewProjectFlockPopulationRepository(db *gorm.DB) ProjectFlockPopulationRepo
} }
} }
func (r *projectFlockPopulationRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockPopulationRepository {
return &projectFlockPopulationRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockPopulation](tx),
}
}
func (r *projectFlockPopulationRepositoryImpl) DB() *gorm.DB {
return r.BaseRepositoryImpl.DB()
}
func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) { func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) {
var record entity.ProjectFlockPopulation var record entity.ProjectFlockPopulation
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
@@ -16,6 +16,7 @@ type ProjectflockRepository interface {
GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error) GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error)
GetMaxPeriodByFlock(ctx context.Context, flockID uint) (int, error) GetMaxPeriodByFlock(ctx context.Context, flockID uint) (int, error)
GetNextPeriodForFlock(ctx context.Context, flockID uint) (int, error) GetNextPeriodForFlock(ctx context.Context, flockID uint) (int, error)
IdExists(ctx context.Context, id uint) (bool, error)
} }
type ProjectflockRepositoryImpl struct { type ProjectflockRepositoryImpl struct {
@@ -28,6 +29,10 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository {
} }
} }
func (r *ProjectflockRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProjectFlock](ctx, r.DB(), id)
}
func (r *ProjectflockRepositoryImpl) GetAllByFlock(ctx context.Context, flockID uint) ([]entity.ProjectFlock, error) { func (r *ProjectflockRepositoryImpl) GetAllByFlock(ctx context.Context, flockID uint) ([]entity.ProjectFlock, error) {
var records []entity.ProjectFlock var records []entity.ProjectFlock
if err := r.DB().WithContext(ctx). if err := r.DB().WithContext(ctx).
@@ -3,6 +3,7 @@ package repository
import ( import (
"context" "context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -14,6 +15,7 @@ type ProjectFlockKandangRepository interface {
DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error)
WithTx(tx *gorm.DB) ProjectFlockKandangRepository WithTx(tx *gorm.DB) ProjectFlockKandangRepository
IdExists(ctx context.Context, id uint) (bool, error)
DB() *gorm.DB DB() *gorm.DB
} }
@@ -67,6 +69,9 @@ func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKand
func (r *projectFlockKandangRepositoryImpl) DB() *gorm.DB { func (r *projectFlockKandangRepositoryImpl) DB() *gorm.DB {
return r.db return r.db
} }
func (r *projectFlockKandangRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.ProjectFlockKandang](ctx, r.db, id)
}
func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) { func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) {
record := new(entity.ProjectFlockKandang) record := new(entity.ProjectFlockKandang)
@@ -670,18 +670,19 @@ func (s *recordingService) getPreviousRecording(tx *gorm.DB, projectFlockKandang
} }
func (s *recordingService) getTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) { func (s *recordingService) getTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) {
var population entity.ProjectFlockPopulation // var population entity.ProjectFlockPopulation
err := tx. // err := tx.
Where("project_flock_kandang_id = ?", projectFlockKandangId). // Where("project_flock_kandang_id = ?", projectFlockKandangId).
Order("created_at DESC"). // Order("created_at DESC").
First(&population).Error // First(&population).Error
if errors.Is(err, gorm.ErrRecordNotFound) { // if errors.Is(err, gorm.ErrRecordNotFound) {
return 0, nil // return 0, nil
} // }
if err != nil { // if err != nil {
return 0, err // return 0, err
} // }
return int64(math.Round(population.InitialQuantity)), nil //todo : nanti ganti lagi mas saya hardcode dulu
return int64(math.Round(1000)), nil
} }
func (s *recordingService) getAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) { func (s *recordingService) getAverageBodyWeight(tx *gorm.DB, recordingID uint) (float64, error) {
+3 -1
View File
@@ -10,6 +10,7 @@ import (
chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins" chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins"
projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks" projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks"
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings" recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
transferLayings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings"
// MODULE IMPORTS // MODULE IMPORTS
) )
@@ -20,8 +21,9 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida
projectflocks.ProjectflockModule{}, projectflocks.ProjectflockModule{},
recordings.RecordingModule{}, recordings.RecordingModule{},
chickins.ChickinModule{}, chickins.ChickinModule{},
transferLayings.TransferLayingModule{},
// MODULE REGISTRY // MODULE REGISTRY
} }
for _, m := range allModules { for _, m := range allModules {
m.RegisterRoutes(group, db, validate) m.RegisterRoutes(group, db, validate)
@@ -0,0 +1,144 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type TransferLayingController struct {
TransferLayingService service.TransferLayingService
}
func NewTransferLayingController(transferLayingService service.TransferLayingService) *TransferLayingController {
return &TransferLayingController{
TransferLayingService: transferLayingService,
}
}
func (u *TransferLayingController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
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.TransferLayingService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.TransferLayingListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all transferLayings successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToTransferLayingListDTOs(result),
})
}
func (u *TransferLayingController) 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.TransferLayingService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get transferLaying successfully",
Data: dto.ToTransferLayingListDTO(*result),
})
}
func (u *TransferLayingController) 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.TransferLayingService.CreateOne(c, req)
if err != nil {
return err
}
return c.Status(fiber.StatusCreated).
JSON(response.Success{
Code: fiber.StatusCreated,
Status: "success",
Message: "Create transferLaying successfully",
Data: dto.ToTransferLayingListDTO(*result),
})
}
func (u *TransferLayingController) 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.TransferLayingService.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 transferLaying successfully",
Data: dto.ToTransferLayingListDTO(*result),
})
}
func (u *TransferLayingController) 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.TransferLayingService.DeleteOne(c, uint(id)); err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Common{
Code: fiber.StatusOK,
Status: "success",
Message: "Delete transferLaying successfully",
})
}
@@ -0,0 +1,64 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type TransferLayingBaseDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type TransferLayingListDTO struct {
TransferLayingBaseDTO
CreatedUser *userDTO.UserBaseDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type TransferLayingDetailDTO struct {
TransferLayingListDTO
}
// === Mapper Functions ===
func ToTransferLayingBaseDTO(e entity.LayingTransfer) TransferLayingBaseDTO {
return TransferLayingBaseDTO{
Id: e.Id,
}
}
func ToTransferLayingListDTO(e entity.LayingTransfer) TransferLayingListDTO {
var createdUser *userDTO.UserBaseDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserBaseDTO(*e.CreatedUser)
createdUser = &mapped
}
return TransferLayingListDTO{
TransferLayingBaseDTO: ToTransferLayingBaseDTO(e),
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToTransferLayingListDTOs(e []entity.LayingTransfer) []TransferLayingListDTO {
result := make([]TransferLayingListDTO, len(e))
for i, r := range e {
result[i] = ToTransferLayingListDTO(r)
}
return result
}
func ToTransferLayingDetailDTO(e entity.LayingTransfer) TransferLayingDetailDTO {
return TransferLayingDetailDTO{
TransferLayingListDTO: ToTransferLayingListDTO(e),
}
}
@@ -0,0 +1,28 @@
package transfer_layings
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
rTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
sTransferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type TransferLayingModule struct{}
func (TransferLayingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
transferLayingRepo := rTransferLaying.NewTransferLayingRepository(db)
userRepo := rUser.NewUserRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
transferLayingService := sTransferLaying.NewTransferLayingService(transferLayingRepo, projectFlockRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
TransferLayingRoutes(router, userService, transferLayingService)
}
@@ -0,0 +1,22 @@
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 TransferLayingRepository interface {
repository.BaseRepository[entity.LayingTransfer]
}
type TransferLayingRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.LayingTransfer]
}
func NewTransferLayingRepository(db *gorm.DB) TransferLayingRepository {
return &TransferLayingRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.LayingTransfer](db),
}
}
@@ -0,0 +1,28 @@
package transfer_layings
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/controllers"
transferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.TransferLayingService) {
ctrl := controller.NewTransferLayingController(s)
route := v1.Group("/transfer_layings")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne)
route.Get("/:id", ctrl.GetOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Delete("/:id", ctrl.DeleteOne)
}
@@ -0,0 +1,173 @@
package service
import (
"errors"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/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 TransferLayingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.LayingTransfer, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error)
DeleteOne(ctx *fiber.Ctx, id uint) error
}
type transferLayingService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.TransferLayingRepository
ProjectFlockRepo ProjectFlockRepository.ProjectflockRepository
ProjectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository
}
func NewTransferLayingService(repo repository.TransferLayingRepository, projectFlockRepo ProjectFlockRepository.ProjectflockRepository, projectFlockKandangRepo ProjectFlockRepository.ProjectFlockKandangRepository, validate *validator.Validate) TransferLayingService {
return &transferLayingService{
Log: utils.Log,
Validate: validate,
Repository: repo,
ProjectFlockRepo: projectFlockRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
}
}
func (s transferLayingService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s transferLayingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.LayingTransfer, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
transferLayings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get transferLayings: %+v", err)
return nil, 0, err
}
return transferLayings, total, nil
}
func (s transferLayingService) GetOne(c *fiber.Ctx, id uint) (*entity.LayingTransfer, error) {
transferLaying, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
}
if err != nil {
s.Log.Errorf("Failed get transferLaying by id: %+v", err)
return nil, err
}
return transferLaying, nil
}
func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.LayingTransfer, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Source Project Flock", ID: &req.SourceProjectFlockId, Exists: s.ProjectFlockRepo.IdExists},
common.RelationCheck{Name: "Target Project Flock", ID: &req.TargetProjectFlockId, Exists: s.ProjectFlockRepo.IdExists},
); err != nil {
return nil, err
}
for _, detail := range req.Details {
if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Project Flock Kandang", ID: &detail.SourceProjectFlockKandangId, Exists: s.ProjectFlockKandangRepo.IdExists},
); err != nil {
return nil, err
}
}
transferDate, err := utils.ParseDateString(req.TransferDate)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transfer date format")
}
var totalQty float64
for _, item := range req.Details {
totalQty += item.Quantity
}
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
createBody := &entity.LayingTransfer{
Notes: req.Reason,
FromProjectFlockId: req.SourceProjectFlockId,
ToProjectFlockId: req.TargetProjectFlockId,
TransferDate: transferDate,
TotalQty: totalQty,
CreatedBy: 1, //todo : harus diambil dari auth
}
if err := s.Repository.WithTx(dbTransaction).CreateOne(c.Context(), createBody, nil); err != nil {
return err
}
return nil
})
if err != nil {
return nil, err
}
return nil, err
}
func (s transferLayingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.LayingTransfer, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
updateBody := make(map[string]any)
if req.Name != nil {
updateBody["name"] = *req.Name
}
if len(updateBody) == 0 {
return s.GetOne(c, id)
}
if err := s.Repository.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "TransferLaying not found")
}
s.Log.Errorf("Failed to update transferLaying: %+v", err)
return nil, err
}
return s.GetOne(c, id)
}
func (s transferLayingService) 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, "TransferLaying not found")
}
s.Log.Errorf("Failed to delete transferLaying: %+v", err)
return err
}
return nil
}
@@ -0,0 +1,24 @@
package validation
type CreateDetail struct {
SourceProjectFlockKandangId uint `json:"source_project_flock_kandang_id" validate:"required"`
Quantity float64 `json:"quantity" validate:"required,gt=0"`
}
type Create struct {
TransferDate string `json:"transfer_date" validate:"required,datetime=2006-01-02"`
SourceProjectFlockId uint `json:"source_project_flock_id" validate:"required"`
TargetProjectFlockId uint `json:"target_project_flock_id" validate:"required"`
Details []CreateDetail `json:"details" validate:"required,min=1,dive,required"`
Reason string `json:"reason" validate:"omitempty,max=1000"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"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"`
}
+32
View File
@@ -79,6 +79,24 @@ const (
WarehouseTypeKandang WarehouseType = "KANDANG" WarehouseTypeKandang WarehouseType = "KANDANG"
) )
// -------------------------------------------------------------------
// Stock log
// -------------------------------------------------------------------
type StockLogTransactionType string
const (
StockLogTransactionTypeIncrease StockLogTransactionType = "INCREASE"
StockLogTransactionTypeDecrease StockLogTransactionType = "DECREASE"
)
type StockLogType string
const (
StockLogTypeAdjustment StockLogType = "ADJUSTMENT"
StockLogTypeTransfer StockLogType = "TRANSFER"
)
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// WarehouseType // WarehouseType
// ------------------------------------------------------------------- // -------------------------------------------------------------------
@@ -140,6 +158,20 @@ var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{
ProjectFlockStepAktif: "Aktif", ProjectFlockStepAktif: "Aktif",
} }
// -------------------------------------------------------------------
// Project Flock Kandang Approval
// -------------------------------------------------------------------
const (
ApprovalWorkflowProjectFlockKandang approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PROJECT_FLOCK_KANDANGS")
ProjectFlockKandangStepPengajuan approvalutils.ApprovalStep = 1
ProjectFlockKandangStepDisetujui approvalutils.ApprovalStep = 2
)
var ProjectFlockKandangApprovalSteps = map[approvalutils.ApprovalStep]string{
ProjectFlockKandangStepPengajuan: "Pengajuan",
ProjectFlockKandangStepDisetujui: "Disetujui",
}
// ------------------------------------------------------------------- // -------------------------------------------------------------------
// Validators // Validators
// ------------------------------------------------------------------- // -------------------------------------------------------------------