diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql new file mode 100644 index 00000000..6ad77820 --- /dev/null +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS laying_transfers; \ No newline at end of file diff --git a/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql new file mode 100644 index 00000000..10ced117 --- /dev/null +++ b/internal/database/migrations/20251029074825_create_laying_transfers_table.up.sql @@ -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); \ No newline at end of file diff --git a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql new file mode 100644 index 00000000..caf4f52d --- /dev/null +++ b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS laying_kandang_transfers; \ No newline at end of file diff --git a/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql new file mode 100644 index 00000000..786f8de3 --- /dev/null +++ b/internal/database/migrations/20251029081833_create_laying_kandang_transfers_table.up.sql @@ -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); \ No newline at end of file diff --git a/internal/database/migrations/20251030134228_refactor_project_chikins.up.sql b/internal/database/migrations/20251030134228_refactor_project_chikins.up.sql new file mode 100644 index 00000000..98ac9b73 --- /dev/null +++ b/internal/database/migrations/20251030134228_refactor_project_chikins.up.sql @@ -0,0 +1,5 @@ +DROP TABLE IF EXISTS project_chickin_details; + +DROP TABLE IF EXISTS project_chickins; + +DROP TABLE IF EXISTS project_flock_populations; \ No newline at end of file diff --git a/internal/database/migrations/20251030134527_recreate_project_chikins_table.down.sql b/internal/database/migrations/20251030134527_recreate_project_chikins_table.down.sql new file mode 100644 index 00000000..e69de29b diff --git a/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql b/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql new file mode 100644 index 00000000..c00765b1 --- /dev/null +++ b/internal/database/migrations/20251030134527_recreate_project_chikins_table.up.sql @@ -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); \ No newline at end of file diff --git a/internal/database/migrations/20251030134542_recreate_project_flock_populations.down.sql b/internal/database/migrations/20251030134542_recreate_project_flock_populations.down.sql new file mode 100644 index 00000000..e69de29b diff --git a/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql b/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql new file mode 100644 index 00000000..2cb76e8f --- /dev/null +++ b/internal/database/migrations/20251030134542_recreate_project_flock_populations.up.sql @@ -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); \ No newline at end of file diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 791cfddb..ca8e8cf3 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -93,9 +93,9 @@ func Run(db *gorm.DB) error { if err := seedTransferStock(tx, adminID); err != nil { return err } - if err := seedChickin(tx, adminID); err != nil { - return err - } + // if err := seedChickin(tx, adminID); err != nil { + // return err + // } fmt.Println("✅ Master data seeding completed") return nil @@ -1134,151 +1134,151 @@ func seedTransferStock(tx *gorm.DB, createdBy uint) error { return nil } -func seedChickin(tx *gorm.DB, createdBy uint) error { - seeds := []struct { - ProjectFlockKandangId uint - ChickInDate string - Quantity float64 - Note string - }{ - {ProjectFlockKandangId: 1, ChickInDate: "2025-10-20", Quantity: 100, Note: "Seeder chickin 1"}, - {ProjectFlockKandangId: 2, ChickInDate: "2025-10-21", Quantity: 200, Note: "Seeder chickin 2"}, - } +// func seedChickin(tx *gorm.DB, createdBy uint) error { +// seeds := []struct { +// ProjectFlockKandangId uint +// ChickInDate string +// Quantity float64 +// Note string +// }{ +// {ProjectFlockKandangId: 1, ChickInDate: "2025-10-20", Quantity: 100, Note: "Seeder chickin 1"}, +// {ProjectFlockKandangId: 2, ChickInDate: "2025-10-21", Quantity: 200, Note: "Seeder chickin 2"}, +// } - for _, seed := range seeds { - chickinDate, err := time.Parse("2006-01-02", seed.ChickInDate) - if err != nil { - return err - } +// for _, seed := range seeds { +// chickinDate, err := time.Parse("2006-01-02", seed.ChickInDate) +// if err != nil { +// return err +// } - // Insert ProjectChickin jika belum ada - var chickin entity.ProjectChickin - err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", seed.ProjectFlockKandangId, chickinDate). - First(&chickin).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - chickin = entity.ProjectChickin{ - ProjectFlockKandangId: seed.ProjectFlockKandangId, - ChickInDate: chickinDate, - Quantity: seed.Quantity, - Note: seed.Note, - CreatedBy: createdBy, - } - if err := tx.Create(&chickin).Error; err != nil { - return err - } - } else if err != nil { - return err - } +// // Insert ProjectChickin jika belum ada +// var chickin entity.ProjectChickin +// err = tx.Where("project_flock_kandang_id = ? AND chick_in_date = ?", seed.ProjectFlockKandangId, chickinDate). +// First(&chickin).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// chickin = entity.ProjectChickin{ +// ProjectFlockKandangId: seed.ProjectFlockKandangId, +// ChickInDate: chickinDate, +// Quantity: seed.Quantity, +// Note: seed.Note, +// CreatedBy: createdBy, +// } +// if err := tx.Create(&chickin).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } - var population entity.ProjectFlockPopulation - err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - population = entity.ProjectFlockPopulation{ - ProjectFlockKandangId: seed.ProjectFlockKandangId, - InitialQuantity: seed.Quantity, - CurrentQuantity: seed.Quantity, - ReservedQuantity: 0, - CreatedBy: createdBy, - } - if err := tx.Create(&population).Error; err != nil { - return err - } - } else if err != nil { - return err - } else { - // Update population quantities - if err := tx.Model(&entity.ProjectFlockPopulation{}). - Where("id = ?", population.Id). - Updates(map[string]any{ - "initial_quantity": population.InitialQuantity + seed.Quantity, - "current_quantity": population.CurrentQuantity + seed.Quantity, - "reserved_quantity": 0, - }).Error; err != nil { - return err - } - } +// var population entity.ProjectFlockPopulation +// err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error +// if errors.Is(err, gorm.ErrRecordNotFound) { +// population = entity.ProjectFlockPopulation{ +// ProjectFlockKandangId: seed.ProjectFlockKandangId, +// InitialQuantity: seed.Quantity, +// CurrentQuantity: seed.Quantity, +// ReservedQuantity: 0, +// CreatedBy: createdBy, +// } +// if err := tx.Create(&population).Error; err != nil { +// return err +// } +// } else if err != nil { +// return err +// } else { +// // Update population quantities +// if err := tx.Model(&entity.ProjectFlockPopulation{}). +// Where("id = ?", population.Id). +// Updates(map[string]any{ +// "initial_quantity": population.InitialQuantity + seed.Quantity, +// "current_quantity": population.CurrentQuantity + seed.Quantity, +// "reserved_quantity": 0, +// }).Error; err != nil { +// 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 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 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 - } +// 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 - } +// // 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 - } +// // 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 - } +// 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 - } - } - } +// 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 - } - } - } +// // zero out pw quantity +// if err := tx.Model(&entity.ProductWarehouse{}).Where("id = ?", pw.Id).Update("quantity", 0).Error; err != nil { +// return err +// } +// } +// } - return nil -} +// return nil +// } func ptr[T any](v T) *T { return &v diff --git a/internal/entities/laying_kandang_transfer.go b/internal/entities/laying_kandang_transfer.go new file mode 100644 index 00000000..8f514f71 --- /dev/null +++ b/internal/entities/laying_kandang_transfer.go @@ -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"` +} diff --git a/internal/entities/laying_transfer.go b/internal/entities/laying_transfer.go new file mode 100644 index 00000000..75c5e23a --- /dev/null +++ b/internal/entities/laying_transfer.go @@ -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"` +} diff --git a/internal/entities/project_chickin.go b/internal/entities/project_chickin.go index 95a658c8..c3ca2671 100644 --- a/internal/entities/project_chickin.go +++ b/internal/entities/project_chickin.go @@ -12,13 +12,16 @@ type ProjectChickin struct { Id uint `gorm:"primaryKey"` ProjectFlockKandangId uint `gorm:"not null"` ChickInDate time.Time `gorm:"not null"` - Quantity float64 `gorm:"not null"` - Note string `gorm:"type:text"` + ProductWarehouseId uint `gorm:"not null"` + 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"` CreatedAt time.Time `gorm:"autoCreateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` + ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` } diff --git a/internal/entities/project_flock_population.go b/internal/entities/project_flock_population.go index 184ace65..e5b3216c 100644 --- a/internal/entities/project_flock_population.go +++ b/internal/entities/project_flock_population.go @@ -7,16 +7,18 @@ import ( ) type ProjectFlockPopulation struct { - Id uint `gorm:"primaryKey"` - ProjectFlockKandangId uint `gorm:"not null"` - InitialQuantity float64 `gorm:"type:numeric(15,3);not null"` - CurrentQuantity float64 `gorm:"type:numeric(15,3);not null"` - ReservedQuantity float64 `gorm:"type:numeric(15,3)"` - CreatedBy uint `gorm:"not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index"` + Id uint `gorm:"primaryKey"` + ProjectChickinId uint `gorm:"not null"` + ProductWarehouseId uint `gorm:"not null"` + TotalQty float64 `gorm:"type:numeric(15,3);not null"` + TotalUsedQty float64 `gorm:"type:numeric(15,3);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"` - ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` - CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` + ProjectChickin *ProjectChickin `gorm:"foreignKey:ProjectChickinId;references:Id"` + ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` } diff --git a/internal/entities/projectflock.go b/internal/entities/projectflock.go index c840892f..4c05298f 100644 --- a/internal/entities/projectflock.go +++ b/internal/entities/projectflock.go @@ -28,3 +28,4 @@ type ProjectFlock struct { KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"` LatestApproval *Approval `gorm:"-" json:"-"` } + diff --git a/internal/entities/projectflock_kandang.go b/internal/entities/projectflock_kandang.go index 1c29c22e..f10dbd17 100644 --- a/internal/entities/projectflock_kandang.go +++ b/internal/entities/projectflock_kandang.go @@ -3,10 +3,12 @@ package entities import "time" type ProjectFlockKandang struct { - Id uint `gorm:"primaryKey"` - 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"` - CreatedAt time.Time `gorm:"autoCreateTime"` - ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` - Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` + Id uint `gorm:"primaryKey"` + 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"` + CreatedAt time.Time `gorm:"autoCreateTime"` + + ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"` + Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"` + Chickins []ProjectChickin `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` } diff --git a/internal/modules/constants/repositories/constant.repository.go b/internal/modules/constants/repositories/constant.repository.go index 4b44d553..493f4cb9 100644 --- a/internal/modules/constants/repositories/constant.repository.go +++ b/internal/modules/constants/repositories/constant.repository.go @@ -82,6 +82,10 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} { "LOKASI", "KANDANG", }, + "stock_log": map[string][]string{ + "log_types": []string{"TRANSFER", "ADJUSTMENT"}, + "transaction_types": []string{"INCREASE", "DECREASE"}, + }, "supplier_categories": []string{ "BOP", "SAPRONAK", diff --git a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go index f1f1fa57..0017fe6c 100644 --- a/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go +++ b/internal/modules/inventory/product-warehouses/repositories/product_warehouse.repository.go @@ -17,6 +17,9 @@ type ProductWarehouseRepository interface { ExistsByID(ctx context.Context, id uint) (bool, error) GetProductWarehouseByProductAndWarehouseID(ctx context.Context, productId, 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 { @@ -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) { return repository.Exists[entity.Product](ctx, r.db, productId) } @@ -89,3 +101,20 @@ func (r *ProductWarehouseRepositoryImpl) GetByCategoryCodeAndWarehouseID(ctx con } 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 +} diff --git a/internal/modules/production/chickins/controllers/chickin.controller.go b/internal/modules/production/chickins/controllers/chickin.controller.go index fadcbc3e..c132b279 100644 --- a/internal/modules/production/chickins/controllers/chickin.controller.go +++ b/internal/modules/production/chickins/controllers/chickin.controller.go @@ -139,23 +139,33 @@ func (u *ChickinController) DeleteOne(c *fiber.Ctx) error { }) } -func (u *ChickinController) Approve(c *fiber.Ctx) error { - param := c.Params("id") - - id, err := strconv.Atoi(param) - if err != nil { - return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") +func (u *ChickinController) Approval(c *fiber.Ctx) error { + req := new(validation.Approve) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") } - if err := u.ChickinService.Approve(c, uint(id)); err != nil { + results, err := u.ChickinService.Approval(c, req) + if err != nil { 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). JSON(response.Success{ Code: fiber.StatusOK, Status: "success", - Message: "Approve chickin successfully", - Data: nil, + Message: message, + Data: data, }) } diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 193257b6..823cbfa5 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -18,8 +18,10 @@ type ChickinBaseDTO struct { Id uint `json:"id"` ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"` ChickInDate time.Time `json:"chick_in_date"` - Quantity float64 `json:"quantity"` - Note string `json:"note"` + ProductWarehouseId uint `json:"product_warehouse_id"` + UsageQty float64 `json:"usage_qty"` + PendingUsageQty float64 `json:"pending_usage_qty"` + Notes string `json:"notes"` } type ProjectFlockDTO struct { @@ -44,8 +46,10 @@ type ChickinSimpleDTO struct { Id uint `json:"id"` ProjectFlockKandangId uint `json:"project_flock_kandang_id"` ChickInDate time.Time `json:"chick_in_date"` - Quantity float64 `json:"quantity"` - Note string `json:"note"` + ProductWarehouseId uint `json:"product_warehouse_id"` + UsageQty float64 `json:"usage_qty"` + PendingUsageQty float64 `json:"pending_usage_qty"` + Notes string `json:"notes"` CreatedBy uint `json:"created_by"` } @@ -138,16 +142,18 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { var pfk *ProjectFlockKandangDTO - if e.ProjectFlockKandang.Id != 0 { - mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang) + if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 { + mapped := ToProjectFlockKandangDTO(*e.ProjectFlockKandang) pfk = &mapped } return ChickinBaseDTO{ Id: e.Id, ProjectFlockKandang: pfk, ChickInDate: e.ChickInDate, - Quantity: e.Quantity, - Note: e.Note, + ProductWarehouseId: e.ProductWarehouseId, + UsageQty: e.UsageQty, + PendingUsageQty: e.PendingUsageQty, + Notes: e.Notes, } } @@ -156,21 +162,23 @@ func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO { Id: e.Id, ProjectFlockKandangId: e.ProjectFlockKandangId, ChickInDate: e.ChickInDate, - Quantity: e.Quantity, - Note: e.Note, + ProductWarehouseId: e.ProductWarehouseId, + UsageQty: e.UsageQty, + PendingUsageQty: e.PendingUsageQty, + Notes: e.Notes, CreatedBy: e.CreatedBy, } } func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO { var createdUser *userBaseDTO.UserBaseDTO - if e.CreatedUser.Id != 0 { - mapped := userBaseDTO.ToUserBaseDTO(e.CreatedUser) + if e.CreatedUser != nil && e.CreatedUser.Id != 0 { + mapped := userBaseDTO.ToUserBaseDTO(*e.CreatedUser) createdUser = &mapped } var pfk *ProjectFlockKandangDTO - if e.ProjectFlockKandang.Id != 0 { - mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang) + if e.ProjectFlockKandang != nil && e.ProjectFlockKandang.Id != 0 { + mapped := ToProjectFlockKandangDTO(*e.ProjectFlockKandang) pfk = &mapped } return ChickinListDTO{ diff --git a/internal/modules/production/chickins/module.go b/internal/modules/production/chickins/module.go index f1e0baea..f6dd554b 100644 --- a/internal/modules/production/chickins/module.go +++ b/internal/modules/production/chickins/module.go @@ -1,10 +1,15 @@ package chickins import ( + "fmt" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" "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" rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/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" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + utils "gitlab.com/mbugroup/lti-api.git/internal/utils" ) type ChickinModule struct{} @@ -32,6 +39,12 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * 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) userService := sUser.NewUserService(userRepo, validate) diff --git a/internal/modules/production/chickins/repositories/project_chickin_detail.repository.go b/internal/modules/production/chickins/repositories/project_chickin_detail.repository.go index 42c267ec..f5be8770 100644 --- a/internal/modules/production/chickins/repositories/project_chickin_detail.repository.go +++ b/internal/modules/production/chickins/repositories/project_chickin_detail.repository.go @@ -1,6 +1,8 @@ package repository import ( + "context" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" @@ -8,6 +10,10 @@ import ( type ProjectChickinDetailRepository interface { 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 { @@ -19,3 +25,22 @@ func NewChickinDetailRepository(db *gorm.DB) ProjectChickinDetailRepository { 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 +} diff --git a/internal/modules/production/chickins/route.go b/internal/modules/production/chickins/route.go index 5fa5237a..0bb5e93d 100644 --- a/internal/modules/production/chickins/route.go +++ b/internal/modules/production/chickins/route.go @@ -25,5 +25,5 @@ func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) - route.Post("/:id/approve", ctrl.Approve) + route.Post("/approvals", ctrl.Approval) } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index f422666f..2b44c242 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -2,7 +2,11 @@ package service import ( "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" 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" @@ -24,7 +28,7 @@ type ChickinService interface { CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, 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 { @@ -77,6 +81,7 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity } offset := (params.Page - 1) * params.Limit + chickins, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) if params.ProjectFlockKandangId != 0 { @@ -109,107 +114,98 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, err } - projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), req.ProjectFlockKandangId) + 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 + return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock Kandang not found") } - warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId) + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectFlockKandang.KandangId) if err != nil { - s.Log.Errorf("Failed to get warehouse: %+v", err) - return nil, err + return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse for Kandang not found") } - // move complex DB query into repository for cleaner service - 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 - } + var productWarehouses []entity.ProductWarehouse - if totalQuantity < 1 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses") + if strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category)) == string(utils.ProjectFlockCategoryGrowing) { + + 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) if err != nil { - s.Log.Errorf("Failed to parse chickin date: %+v", err) return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid ChickInDate format") } - newChickin := &entity.ProjectChickin{ - ProjectFlockKandangId: projectflockkandang.Id, - ChickInDate: chickinDate, - Quantity: totalQuantity, - Note: req.Note, - CreatedBy: 1, //todo: ganti dengan user login + + actorID := uint(1) // todo nanti ambil dari auth context + newChikins := make([]*entity.ProjectChickin, 0) + for _, productWarehouse := range productWarehouses { + + 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 { - s.Log.Errorf("Failed to create chickin: %+v", err) - return nil, err + return nil, fiber.NewError(fiber.StatusBadRequest, err.Error()) } - // Update semua product warehouse: set quantity jadi 0 - 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) + return newChikins[0], nil } 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 } if req.Note != "" { - updateBody["note"] = req.Note + // entity uses `Notes` => column `notes` + updateBody["notes"] = req.Note } if len(updateBody) == 0 { 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 { - db := s.Repository.DB() - 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) + // Simplified delete: directly call repository delete. Complex restore logic removed for now. + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Chickin not found") } return err } - chickinRepoTx := s.Repository.WithTx(tx) - pfkRepoTx := s.ProjectflockKandangRepo.WithTx(tx) - productWarehouseRepoTx := s.ProductWarehouseRepo.WithTx(tx) + return nil +} - chickin, err := chickinRepoTx.GetByID(c.Context(), id, nil) - if errors.Is(err, gorm.ErrRecordNotFound) { - return rollback(fiber.NewError(fiber.StatusNotFound, "Chickin not found")) - } - if err != nil { - s.Log.Errorf("Failed get chickin by id: %+v", err) - return rollback(err) +func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectChickin, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err } - 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 rollback(fiber.NewError(fiber.StatusNotFound, "Project flock population not found")) - } - s.Log.Errorf("Failed to get project flock population: %+v", err) - return rollback(err) + actorID := uint(1) // todo nanti ambil dari auth context + + var action entity.ApprovalAction + switch strings.ToUpper(strings.TrimSpace(req.Action)) { + case string(entity.ApprovalActionRejected): + action = entity.ApprovalActionRejected + 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 - 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) + approvableIDs := uniqueUintSlice(req.ApprovableIds) + if len(approvableIDs) == 0 { + return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id") } - 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 - } + step := utils.ProjectFlockKandangStepPengajuan + if action == entity.ApprovalActionApproved { + step = utils.ProjectFlockKandangStepDisetujui + } - 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) { + err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction) + 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 err := productWarehouseRepoTx.PatchOne(c.Context(), pw.Id, map[string]any{"quantity": updatedQuantity}, nil); err != nil { - return false, err + if action == entity.ApprovalActionApproved { + + 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 false, err - } - return true, nil - } + return 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) + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr } - - 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")) + return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") } - s.Log.Errorf("Failed to delete chickin: %+v", err) - return rollback(err) + s.Log.Errorf("Failed to record approval for chickins %+v: %+v", approvableIDs, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") } - if err := tx.Commit().Error; err != nil { - s.Log.Errorf("Failed to commit transaction: %+v", err) - return rollback(err) + updated := make([]entity.ProjectChickin, 0) + for _, kandangID := range approvableIDs { + 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 { - - // todo: ini contoh akhir jika sudah approved - - chickin, err := s.Repository.GetByID( - c.Context(), - id, - nil, - ) - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Chickin not found") +func uniqueUintSlice(values []uint) []uint { + seen := make(map[uint]struct{}, len(values)) + result := make([]uint, 0, len(values)) + for _, v := range values { + if _, ok := seen[v]; ok { + continue + } + seen[v] = struct{}{} + result = append(result, v) } - if err != nil { - 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 + return result } diff --git a/internal/modules/production/chickins/validations/chickin.validation.go b/internal/modules/production/chickins/validations/chickin.validation.go index 9747ee07..c2676130 100644 --- a/internal/modules/production/chickins/validations/chickin.validation.go +++ b/internal/modules/production/chickins/validations/chickin.validation.go @@ -3,7 +3,7 @@ package validation type Create struct { ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` 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 { @@ -16,3 +16,9 @@ type Query struct { Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` 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"` +} diff --git a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go index cb4b0d5f..3328d286 100644 --- a/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -9,8 +9,16 @@ import ( ) type ProjectFlockPopulationRepository interface { - repository.BaseRepository[entity.ProjectFlockPopulation] + // domain-specific 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 { @@ -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) { var record entity.ProjectFlockPopulation err := r.DB().WithContext(ctx). diff --git a/internal/modules/production/project_flocks/repositories/projectflock.repository.go b/internal/modules/production/project_flocks/repositories/projectflock.repository.go index 476b061b..bc5a0c6b 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock.repository.go @@ -16,6 +16,7 @@ type ProjectflockRepository interface { GetActiveByFlock(ctx context.Context, flockID uint) (*entity.ProjectFlock, error) GetMaxPeriodByFlock(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 { @@ -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) { var records []entity.ProjectFlock if err := r.DB().WithContext(ctx). diff --git a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go index 5c78f830..7205b72a 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -3,6 +3,7 @@ package repository import ( "context" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gorm.io/gorm" ) @@ -14,6 +15,7 @@ type ProjectFlockKandangRepository interface { DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) WithTx(tx *gorm.DB) ProjectFlockKandangRepository + IdExists(ctx context.Context, id uint) (bool, error) DB() *gorm.DB } @@ -67,6 +69,9 @@ func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKand func (r *projectFlockKandangRepositoryImpl) DB() *gorm.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) { record := new(entity.ProjectFlockKandang) diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 46ba36cc..b3bdaeaf 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -670,18 +670,19 @@ func (s *recordingService) getPreviousRecording(tx *gorm.DB, projectFlockKandang } func (s *recordingService) getTotalChick(tx *gorm.DB, projectFlockKandangId uint) (int64, error) { - var population entity.ProjectFlockPopulation - err := tx. - Where("project_flock_kandang_id = ?", projectFlockKandangId). - Order("created_at DESC"). - First(&population).Error - if errors.Is(err, gorm.ErrRecordNotFound) { - return 0, nil - } - if err != nil { - return 0, err - } - return int64(math.Round(population.InitialQuantity)), nil + // var population entity.ProjectFlockPopulation + // err := tx. + // Where("project_flock_kandang_id = ?", projectFlockKandangId). + // Order("created_at DESC"). + // First(&population).Error + // if errors.Is(err, gorm.ErrRecordNotFound) { + // return 0, nil + // } + // if err != nil { + // return 0, err + // } + //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) { diff --git a/internal/modules/production/route.go b/internal/modules/production/route.go index b41ef1e7..10effb58 100644 --- a/internal/modules/production/route.go +++ b/internal/modules/production/route.go @@ -10,6 +10,7 @@ import ( chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins" projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks" 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 ) @@ -20,8 +21,9 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida projectflocks.ProjectflockModule{}, recordings.RecordingModule{}, chickins.ChickinModule{}, + transferLayings.TransferLayingModule{}, // MODULE REGISTRY - } +} for _, m := range allModules { m.RegisterRoutes(group, db, validate) diff --git a/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go new file mode 100644 index 00000000..01f49867 --- /dev/null +++ b/internal/modules/production/transfer_layings/controllers/transfer_laying.controller.go @@ -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", + }) +} diff --git a/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go new file mode 100644 index 00000000..69aced33 --- /dev/null +++ b/internal/modules/production/transfer_layings/dto/transfer_laying.dto.go @@ -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), + } +} diff --git a/internal/modules/production/transfer_layings/module.go b/internal/modules/production/transfer_layings/module.go new file mode 100644 index 00000000..b309f19e --- /dev/null +++ b/internal/modules/production/transfer_layings/module.go @@ -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) +} diff --git a/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go new file mode 100644 index 00000000..e92e8f95 --- /dev/null +++ b/internal/modules/production/transfer_layings/repositories/laying_transfer.repository.go @@ -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), + } +} diff --git a/internal/modules/production/transfer_layings/route.go b/internal/modules/production/transfer_layings/route.go new file mode 100644 index 00000000..ee806506 --- /dev/null +++ b/internal/modules/production/transfer_layings/route.go @@ -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) +} diff --git a/internal/modules/production/transfer_layings/services/transfer_laying.service.go b/internal/modules/production/transfer_layings/services/transfer_laying.service.go new file mode 100644 index 00000000..bc66bd13 --- /dev/null +++ b/internal/modules/production/transfer_layings/services/transfer_laying.service.go @@ -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 +} diff --git a/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go new file mode 100644 index 00000000..3b83132f --- /dev/null +++ b/internal/modules/production/transfer_layings/validations/transfer_laying.validation.go @@ -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"` +} diff --git a/internal/utils/constant.go b/internal/utils/constant.go index bdbc53b6..8328f4d7 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -79,6 +79,24 @@ const ( 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 // ------------------------------------------------------------------- @@ -140,6 +158,20 @@ var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{ 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 // -------------------------------------------------------------------