From 79700420d49945438747148411d5e4308bd857bf Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 17 Oct 2025 12:04:19 +0700 Subject: [PATCH 01/14] fix(BE): add missing product json in transfer get all & support flag param filter in product warehouses --- .../product_warehouse.controller.go | 3 +- .../services/product_warehouse.service.go | 12 +++- .../product_warehouse.validation.go | 9 +-- .../inventory/transfers/dto/transfer.dto.go | 64 ++++++++++++++----- .../transfers/services/transfer.service.go | 2 + 5 files changed, 66 insertions(+), 24 deletions(-) diff --git a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go index a0b72a4d..f21eef96 100644 --- a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go +++ b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go @@ -28,6 +28,7 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error { Limit: c.QueryInt("limit", 10), ProductId: uint(c.QueryInt("product_id", 0)), WarehouseId: uint(c.QueryInt("warehouse_id", 0)), + Flag: c.Query("flag"), } result, totalResults, err := u.ProductWarehouseService.GetAll(c, query) @@ -71,5 +72,3 @@ func (u *ProductWarehouseController) GetOne(c *fiber.Ctx) error { Data: dto.ToProductWarehouseListDTO(*result), }) } - - diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go index 9afe5707..4871dfe1 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -34,7 +34,11 @@ func NewProductWarehouseService(repo repository.ProductWarehouseRepository, vali } func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("Product.Flags").Preload("Product").Preload("Warehouse").Preload("CreatedUser") + return db. + Preload("Product.Flags"). + Preload("Product"). + Preload("Warehouse"). + Preload("CreatedUser") } func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) { @@ -55,6 +59,12 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) db = db.Where("warehouse_id = ?", params.WarehouseId) } + if params.Flag != "" { + db = db.Joins("JOIN products ON products.id = product_warehouses.product_id") + db = db.Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products") + db = db.Where("flags.name = ?", params.Flag) + } + return db.Order("created_at DESC").Order("updated_at DESC") }) diff --git a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go index 02648300..30a5bed1 100644 --- a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go +++ b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go @@ -13,8 +13,9 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` - ProductId uint `query:"product_id" validate:"omitempty,number,min=1"` - WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"` + Page int `query:"page" validate:"omitempty,number,min=1"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + ProductId uint `query:"product_id" validate:"omitempty,number,min=1"` + WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"` + Flag string `query:"flag" validate:"omitempty"` } diff --git a/internal/modules/inventory/transfers/dto/transfer.dto.go b/internal/modules/inventory/transfers/dto/transfer.dto.go index 217e5038..1b08ecbb 100644 --- a/internal/modules/inventory/transfers/dto/transfer.dto.go +++ b/internal/modules/inventory/transfers/dto/transfer.dto.go @@ -57,17 +57,17 @@ type TransferDetailDTO struct { // Detail produk type TransferDetailItemDTO struct { - Id uint64 `json:"id"` - ProductId uint64 `json:"product_id"` - Quantity float64 `json:"quantity"` - BeforeQuantity float64 `json:"before_quantity"` - AfterQuantity float64 `json:"after_quantity"` + Id uint64 `json:"id"` + Product *ProductDTO `json:"product,omitempty"` + Quantity float64 `json:"quantity"` + BeforeQuantity float64 `json:"before_quantity"` + AfterQuantity float64 `json:"after_quantity"` } // Delivery ekspedisi type TransferDeliveryDTO struct { Id uint64 `json:"id"` - SupplierId uint64 `json:"supplier_id"` + Supplier *SupplierDTO `json:"supplier,omitempty"` VehiclePlate string `json:"vehicle_plate"` DriverName string `json:"driver_name"` DocumentNumber string `json:"document_number"` @@ -83,6 +83,16 @@ type TransferDeliveryItemDTO struct { Quantity float64 `json:"quantity"` } +type ProductDTO struct { + Id uint64 `json:"id"` + Name string `json:"name"` +} + +type SupplierDTO struct { + Id uint64 `json:"id"` + Name string `json:"name"` +} + // === Mapper Functions === func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO { @@ -114,6 +124,26 @@ func toAreaDTO(a *entity.Area) *AreaDTO { } } +func toProductDTO(p *entity.Product) *ProductDTO { + if p == nil { + return nil + } + return &ProductDTO{ + Id: uint64(p.Id), + Name: p.Name, + } +} + +func toSupplierDTO(s *entity.Supplier) *SupplierDTO { + if s == nil { + return nil + } + return &SupplierDTO{ + Id: uint64(s.Id), + Name: s.Name, + } +} + func toLocationDTO(l *entity.Location) *LocationDTO { if l == nil { return nil @@ -142,19 +172,19 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) createdUser = &mapped } - // Map details + var details []TransferDetailItemDTO for _, d := range e.Details { details = append(details, TransferDetailItemDTO{ - Id: d.Id, - ProductId: d.ProductId, - Quantity: d.Quantity, + Id: d.Id, + Product: toProductDTO(d.Product), + Quantity: d.Quantity, }) } - // Map deliveries + var deliveries []TransferDeliveryDTO for _, del := range e.Deliveries { - // Map delivery items + var items []TransferDeliveryItemDTO for _, item := range del.Items { items = append(items, TransferDeliveryItemDTO{ @@ -165,8 +195,8 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { } deliveries = append(deliveries, TransferDeliveryDTO{ Id: del.Id, - SupplierId: del.SupplierId, VehiclePlate: del.VehiclePlate, + Supplier: toSupplierDTO(del.Supplier), DriverName: del.DriverName, DocumentNumber: del.DocumentNumber, DocumentPath: del.DocumentPath, @@ -198,9 +228,9 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO { var details []TransferDetailItemDTO for _, d := range e.Details { details = append(details, TransferDetailItemDTO{ - Id: d.Id, - ProductId: d.ProductId, - Quantity: d.Quantity, + Id: d.Id, + Product: toProductDTO(d.Product), + Quantity: d.Quantity, }) } // Map deliveries @@ -208,7 +238,7 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO { for _, del := range e.Deliveries { deliveries = append(deliveries, TransferDeliveryDTO{ Id: del.Id, - SupplierId: del.SupplierId, + Supplier: toSupplierDTO(del.Supplier), VehiclePlate: del.VehiclePlate, DriverName: del.DriverName, DocumentNumber: del.DocumentNumber, diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index 7f18d257..4579d4c0 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -60,6 +60,8 @@ func (s transferService) withRelations(db *gorm.DB) *gorm.DB { Preload("ToWarehouse.Location"). Preload("ToWarehouse.Area"). Preload("Details"). + Preload("Details.Product"). + Preload("Deliveries.Supplier"). Preload("Deliveries.Items") } From a45c20d2ff14ccd3d610e5ed41911965c112e22d Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Fri, 17 Oct 2025 20:43:31 +0700 Subject: [PATCH 02/14] fix(BE): improve product and warehouse existence check in adjustment service --- .../services/adjustment.service.go | 22 +++++++++++++++++-- .../services/product_warehouse.service.go | 5 +++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index 929a5c8a..af89f442 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -83,7 +83,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product") } if !isProductExist { - return nil, fiber.NewError(fiber.StatusBadRequest, "Product not found") + return nil, fiber.NewError(fiber.StatusNotFound, "Product not found") } isWarehouseExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(req.WarehouseID)) @@ -92,7 +92,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse") } if !isWarehouseExist { - return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse not found") + return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") } if req.Quantity <= 0 { @@ -187,6 +187,24 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu offset := (query.Page - 1) * query.Limit + isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID)) + if err != nil { + s.Log.Errorf("Failed to check warehouse existence: %+v", err) + return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse") + } + if query.WarehouseID > 0 && !isWarehousesExist { + return nil, 0, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") + } + + isProductsExist, err := s.ProductRepo.IdExists(c.Context(), uint(query.ProductID)) + if err != nil { + s.Log.Errorf("Failed to check product existence: %+v", err) + return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product") + } + if query.ProductID > 0 && !isProductsExist { + return nil, 0, fiber.NewError(fiber.StatusNotFound, "Product not found") + } + stockLogs, total, err := s.StockLogsRepository.GetAll(c.Context(), offset, query.Limit, func(db *gorm.DB) *gorm.DB { db = s.withRelations(db) diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go index 4871dfe1..0d86e073 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -72,6 +72,11 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) s.Log.Errorf("Failed to get productWarehouses: %+v", err) return nil, 0, err } + + if len(productWarehouses) == 0 { + return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProductWarehouses not found") + } + return productWarehouses, total, nil } From 68a670a2bd59a8919172c8a9b19e137fe3006f2a Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Sat, 18 Oct 2025 16:30:13 +0700 Subject: [PATCH 03/14] feat(BE-116): add project chick in database schema --- ...53_create_project_chick_ins_table.down.sql | 18 +++ ...5953_create_project_chick_ins_table.up.sql | 1 + internal/entities/project_chickin.go | 24 +++ .../dto/product_warehouse.dto.go | 83 ++++++++++- .../services/product_warehouse.service.go | 6 +- .../controllers/chickin.controller.go | 140 ++++++++++++++++++ .../production/chickins/dto/chickin.dto.go | 84 +++++++++++ .../modules/production/chickins/module.go | 26 ++++ .../repositories/chickin.repository.go | 21 +++ internal/modules/production/chickins/route.go | 28 ++++ .../chickins/services/chickin.service.go | 129 ++++++++++++++++ .../validations/chickin.validation.go | 15 ++ internal/modules/production/route.go | 2 + 13 files changed, 568 insertions(+), 9 deletions(-) create mode 100644 internal/database/migrations/20251017135953_create_project_chick_ins_table.down.sql create mode 100644 internal/database/migrations/20251017135953_create_project_chick_ins_table.up.sql create mode 100644 internal/entities/project_chickin.go create mode 100644 internal/modules/production/chickins/controllers/chickin.controller.go create mode 100644 internal/modules/production/chickins/dto/chickin.dto.go create mode 100644 internal/modules/production/chickins/module.go create mode 100644 internal/modules/production/chickins/repositories/chickin.repository.go create mode 100644 internal/modules/production/chickins/route.go create mode 100644 internal/modules/production/chickins/services/chickin.service.go create mode 100644 internal/modules/production/chickins/validations/chickin.validation.go diff --git a/internal/database/migrations/20251017135953_create_project_chick_ins_table.down.sql b/internal/database/migrations/20251017135953_create_project_chick_ins_table.down.sql new file mode 100644 index 00000000..ac9a0f59 --- /dev/null +++ b/internal/database/migrations/20251017135953_create_project_chick_ins_table.down.sql @@ -0,0 +1,18 @@ +CREATE TABLE project_chick_ins ( + id BIGSERIAL PRIMARY KEY, + project_floc_id BIGINT NOT NULL REFERENCES project_flocs (id), + product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id), + chick_in_date DATE NOT NULL, + quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0), + note TEXT, + created_by BIGINT NOT NULL REFERENCES users (id), + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_project_chick_ins_project_floc_id ON project_chick_ins (project_floc_id); + +CREATE INDEX idx_project_chick_ins_product_warehouse_id ON project_chick_ins (product_warehouse_id); + +CREATE INDEX idx_project_chick_ins_created_by ON project_chick_ins (created_by); \ No newline at end of file diff --git a/internal/database/migrations/20251017135953_create_project_chick_ins_table.up.sql b/internal/database/migrations/20251017135953_create_project_chick_ins_table.up.sql new file mode 100644 index 00000000..b1435759 --- /dev/null +++ b/internal/database/migrations/20251017135953_create_project_chick_ins_table.up.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS project_chick_ins; \ No newline at end of file diff --git a/internal/entities/project_chickin.go b/internal/entities/project_chickin.go new file mode 100644 index 00000000..a4a00596 --- /dev/null +++ b/internal/entities/project_chickin.go @@ -0,0 +1,24 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type ProjectChickin struct { + Id uint `gorm:"primaryKey"` + ProjectFlocId uint `gorm:"not null"` + ProductWarehouseId uint `gorm:"not null"` + ChickInDate time.Time `gorm:"not null"` + Quantity float64 `gorm:"not null"` + Note 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:"-"` + + ProjectFloc ProjectFlock `gorm:"foreignKey:ProjectFlocId;references:Id"` + ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go index fdebb519..8c9f3846 100644 --- a/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go +++ b/internal/modules/inventory/product-warehouses/dto/product_warehouse.dto.go @@ -4,7 +4,6 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) // === DTO Structs === @@ -18,11 +17,16 @@ type ProductWarehouseBaseDTO struct { type ProductWarehouseListDTO struct { ProductWarehouseBaseDTO - Product *ProductBaseDTO `json:"product,omitempty"` - Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user,omitempty"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Product *ProductBaseDTO `json:"product,omitempty"` + Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"` + CreatedUser *UserBaseDTO `json:"created_user,omitempty"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type UserBaseDTO struct { + Id uint `json:"id"` + Username string `json:"username"` } type ProductWarehouseDetailDTO struct { @@ -38,6 +42,24 @@ type ProductBaseDTO struct { } type WarehouseBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` + Kandang *KandangBaseDTO `json:"kandang,omitempty"` + Location *LocationBaseDTO `json:"location,omitempty"` + Area *AreaBaseDTO `json:"area,omitempty"` +} + +type KandangBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type LocationBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type AreaBaseDTO struct { Id uint `json:"id"` Name string `json:"name"` } @@ -69,7 +91,6 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT if e.Product.Sku != nil { product.Sku = *e.Product.Sku } - // Map flags from Product relation if len(e.Product.Flags) > 0 { for _, f := range e.Product.Flags { product.Flags = append(product.Flags, f.Name) @@ -84,12 +105,37 @@ func ToProductWarehouseListDTO(e entity.ProductWarehouse) ProductWarehouseListDT Id: e.Warehouse.Id, Name: e.Warehouse.Name, } + // Map Kandang jika ada + if e.Warehouse.Kandang != nil && e.Warehouse.Kandang.Id != 0 { + warehouse.Kandang = &KandangBaseDTO{ + Id: e.Warehouse.Kandang.Id, + Name: e.Warehouse.Kandang.Name, + } + } + // Map Location jika ada + if e.Warehouse.Location != nil && e.Warehouse.Location.Id != 0 { + warehouse.Location = &LocationBaseDTO{ + Id: e.Warehouse.Location.Id, + Name: e.Warehouse.Location.Name, + } + } + + if &e.Warehouse.Area != nil && e.Warehouse.Area.Id != 0 { + warehouse.Area = &AreaBaseDTO{ + Id: e.Warehouse.Area.Id, + Name: e.Warehouse.Area.Name, + } + } + dto.Warehouse = &warehouse } // Map CreatedUser relation jika ada if e.CreatedUser.Id != 0 { - user := userDTO.ToUserBaseDTO(e.CreatedUser) + user := UserBaseDTO{ + Id: e.CreatedUser.Id, + Username: e.CreatedUser.Name, + } dto.CreatedUser = &user } @@ -109,3 +155,24 @@ func ToProductWarehouseDetailDTO(e entity.ProductWarehouse) ProductWarehouseDeta ProductWarehouseListDTO: ToProductWarehouseListDTO(e), } } + +func ToKandangBaseDTO(e entity.Kandang) KandangBaseDTO { + return KandangBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToLocationBaseDTO(e entity.Location) LocationBaseDTO { + return LocationBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToAreaBaseDTO(e entity.Area) AreaBaseDTO { + return AreaBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go index 0d86e073..a36e3621 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -35,9 +35,12 @@ func NewProductWarehouseService(repo repository.ProductWarehouseRepository, vali func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB { return db. - Preload("Product.Flags"). Preload("Product"). + Preload("Product.Flags"). Preload("Warehouse"). + Preload("Warehouse.Location"). + Preload("Warehouse.Kandang"). + Preload("Warehouse.Area"). Preload("CreatedUser") } @@ -85,6 +88,7 @@ func (s productWarehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductW if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found") } + if err != nil { s.Log.Errorf("Failed get productWarehouse by id: %+v", err) return nil, err diff --git a/internal/modules/production/chickins/controllers/chickin.controller.go b/internal/modules/production/chickins/controllers/chickin.controller.go new file mode 100644 index 00000000..aae59ff2 --- /dev/null +++ b/internal/modules/production/chickins/controllers/chickin.controller.go @@ -0,0 +1,140 @@ +package controller + +import ( + "math" + "strconv" + + "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/dto" + service "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" + + "github.com/gofiber/fiber/v2" +) + +type ChickinController struct { + ChickinService service.ChickinService +} + +func NewChickinController(chickinService service.ChickinService) *ChickinController { + return &ChickinController{ + ChickinService: chickinService, + } +} + +func (u *ChickinController) GetAll(c *fiber.Ctx) error { + query := &validation.Query{ + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + } + + result, totalResults, err := u.ChickinService.GetAll(c, query) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.ChickinListDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get all chickins successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: dto.ToChickinListDTOs(result), + }) +} + +func (u *ChickinController) 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.ChickinService.GetOne(c, uint(id)) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get chickin successfully", + Data: dto.ToChickinListDTO(*result), + }) +} + +func (u *ChickinController) 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.ChickinService.CreateOne(c, req) + if err != nil { + return err + } + + return c.Status(fiber.StatusCreated). + JSON(response.Success{ + Code: fiber.StatusCreated, + Status: "success", + Message: "Create chickin successfully", + Data: dto.ToChickinListDTO(*result), + }) +} + +func (u *ChickinController) 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.ChickinService.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 chickin successfully", + Data: dto.ToChickinListDTO(*result), + }) +} + +func (u *ChickinController) 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.ChickinService.DeleteOne(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Common{ + Code: fiber.StatusOK, + Status: "success", + Message: "Delete chickin successfully", + }) +} diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go new file mode 100644 index 00000000..6e317e79 --- /dev/null +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -0,0 +1,84 @@ +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 ChickinBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type ChickinSimpleDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type ChickinListDTO struct { + ChickinBaseDTO + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ChickinDetailDTO struct { + ChickinListDTO +} + +// === Mapper Functions === + +func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { + return ChickinBaseDTO{ + Id: e.Id, + + } +} + +func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO { + return ChickinSimpleDTO{ + Id: e.Id, + + } +} + +func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO { + var createdUser *userDTO.UserBaseDTO + if e.CreatedUser.Id != 0 { + mapped := userDTO.ToUserBaseDTO(e.CreatedUser) + createdUser = &mapped + } + + return ChickinListDTO{ + ChickinBaseDTO: ToChickinBaseDTO(e), + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } +} + +func ToChickinListDTOs(e []entity.ProjectChickin) []ChickinListDTO { + result := make([]ChickinListDTO, len(e)) + for i, r := range e { + result[i] = ToChickinListDTO(r) + } + return result +} + +func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO { + result := make([]ChickinSimpleDTO, len(e)) + for i, r := range e { + result[i] = ToChickinSimpleDTO(r) + } + return result +} + +func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO { + return ChickinDetailDTO{ + ChickinListDTO: ToChickinListDTO(e), + } +} \ No newline at end of file diff --git a/internal/modules/production/chickins/module.go b/internal/modules/production/chickins/module.go new file mode 100644 index 00000000..330bf698 --- /dev/null +++ b/internal/modules/production/chickins/module.go @@ -0,0 +1,26 @@ +package chickins + +import ( + "github.com/go-playground/validator/v10" + "github.com/gofiber/fiber/v2" + "gorm.io/gorm" + + rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" + sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type ChickinModule struct{} + +func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + chickinRepo := rChickin.NewChickinRepository(db) + userRepo := rUser.NewUserRepository(db) + + chickinService := sChickin.NewChickinService(chickinRepo, validate) + userService := sUser.NewUserService(userRepo, validate) + + ChickinRoutes(router, userService, chickinService) +} + diff --git a/internal/modules/production/chickins/repositories/chickin.repository.go b/internal/modules/production/chickins/repositories/chickin.repository.go new file mode 100644 index 00000000..e8c3fccf --- /dev/null +++ b/internal/modules/production/chickins/repositories/chickin.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type ChickinRepository interface { + repository.BaseRepository[entity.ProjectChickin] +} + +type ChickinRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.ProjectChickin] +} + +func NewChickinRepository(db *gorm.DB) ChickinRepository { + return &ChickinRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickin](db), + } +} diff --git a/internal/modules/production/chickins/route.go b/internal/modules/production/chickins/route.go new file mode 100644 index 00000000..8948459e --- /dev/null +++ b/internal/modules/production/chickins/route.go @@ -0,0 +1,28 @@ +package chickins + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/controllers" + chickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" + + "github.com/gofiber/fiber/v2" +) + +func ChickinRoutes(v1 fiber.Router, u user.UserService, s chickin.ChickinService) { + ctrl := controller.NewChickinController(s) + + route := v1.Group("/chickins") + + // 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/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go new file mode 100644 index 00000000..00a3012e --- /dev/null +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -0,0 +1,129 @@ +package service + +import ( + "errors" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/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 ChickinService interface { + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error) + GetOne(ctx *fiber.Ctx, id uint) (*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) + DeleteOne(ctx *fiber.Ctx, id uint) error +} + +type chickinService struct { + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ChickinRepository +} + +func NewChickinService(repo repository.ChickinRepository, validate *validator.Validate) ChickinService { + return &chickinService{ + Log: utils.Log, + Validate: validate, + Repository: repo, + } +} + +func (s chickinService) withRelations(db *gorm.DB) *gorm.DB { + return db.Preload("CreatedUser") +} + +func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error) { + if err := s.Validate.Struct(params); err != nil { + return nil, 0, err + } + + 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.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 chickins: %+v", err) + return nil, 0, err + } + return chickins, total, nil +} + +func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, error) { + chickin, err := s.Repository.GetByID(c.Context(), id, s.withRelations) + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + if err != nil { + s.Log.Errorf("Failed get chickin by id: %+v", err) + return nil, err + } + return chickin, nil +} + +func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.ProjectChickin, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + createBody := &entity.ProjectChickin{ + ProjectFlocId: 1, + } + + if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { + s.Log.Errorf("Failed to create chickin: %+v", err) + return nil, err + } + + return s.GetOne(c, createBody.Id) +} + +func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, 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, "Chickin not found") + } + s.Log.Errorf("Failed to update chickin: %+v", err) + return nil, err + } + + return s.GetOne(c, id) +} + +func (s chickinService) 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, "Chickin not found") + } + s.Log.Errorf("Failed to delete chickin: %+v", err) + return err + } + return nil +} diff --git a/internal/modules/production/chickins/validations/chickin.validation.go b/internal/modules/production/chickins/validations/chickin.validation.go new file mode 100644 index 00000000..95505746 --- /dev/null +++ b/internal/modules/production/chickins/validations/chickin.validation.go @@ -0,0 +1,15 @@ +package validation + +type Create struct { + Name string `json:"name" validate:"required_strict,min=3"` +} + +type Update struct { + Name *string `json:"name,omitempty" validate:"omitempty"` +} + +type Query struct { + Page int `query:"page" validate:"omitempty,number,min=1"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + Search string `query:"search" validate:"omitempty,max=50"` +} diff --git a/internal/modules/production/route.go b/internal/modules/production/route.go index 73bbe8da..597fbc62 100644 --- a/internal/modules/production/route.go +++ b/internal/modules/production/route.go @@ -9,6 +9,7 @@ import ( projectflocks "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks" recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings" + chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins" // MODULE IMPORTS ) @@ -18,6 +19,7 @@ func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Valida allModules := []modules.Module{ projectflocks.ProjectflockModule{}, recordings.RecordingModule{}, + chickins.ChickinModule{}, // MODULE REGISTRY } From 83c3e611136617ea63751d05b282182abd576f66 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 20 Oct 2025 06:01:16 +0700 Subject: [PATCH 04/14] feat(BE-115,116,117): implement chickin CRUD, approve logic, and stock availabilit --- ...53_create_project_chick_ins_table.down.sql | 18 -- ...5953_create_project_chick_ins_table.up.sql | 1 - ...49_create_project_chick_ins_table.down.sql | 1 + ...0649_create_project_chick_ins_table.up.sql | 21 ++ ...create_stock_availabilities_table.down.sql | 1 + ...6_create_stock_availabilities_table.up.sql | 15 ++ ...019141014_create_audit_logs_table.down.sql | 1 + ...51019141014_create_audit_logs_table.up.sql | 13 ++ internal/entities/audit_log.go | 18 ++ internal/entities/project_chickin.go | 28 +-- internal/entities/stock_availabilites.go | 26 +++ internal/entities/stock_log.go | 1 + .../modules/inventory/adjustments/module.go | 2 +- .../services/adjustment.service.go | 2 +- .../product_warehouse.controller.go | 3 +- .../services/product_warehouse.service.go | 21 +- .../product_warehouse.validation.go | 9 +- .../inventory/transfers/dto/transfer.dto.go | 64 ++---- .../modules/inventory/transfers/module.go | 2 +- .../transfers/services/transfer.service.go | 4 +- .../repositories/kandang.repository.go | 12 ++ .../repositories/warehouse.repository.go | 13 ++ .../controllers/chickin.controller.go | 23 ++- .../production/chickins/dto/chickin.dto.go | 141 +++++++++---- .../modules/production/chickins/module.go | 15 +- .../repositories/chickin.repository.go | 21 -- .../project_chickin.repository.go | 36 ++++ internal/modules/production/chickins/route.go | 1 + .../chickins/services/chickin.service.go | 195 ++++++++++++++++-- .../validations/chickin.validation.go | 5 +- internal/modules/production/route.go | 2 +- .../repositories/audit-logs.repository.go | 21 ++ .../stock-availabilites.repository.go | 21 ++ .../repositories/stock-logs.repository.go | 0 34 files changed, 558 insertions(+), 199 deletions(-) delete mode 100644 internal/database/migrations/20251017135953_create_project_chick_ins_table.down.sql delete mode 100644 internal/database/migrations/20251017135953_create_project_chick_ins_table.up.sql create mode 100644 internal/database/migrations/20251018120649_create_project_chick_ins_table.down.sql create mode 100644 internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql create mode 100644 internal/database/migrations/20251019040246_create_stock_availabilities_table.down.sql create mode 100644 internal/database/migrations/20251019040246_create_stock_availabilities_table.up.sql create mode 100644 internal/database/migrations/20251019141014_create_audit_logs_table.down.sql create mode 100644 internal/database/migrations/20251019141014_create_audit_logs_table.up.sql create mode 100644 internal/entities/audit_log.go create mode 100644 internal/entities/stock_availabilites.go delete mode 100644 internal/modules/production/chickins/repositories/chickin.repository.go create mode 100644 internal/modules/production/chickins/repositories/project_chickin.repository.go create mode 100644 internal/modules/shared/repositories/audit-logs.repository.go create mode 100644 internal/modules/shared/repositories/stock-availabilites.repository.go rename internal/modules/shared/{stock-logs => }/repositories/stock-logs.repository.go (100%) diff --git a/internal/database/migrations/20251017135953_create_project_chick_ins_table.down.sql b/internal/database/migrations/20251017135953_create_project_chick_ins_table.down.sql deleted file mode 100644 index ac9a0f59..00000000 --- a/internal/database/migrations/20251017135953_create_project_chick_ins_table.down.sql +++ /dev/null @@ -1,18 +0,0 @@ -CREATE TABLE project_chick_ins ( - id BIGSERIAL PRIMARY KEY, - project_floc_id BIGINT NOT NULL REFERENCES project_flocs (id), - product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id), - chick_in_date DATE NOT NULL, - quantity NUMERIC(15, 3) NOT NULL CHECK (quantity > 0), - note TEXT, - created_by BIGINT NOT NULL REFERENCES users (id), - created_at TIMESTAMPTZ DEFAULT now(), - updated_at TIMESTAMPTZ DEFAULT now(), - deleted_at TIMESTAMPTZ -); - -CREATE INDEX idx_project_chick_ins_project_floc_id ON project_chick_ins (project_floc_id); - -CREATE INDEX idx_project_chick_ins_product_warehouse_id ON project_chick_ins (product_warehouse_id); - -CREATE INDEX idx_project_chick_ins_created_by ON project_chick_ins (created_by); \ No newline at end of file diff --git a/internal/database/migrations/20251017135953_create_project_chick_ins_table.up.sql b/internal/database/migrations/20251017135953_create_project_chick_ins_table.up.sql deleted file mode 100644 index b1435759..00000000 --- a/internal/database/migrations/20251017135953_create_project_chick_ins_table.up.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS project_chick_ins; \ No newline at end of file diff --git a/internal/database/migrations/20251018120649_create_project_chick_ins_table.down.sql b/internal/database/migrations/20251018120649_create_project_chick_ins_table.down.sql new file mode 100644 index 00000000..bb8f8a2d --- /dev/null +++ b/internal/database/migrations/20251018120649_create_project_chick_ins_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS project_chickins; \ No newline at end of file diff --git a/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql b/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql new file mode 100644 index 00000000..a3b7dfb3 --- /dev/null +++ b/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql @@ -0,0 +1,21 @@ +CREATE TABLE project_chickins ( + id BIGSERIAL PRIMARY KEY, + project_floc_id BIGINT NOT NULL, + chick_in_date DATE NOT NULL, + quantity NUMERIC(15, 3) NOT NULL, + note TEXT, + created_by BIGINT NOT NULL, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ +); + +CREATE INDEX idx_project_chickins_project_floc_id ON project_chickins (project_floc_id); + +CREATE INDEX idx_project_chickins_created_by ON project_chickins (created_by); + +ALTER TABLE project_chickins +ADD CONSTRAINT fk_project_floc_id FOREIGN KEY (project_floc_id) REFERENCES project_flocks (id); + +ALTER TABLE project_chickins +ADD CONSTRAINT fk_created_by FOREIGN KEY (created_by) REFERENCES users (id); \ No newline at end of file diff --git a/internal/database/migrations/20251019040246_create_stock_availabilities_table.down.sql b/internal/database/migrations/20251019040246_create_stock_availabilities_table.down.sql new file mode 100644 index 00000000..1d50d98b --- /dev/null +++ b/internal/database/migrations/20251019040246_create_stock_availabilities_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS stock_availabilities; \ No newline at end of file diff --git a/internal/database/migrations/20251019040246_create_stock_availabilities_table.up.sql b/internal/database/migrations/20251019040246_create_stock_availabilities_table.up.sql new file mode 100644 index 00000000..bce6f7e6 --- /dev/null +++ b/internal/database/migrations/20251019040246_create_stock_availabilities_table.up.sql @@ -0,0 +1,15 @@ +CREATE TABLE stock_availabilities ( + id BIGSERIAL PRIMARY KEY, + entity_type VARCHAR(50) NOT NULL, + entity_id BIGINT NOT NULL, + product_id BIGINT, + quantity NUMERIC(15, 3) NOT NULL DEFAULT 0, + reserved_quantity NUMERIC(15, 3) NOT NULL DEFAULT 0, + unit VARCHAR(20), + last_updated TIMESTAMPTZ DEFAULT now(), + created_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ +); + +ALTER TABLE stock_availabilities +ADD CONSTRAINT fk_product_id FOREIGN KEY (product_id) REFERENCES products (id); \ No newline at end of file diff --git a/internal/database/migrations/20251019141014_create_audit_logs_table.down.sql b/internal/database/migrations/20251019141014_create_audit_logs_table.down.sql new file mode 100644 index 00000000..4cf6b411 --- /dev/null +++ b/internal/database/migrations/20251019141014_create_audit_logs_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS audit_logs; \ No newline at end of file diff --git a/internal/database/migrations/20251019141014_create_audit_logs_table.up.sql b/internal/database/migrations/20251019141014_create_audit_logs_table.up.sql new file mode 100644 index 00000000..13731dcc --- /dev/null +++ b/internal/database/migrations/20251019141014_create_audit_logs_table.up.sql @@ -0,0 +1,13 @@ +CREATE TABLE audit_logs ( + id BIGSERIAL PRIMARY KEY, + table_name VARCHAR(100) NOT NULL, + record_id BIGINT NOT NULL, + action VARCHAR(30) NOT NULL, + before_data JSONB, + after_data JSONB, + changed_by BIGINT, + created_at TIMESTAMPTZ DEFAULT now() +); + +ALTER TABLE audit_logs +ADD CONSTRAINT fk_changed_by FOREIGN KEY (changed_by) REFERENCES users (id); \ No newline at end of file diff --git a/internal/entities/audit_log.go b/internal/entities/audit_log.go new file mode 100644 index 00000000..3b770125 --- /dev/null +++ b/internal/entities/audit_log.go @@ -0,0 +1,18 @@ +package entities + +import ( + "time" +) + +type AuditLog struct { + Id uint `gorm:"primaryKey"` + TableName string `gorm:"size:100;not null"` + RecordId uint `gorm:"not null"` + Action string `gorm:"size:30;not null"` + BeforeData string `gorm:"type:jsonb"` + AfterData string `gorm:"type:jsonb"` + ChangedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + + User *User `gorm:"foreignKey:ChangedBy;references:Id"` +} diff --git a/internal/entities/project_chickin.go b/internal/entities/project_chickin.go index a4a00596..631c8ff3 100644 --- a/internal/entities/project_chickin.go +++ b/internal/entities/project_chickin.go @@ -6,19 +6,19 @@ import ( "gorm.io/gorm" ) -type ProjectChickin struct { - Id uint `gorm:"primaryKey"` - ProjectFlocId uint `gorm:"not null"` - ProductWarehouseId uint `gorm:"not null"` - ChickInDate time.Time `gorm:"not null"` - Quantity float64 `gorm:"not null"` - Note 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:"-"` +const () - ProjectFloc ProjectFlock `gorm:"foreignKey:ProjectFlocId;references:Id"` - ProductWarehouse ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` +type ProjectChickin struct { + Id uint `gorm:"primaryKey"` + ProjectFlocId uint `gorm:"not null"` + ChickInDate time.Time `gorm:"not null"` + Quantity float64 `gorm:"not null"` + Note 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:"-"` + + ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlocId;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` } diff --git a/internal/entities/stock_availabilites.go b/internal/entities/stock_availabilites.go new file mode 100644 index 00000000..ec24d36b --- /dev/null +++ b/internal/entities/stock_availabilites.go @@ -0,0 +1,26 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +const ( + EntityTypeProjectFlockKandang = "PROJECT_FLOCK_KANDANG" +) + +type StockAvailability struct { + Id uint `gorm:"primaryKey"` + EntityType string `gorm:"size:50;not null"` + EntityId uint `gorm:"not null"` + ProductId uint `gorm:"not null"` + Quantity float64 `gorm:"not null;default:0"` + ReservedQuantity float64 `gorm:"not null;default:0"` + Unit string `gorm:"size:20"` + LastUpdated time.Time `gorm:"autoUpdateTime"` + CreatedAt time.Time `gorm:"autoCreateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Product *Product `gorm:"foreignKey:ProductId;references:Id"` +} diff --git a/internal/entities/stock_log.go b/internal/entities/stock_log.go index 21e86bd4..6546e790 100644 --- a/internal/entities/stock_log.go +++ b/internal/entities/stock_log.go @@ -8,6 +8,7 @@ import ( const ( LogTypeAdjustment = "ADJUSTMENT" + LogTypeTransfer = "TRANSFER" ) const ( diff --git a/internal/modules/inventory/adjustments/module.go b/internal/modules/inventory/adjustments/module.go index cfe01118..b3e12676 100644 --- a/internal/modules/inventory/adjustments/module.go +++ b/internal/modules/inventory/adjustments/module.go @@ -9,7 +9,7 @@ import ( rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" - rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories" + rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index af89f442..69654b85 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -9,7 +9,7 @@ import ( ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories" warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" - stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories" + stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" diff --git a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go index f21eef96..a0b72a4d 100644 --- a/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go +++ b/internal/modules/inventory/product-warehouses/controllers/product_warehouse.controller.go @@ -28,7 +28,6 @@ func (u *ProductWarehouseController) GetAll(c *fiber.Ctx) error { Limit: c.QueryInt("limit", 10), ProductId: uint(c.QueryInt("product_id", 0)), WarehouseId: uint(c.QueryInt("warehouse_id", 0)), - Flag: c.Query("flag"), } result, totalResults, err := u.ProductWarehouseService.GetAll(c, query) @@ -72,3 +71,5 @@ func (u *ProductWarehouseController) GetOne(c *fiber.Ctx) error { Data: dto.ToProductWarehouseListDTO(*result), }) } + + diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go index a36e3621..9afe5707 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -34,14 +34,7 @@ func NewProductWarehouseService(repo repository.ProductWarehouseRepository, vali } func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB { - return db. - Preload("Product"). - Preload("Product.Flags"). - Preload("Warehouse"). - Preload("Warehouse.Location"). - Preload("Warehouse.Kandang"). - Preload("Warehouse.Area"). - Preload("CreatedUser") + return db.Preload("Product.Flags").Preload("Product").Preload("Warehouse").Preload("CreatedUser") } func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) { @@ -62,12 +55,6 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) db = db.Where("warehouse_id = ?", params.WarehouseId) } - if params.Flag != "" { - db = db.Joins("JOIN products ON products.id = product_warehouses.product_id") - db = db.Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ?", "products") - db = db.Where("flags.name = ?", params.Flag) - } - return db.Order("created_at DESC").Order("updated_at DESC") }) @@ -75,11 +62,6 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) s.Log.Errorf("Failed to get productWarehouses: %+v", err) return nil, 0, err } - - if len(productWarehouses) == 0 { - return nil, 0, fiber.NewError(fiber.StatusNotFound, "ProductWarehouses not found") - } - return productWarehouses, total, nil } @@ -88,7 +70,6 @@ func (s productWarehouseService) GetOne(c *fiber.Ctx, id uint) (*entity.ProductW if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "ProductWarehouse not found") } - if err != nil { s.Log.Errorf("Failed get productWarehouse by id: %+v", err) return nil, err diff --git a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go index 30a5bed1..02648300 100644 --- a/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go +++ b/internal/modules/inventory/product-warehouses/validations/product_warehouse.validation.go @@ -13,9 +13,8 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` - ProductId uint `query:"product_id" validate:"omitempty,number,min=1"` - WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"` - Flag string `query:"flag" validate:"omitempty"` + Page int `query:"page" validate:"omitempty,number,min=1"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + ProductId uint `query:"product_id" validate:"omitempty,number,min=1"` + WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"` } diff --git a/internal/modules/inventory/transfers/dto/transfer.dto.go b/internal/modules/inventory/transfers/dto/transfer.dto.go index 1b08ecbb..217e5038 100644 --- a/internal/modules/inventory/transfers/dto/transfer.dto.go +++ b/internal/modules/inventory/transfers/dto/transfer.dto.go @@ -57,17 +57,17 @@ type TransferDetailDTO struct { // Detail produk type TransferDetailItemDTO struct { - Id uint64 `json:"id"` - Product *ProductDTO `json:"product,omitempty"` - Quantity float64 `json:"quantity"` - BeforeQuantity float64 `json:"before_quantity"` - AfterQuantity float64 `json:"after_quantity"` + Id uint64 `json:"id"` + ProductId uint64 `json:"product_id"` + Quantity float64 `json:"quantity"` + BeforeQuantity float64 `json:"before_quantity"` + AfterQuantity float64 `json:"after_quantity"` } // Delivery ekspedisi type TransferDeliveryDTO struct { Id uint64 `json:"id"` - Supplier *SupplierDTO `json:"supplier,omitempty"` + SupplierId uint64 `json:"supplier_id"` VehiclePlate string `json:"vehicle_plate"` DriverName string `json:"driver_name"` DocumentNumber string `json:"document_number"` @@ -83,16 +83,6 @@ type TransferDeliveryItemDTO struct { Quantity float64 `json:"quantity"` } -type ProductDTO struct { - Id uint64 `json:"id"` - Name string `json:"name"` -} - -type SupplierDTO struct { - Id uint64 `json:"id"` - Name string `json:"name"` -} - // === Mapper Functions === func ToTransferBaseDTO(e entity.StockTransfer) TransferBaseDTO { @@ -124,26 +114,6 @@ func toAreaDTO(a *entity.Area) *AreaDTO { } } -func toProductDTO(p *entity.Product) *ProductDTO { - if p == nil { - return nil - } - return &ProductDTO{ - Id: uint64(p.Id), - Name: p.Name, - } -} - -func toSupplierDTO(s *entity.Supplier) *SupplierDTO { - if s == nil { - return nil - } - return &SupplierDTO{ - Id: uint64(s.Id), - Name: s.Name, - } -} - func toLocationDTO(l *entity.Location) *LocationDTO { if l == nil { return nil @@ -172,19 +142,19 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { mapped := userDTO.ToUserBaseDTO(*e.CreatedUser) createdUser = &mapped } - + // Map details var details []TransferDetailItemDTO for _, d := range e.Details { details = append(details, TransferDetailItemDTO{ - Id: d.Id, - Product: toProductDTO(d.Product), - Quantity: d.Quantity, + Id: d.Id, + ProductId: d.ProductId, + Quantity: d.Quantity, }) } - + // Map deliveries var deliveries []TransferDeliveryDTO for _, del := range e.Deliveries { - + // Map delivery items var items []TransferDeliveryItemDTO for _, item := range del.Items { items = append(items, TransferDeliveryItemDTO{ @@ -195,8 +165,8 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { } deliveries = append(deliveries, TransferDeliveryDTO{ Id: del.Id, + SupplierId: del.SupplierId, VehiclePlate: del.VehiclePlate, - Supplier: toSupplierDTO(del.Supplier), DriverName: del.DriverName, DocumentNumber: del.DocumentNumber, DocumentPath: del.DocumentPath, @@ -228,9 +198,9 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO { var details []TransferDetailItemDTO for _, d := range e.Details { details = append(details, TransferDetailItemDTO{ - Id: d.Id, - Product: toProductDTO(d.Product), - Quantity: d.Quantity, + Id: d.Id, + ProductId: d.ProductId, + Quantity: d.Quantity, }) } // Map deliveries @@ -238,7 +208,7 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO { for _, del := range e.Deliveries { deliveries = append(deliveries, TransferDeliveryDTO{ Id: del.Id, - Supplier: toSupplierDTO(del.Supplier), + SupplierId: del.SupplierId, VehiclePlate: del.VehiclePlate, DriverName: del.DriverName, DocumentNumber: del.DocumentNumber, diff --git a/internal/modules/inventory/transfers/module.go b/internal/modules/inventory/transfers/module.go index 21f0ec89..734f0f03 100644 --- a/internal/modules/inventory/transfers/module.go +++ b/internal/modules/inventory/transfers/module.go @@ -9,7 +9,7 @@ import ( rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories" sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services" rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" - rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories" + rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" ) diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index 4579d4c0..bdc8abf6 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -10,7 +10,7 @@ import ( rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations" rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" - rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/stock-logs/repositories" + rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/go-playground/validator/v10" @@ -60,8 +60,6 @@ func (s transferService) withRelations(db *gorm.DB) *gorm.DB { Preload("ToWarehouse.Location"). Preload("ToWarehouse.Area"). Preload("Details"). - Preload("Details.Product"). - Preload("Deliveries.Supplier"). Preload("Deliveries.Items") } diff --git a/internal/modules/master/kandangs/repositories/kandang.repository.go b/internal/modules/master/kandangs/repositories/kandang.repository.go index b253fade..bcb03854 100644 --- a/internal/modules/master/kandangs/repositories/kandang.repository.go +++ b/internal/modules/master/kandangs/repositories/kandang.repository.go @@ -15,6 +15,7 @@ type KandangRepository interface { PicExists(ctx context.Context, areaId uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) ProjectFlockExists(ctx context.Context, projectFlockID uint) (bool, error) + GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) } @@ -69,3 +70,14 @@ func (r *KandangRepositoryImpl) HasActiveKandangForProjectFlock(ctx context.Cont } return count > 0, nil } + +func (r *KandangRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) { + kandang := new(entity.Kandang) + err := r.db.WithContext(ctx). + Where("project_flock_id = ?", projectFlockID). + First(kandang).Error + if err != nil { + return nil, err + } + return kandang, nil +} diff --git a/internal/modules/master/warehouses/repositories/warehouse.repository.go b/internal/modules/master/warehouses/repositories/warehouse.repository.go index 5c791e01..956c30ef 100644 --- a/internal/modules/master/warehouses/repositories/warehouse.repository.go +++ b/internal/modules/master/warehouses/repositories/warehouse.repository.go @@ -15,6 +15,7 @@ type WarehouseRepository interface { KandangExists(ctx context.Context, kandangId uint) (bool, error) NameExists(ctx context.Context, name string, excludeID *uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error) + GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) } type WarehouseRepositoryImpl struct { @@ -47,3 +48,15 @@ func (r *WarehouseRepositoryImpl) NameExists(ctx context.Context, name string, e func (r *WarehouseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { return repository.Exists[entity.Warehouse](ctx, r.db, id) } + +func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) { + var warehouse entity.Warehouse + err := r.db.WithContext(ctx). + Where("kandang_id = ?", kandangId). + Where("deleted_at IS NULL"). + First(&warehouse).Error + if err != nil { + return nil, err + } + return &warehouse, nil +} diff --git a/internal/modules/production/chickins/controllers/chickin.controller.go b/internal/modules/production/chickins/controllers/chickin.controller.go index aae59ff2..6514f8c8 100644 --- a/internal/modules/production/chickins/controllers/chickin.controller.go +++ b/internal/modules/production/chickins/controllers/chickin.controller.go @@ -88,7 +88,7 @@ func (u *ChickinController) CreateOne(c *fiber.Ctx) error { Code: fiber.StatusCreated, Status: "success", Message: "Create chickin successfully", - Data: dto.ToChickinListDTO(*result), + Data: result, }) } @@ -138,3 +138,24 @@ func (u *ChickinController) DeleteOne(c *fiber.Ctx) error { Message: "Delete chickin successfully", }) } + +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") + } + + if err := u.ChickinService.Approve(c, uint(id)); err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Approve chickin successfully", + Data: nil, + }) +} diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 6e317e79..7a8b6773 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -1,84 +1,135 @@ package dto import ( - "time" + "time" - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" ) // === DTO Structs === type ChickinBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` + Id uint `json:"id"` + ProjectFlocId uint `json:"project_floc_id"` + ChickInDate time.Time `json:"chick_in_date"` + Quantity float64 `json:"quantity"` + Note string `json:"note"` } type ChickinSimpleDTO struct { - Id uint `json:"id"` - Name string `json:"name"` + Id uint `json:"id"` + ProjectFlocId uint `json:"project_floc_id"` + ChickInDate time.Time `json:"chick_in_date"` + Quantity float64 `json:"quantity"` + Note string `json:"note"` + CreatedBy uint `json:"created_by"` +} + +type UserBaseDTO struct { + Id uint `json:"id"` + Name string `json:"name"` } type ChickinListDTO struct { - ChickinBaseDTO - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ChickinBaseDTO + ProjectFlock *ProjectFlockDTO `json:"project_flock"` + CreatedUser *UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` +} + +type ProjectFlockDTO struct { + Id uint `json:"id"` + Period int `json:"period"` + FlockId uint `json:"flock_id"` + FlockName string `json:"flock_name"` +} + +// === Mapper Functions === + +func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { + return ProjectFlockDTO{ + Id: e.Id, + Period: e.Period, + FlockId: e.FlockId, + FlockName: e.Flock.Name, + } } type ChickinDetailDTO struct { - ChickinListDTO + ChickinListDTO } // === Mapper Functions === func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { - return ChickinBaseDTO{ - Id: e.Id, - - } + return ChickinBaseDTO{ + Id: e.Id, + ProjectFlocId: e.ProjectFlocId, + ChickInDate: e.ChickInDate, + Quantity: e.Quantity, + Note: e.Note, + } } func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO { - return ChickinSimpleDTO{ - Id: e.Id, - - } + return ChickinSimpleDTO{ + Id: e.Id, + ProjectFlocId: e.ProjectFlocId, + ChickInDate: e.ChickInDate, + Quantity: e.Quantity, + Note: e.Note, + CreatedBy: e.CreatedBy, + } } func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO { - var createdUser *userDTO.UserBaseDTO - if e.CreatedUser.Id != 0 { - mapped := userDTO.ToUserBaseDTO(e.CreatedUser) - createdUser = &mapped - } + var createdUser *UserBaseDTO + if e.CreatedUser.Id != 0 { + mapped := ToUserBaseDTO(e.CreatedUser) + createdUser = &mapped + } - return ChickinListDTO{ - ChickinBaseDTO: ToChickinBaseDTO(e), - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, - } + var projectFlock *ProjectFlockDTO + if e.ProjectFlock.Id != 0 { + mapped := ToProjectFlockDTO(e.ProjectFlock) + projectFlock = &mapped + } + + return ChickinListDTO{ + ChickinBaseDTO: ToChickinBaseDTO(e), + ProjectFlock: projectFlock, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, + CreatedUser: createdUser, + } } func ToChickinListDTOs(e []entity.ProjectChickin) []ChickinListDTO { - result := make([]ChickinListDTO, len(e)) - for i, r := range e { - result[i] = ToChickinListDTO(r) - } - return result + result := make([]ChickinListDTO, len(e)) + for i, r := range e { + result[i] = ToChickinListDTO(r) + } + return result } func ToChickinSimpleDTOs(e []entity.ProjectChickin) []ChickinSimpleDTO { - result := make([]ChickinSimpleDTO, len(e)) - for i, r := range e { - result[i] = ToChickinSimpleDTO(r) - } - return result + result := make([]ChickinSimpleDTO, len(e)) + for i, r := range e { + result[i] = ToChickinSimpleDTO(r) + } + return result } func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO { - return ChickinDetailDTO{ - ChickinListDTO: ToChickinListDTO(e), - } -} \ No newline at end of file + return ChickinDetailDTO{ + ChickinListDTO: ToChickinListDTO(e), + } +} + +func ToUserBaseDTO(e entity.User) UserBaseDTO { + return UserBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} diff --git a/internal/modules/production/chickins/module.go b/internal/modules/production/chickins/module.go index 330bf698..30146724 100644 --- a/internal/modules/production/chickins/module.go +++ b/internal/modules/production/chickins/module.go @@ -5,8 +5,14 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" + 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" rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services" + rAuditLog "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" + + rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -16,11 +22,16 @@ type ChickinModule struct{} func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { chickinRepo := rChickin.NewChickinRepository(db) + kandangRepo := rKandang.NewKandangRepository(db) + auditlogrepo := rAuditLog.NewAuditLogRepository(db) + warehouseRepo := rWarehouse.NewWarehouseRepository(db) + projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) + productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + userRepo := rUser.NewUserRepository(db) - chickinService := sChickin.NewChickinService(chickinRepo, validate) + chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, auditlogrepo, validate) userService := sUser.NewUserService(userRepo, validate) ChickinRoutes(router, userService, chickinService) } - diff --git a/internal/modules/production/chickins/repositories/chickin.repository.go b/internal/modules/production/chickins/repositories/chickin.repository.go deleted file mode 100644 index e8c3fccf..00000000 --- a/internal/modules/production/chickins/repositories/chickin.repository.go +++ /dev/null @@ -1,21 +0,0 @@ -package repository - -import ( - "gitlab.com/mbugroup/lti-api.git/internal/common/repository" - entity "gitlab.com/mbugroup/lti-api.git/internal/entities" - "gorm.io/gorm" -) - -type ChickinRepository interface { - repository.BaseRepository[entity.ProjectChickin] -} - -type ChickinRepositoryImpl struct { - *repository.BaseRepositoryImpl[entity.ProjectChickin] -} - -func NewChickinRepository(db *gorm.DB) ChickinRepository { - return &ChickinRepositoryImpl{ - BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickin](db), - } -} diff --git a/internal/modules/production/chickins/repositories/project_chickin.repository.go b/internal/modules/production/chickins/repositories/project_chickin.repository.go new file mode 100644 index 00000000..64e2e4b4 --- /dev/null +++ b/internal/modules/production/chickins/repositories/project_chickin.repository.go @@ -0,0 +1,36 @@ +package repository + +import ( + "context" + + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type ProjectChickinRepository interface { + repository.BaseRepository[entity.ProjectChickin] + GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) +} + +type ChickinRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.ProjectChickin] +} + +func NewChickinRepository(db *gorm.DB) ProjectChickinRepository { + return &ChickinRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectChickin](db), + } +} + +func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error) { + var chickin entity.ProjectChickin + err := r.DB().WithContext(ctx). + Where("project_floc_id = ?", projectFlockID). + Where("deleted_at IS NULL"). + First(&chickin).Error + if err != nil { + return nil, err + } + return &chickin, nil +} diff --git a/internal/modules/production/chickins/route.go b/internal/modules/production/chickins/route.go index 8948459e..5fa5237a 100644 --- a/internal/modules/production/chickins/route.go +++ b/internal/modules/production/chickins/route.go @@ -25,4 +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) } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 00a3012e..75dc0242 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -4,8 +4,14 @@ import ( "errors" 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" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations" + rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + AuditLogRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "github.com/go-playground/validator/v10" @@ -20,24 +26,39 @@ 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 } type chickinService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.ChickinRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ProjectChickinRepository + KandangRepo KandangRepo.KandangRepository + WarehouseRepo rWarehouse.WarehouseRepository + ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository + ProjectFlockRepo rProjectFlock.ProjectflockRepository + AuditLogRepo AuditLogRepo.AuditLogRepository } -func NewChickinService(repo repository.ChickinRepository, validate *validator.Validate) ChickinService { +func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, auditLogRepo AuditLogRepo.AuditLogRepository, validate *validator.Validate) ChickinService { return &chickinService{ - Log: utils.Log, - Validate: validate, - Repository: repo, + Log: utils.Log, + Validate: validate, + Repository: repo, + KandangRepo: kandangRepo, + WarehouseRepo: warehouseRepo, + ProductWarehouseRepo: productWarehouseRepo, + ProjectFlockRepo: projectFlockRepo, + AuditLogRepo: auditLogRepo, } } func (s chickinService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("CreatedUser") + return db. + Preload("CreatedUser"). + Preload("ProjectFlock"). + Preload("ProjectFlock.ProductCategory") + } func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectChickin, int64, error) { @@ -79,16 +100,125 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, err } - createBody := &entity.ProjectChickin{ - ProjectFlocId: 1, + // ambil salah satu kandang dari project_floc_id dari kandang repository + kandang, err := s.KandangRepo.GetFirstByProjectFlockID(c.Context(), 1) + if err != nil { + s.Log.Errorf("Failed to get kandang: %+v", err) + return nil, err + } + // ambil warehouse dari kandangid + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandang.Id) + if err != nil { + s.Log.Errorf("Failed to get warehouse: %+v", err) + return nil, err + } + // getprojectflock id with relation + projectFlock, err := s.ProjectFlockRepo.GetByID( + c.Context(), + req.ProjectFlockId, + func(db *gorm.DB) *gorm.DB { + return db.Preload("ProductCategory") + }, + ) + if err != nil { + s.Log.Errorf("Failed to get project flock: %+v", err) + return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found") + } + // ambil quantity + var productWarehouse entity.ProductWarehouse + err = s.ProductWarehouseRepo.DB().WithContext(c.Context()). + Joins("JOIN products ON products.id = product_warehouses.product_id"). + Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). + Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", projectFlock.ProductCategory.Code, warehouse.Id). + Order("created_at DESC"). + First(&productWarehouse).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, 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 nil, err } - if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { + if productWarehouse.Quantity < 1 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient product quantity in warehouse") + } + + // masukan ke chic in + 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{ + ProjectFlocId: req.ProjectFlockId, + ChickInDate: chickinDate, + Quantity: productWarehouse.Quantity, + Note: "", + CreatedBy: 1, //todo: ganti dengan + } + + err = s.Repository.CreateOne(c.Context(), newChickin, nil) + if err != nil { s.Log.Errorf("Failed to create chickin: %+v", err) return nil, err } - return s.GetOne(c, createBody.Id) + // Kurangi quantity di product warehouse + updatedQuantity := productWarehouse.Quantity - newChickin.Quantity + if updatedQuantity < 0 { + updatedQuantity = 0 + } + err = s.ProductWarehouseRepo.PatchOne(c.Context(), productWarehouse.Id, map[string]any{ + "quantity": updatedQuantity, + }, nil) + if err != nil { + s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) + return nil, err + } + + // masukan check apakah stock availability ada, jika ada update, jika tidak buat baru + stockAvailability := &entity.StockAvailability{ + EntityType: entity.EntityTypeProjectFlockKandang, + ReservedQuantity: productWarehouse.Quantity, + EntityId: req.ProjectFlockId, //todo: nanti pakek projct flock kandang id + ProductId: productWarehouse.ProductId, + } + + var existingStockAvailability entity.StockAvailability + err = s.ProductWarehouseRepo.DB().WithContext(c.Context()). + Where("entity_type = ? AND entity_id = ? AND product_id = ?", stockAvailability.EntityType, stockAvailability.EntityId, stockAvailability.ProductId). + First(&existingStockAvailability).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + // buat baru + stockAvailability.ReservedQuantity = newChickin.Quantity + stockAvailability.Quantity = 0 + err = s.ProductWarehouseRepo.DB().WithContext(c.Context()).Create(stockAvailability).Error + if err != nil { + s.Log.Errorf("Failed to create stock availability: %+v", err) + return nil, err + } + } else { + s.Log.Errorf("Failed to get stock availability: %+v", err) + return nil, err + } + } else { + // update existing + newQuantity := existingStockAvailability.ReservedQuantity + newChickin.Quantity + err = s.ProductWarehouseRepo.DB().WithContext(c.Context()). + Model(&existingStockAvailability). + Update("reserved_quantity", newQuantity).Error + if err != nil { + s.Log.Errorf("Failed to update stock availability: %+v", err) + return nil, err + } + + } + return nil, nil } func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) { @@ -98,10 +228,9 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) updateBody := make(map[string]any) - if req.Name != nil { - updateBody["name"] = *req.Name + if req.ChickInDate != "" { + updateBody["chick_in_date"] = req.ChickInDate } - if len(updateBody) == 0 { return s.GetOne(c, id) } @@ -125,5 +254,41 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { s.Log.Errorf("Failed to delete chickin: %+v", err) return err } + return nil +} + +func (s *chickinService) Approve(c *fiber.Ctx, id uint) error { + + chickin, err := s.Repository.GetByID( + c.Context(), + id, + nil, + ) + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + if err != nil { + s.Log.Errorf("Failed get chickin by id: %+v", err) + return err + } + + //pindahkan stock dari reserved ke actual stock + // get stock avaibility untuk di update + var stockAvailability entity.StockAvailability + err = s.ProductWarehouseRepo.DB().WithContext(c.Context()). + Where("entity_type = ? AND entity_id = ? ", entity.EntityTypeProjectFlockKandang, chickin.ProjectFlocId). + First(&stockAvailability).Error + if err != nil { + s.Log.Errorf("Failed to get stock availability: %+v", err) + return err + } + + newReservedQuantity := stockAvailability.ReservedQuantity - chickin.Quantity + if newReservedQuantity < 0 { + newReservedQuantity = 0 + } + + + return nil } diff --git a/internal/modules/production/chickins/validations/chickin.validation.go b/internal/modules/production/chickins/validations/chickin.validation.go index 95505746..152b3f22 100644 --- a/internal/modules/production/chickins/validations/chickin.validation.go +++ b/internal/modules/production/chickins/validations/chickin.validation.go @@ -1,11 +1,12 @@ package validation type Create struct { - Name string `json:"name" validate:"required_strict,min=3"` + ProjectFlockId uint `json:"project_flock_id" validate:"required,number,min=1"` + ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"` } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"` } type Query struct { diff --git a/internal/modules/production/route.go b/internal/modules/production/route.go index 597fbc62..b41ef1e7 100644 --- a/internal/modules/production/route.go +++ b/internal/modules/production/route.go @@ -7,9 +7,9 @@ import ( "github.com/gofiber/fiber/v2" "gorm.io/gorm" + 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" - chickins "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins" // MODULE IMPORTS ) diff --git a/internal/modules/shared/repositories/audit-logs.repository.go b/internal/modules/shared/repositories/audit-logs.repository.go new file mode 100644 index 00000000..b247f3f2 --- /dev/null +++ b/internal/modules/shared/repositories/audit-logs.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type AuditLogRepository interface { + repository.BaseRepository[entity.AuditLog] +} + +type AuditLogRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.AuditLog] +} + +func NewAuditLogRepository(db *gorm.DB) AuditLogRepository { + return &AuditLogRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.AuditLog](db), + } +} diff --git a/internal/modules/shared/repositories/stock-availabilites.repository.go b/internal/modules/shared/repositories/stock-availabilites.repository.go new file mode 100644 index 00000000..9d3ae632 --- /dev/null +++ b/internal/modules/shared/repositories/stock-availabilites.repository.go @@ -0,0 +1,21 @@ +package repository + +import ( + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type StockAvailabilityRepository interface { + repository.BaseRepository[entity.StockAvailability] +} + +type StockAvailabilityRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.StockAvailability] +} + +func NewStockAvailabilityRepository(db *gorm.DB) StockAvailabilityRepository { + return &StockAvailabilityRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.StockAvailability](db), + } +} diff --git a/internal/modules/shared/stock-logs/repositories/stock-logs.repository.go b/internal/modules/shared/repositories/stock-logs.repository.go similarity index 100% rename from internal/modules/shared/stock-logs/repositories/stock-logs.repository.go rename to internal/modules/shared/repositories/stock-logs.repository.go From 5c3787886b38b066cb992fda08cc61b6c6c913fb Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 20 Oct 2025 08:45:31 +0700 Subject: [PATCH 05/14] FIX[BE]: adjust response on proudctwarehouses --- ...0649_create_project_chick_ins_table.up.sql | 31 ++- internal/entities/project_chickin.go | 22 +- .../services/product_warehouse.service.go | 9 +- .../production/chickins/dto/chickin.dto.go | 255 +++++++++++++----- .../modules/production/chickins/module.go | 3 +- .../chickins/services/chickin.service.go | 75 +++--- .../validations/chickin.validation.go | 4 +- .../projectflock_kandang.repository.go | 14 + 8 files changed, 291 insertions(+), 122 deletions(-) diff --git a/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql b/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql index a3b7dfb3..04475e21 100644 --- a/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql +++ b/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql @@ -1,6 +1,6 @@ -CREATE TABLE project_chickins ( +CREATE TABLE IF NOT EXISTS project_chickins ( id BIGSERIAL PRIMARY KEY, - project_floc_id BIGINT NOT NULL, + project_floc_kandang_id BIGINT NOT NULL, chick_in_date DATE NOT NULL, quantity NUMERIC(15, 3) NOT NULL, note TEXT, @@ -10,12 +10,27 @@ CREATE TABLE project_chickins ( deleted_at TIMESTAMPTZ ); -CREATE INDEX idx_project_chickins_project_floc_id ON project_chickins (project_floc_id); +-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN + ALTER TABLE project_chickins + ADD CONSTRAINT fk_project_floc_kandang_id + FOREIGN KEY (project_floc_kandang_id) + REFERENCES project_flock_kandangs(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; -CREATE INDEX idx_project_chickins_created_by ON project_chickins (created_by); + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE project_chickins + ADD CONSTRAINT fk_created_by + FOREIGN KEY (created_by) + REFERENCES users(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; -ALTER TABLE project_chickins -ADD CONSTRAINT fk_project_floc_id FOREIGN KEY (project_floc_id) REFERENCES project_flocks (id); +-- INDEXES +CREATE INDEX IF NOT EXISTS idx_project_chickins_project_floc_kandang_id ON project_chickins (project_floc_kandang_id); -ALTER TABLE project_chickins -ADD CONSTRAINT fk_created_by FOREIGN KEY (created_by) REFERENCES users (id); \ No newline at end of file +CREATE INDEX IF NOT EXISTS idx_project_chickins_created_by ON project_chickins (created_by); \ No newline at end of file diff --git a/internal/entities/project_chickin.go b/internal/entities/project_chickin.go index 631c8ff3..07536187 100644 --- a/internal/entities/project_chickin.go +++ b/internal/entities/project_chickin.go @@ -9,16 +9,16 @@ import ( const () type ProjectChickin struct { - Id uint `gorm:"primaryKey"` - ProjectFlocId uint `gorm:"not null"` - ChickInDate time.Time `gorm:"not null"` - Quantity float64 `gorm:"not null"` - Note 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:"-"` + Id uint `gorm:"primaryKey"` + ProjectFlocKandangId uint `gorm:"not null"` + ChickInDate time.Time `gorm:"not null"` + Quantity float64 `gorm:"not null"` + Note 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:"-"` - ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlocId;references:Id"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlocKandangId;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` } diff --git a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go index 9afe5707..4fad5dc5 100644 --- a/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go +++ b/internal/modules/inventory/product-warehouses/services/product_warehouse.service.go @@ -34,7 +34,14 @@ func NewProductWarehouseService(repo repository.ProductWarehouseRepository, vali } func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB { - return db.Preload("Product.Flags").Preload("Product").Preload("Warehouse").Preload("CreatedUser") + return db. + Preload("Product.Flags"). + Preload("Product"). + Preload("Warehouse"). + Preload("Warehouse.Location"). + Preload("Warehouse.Area"). + Preload("Warehouse.Kandang"). + Preload("CreatedUser") } func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) { diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 7a8b6773..c89caa0e 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -6,23 +6,52 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" ) -// === DTO Structs === +// === DTO Structs (ordered) === -type ChickinBaseDTO struct { - Id uint `json:"id"` - ProjectFlocId uint `json:"project_floc_id"` - ChickInDate time.Time `json:"chick_in_date"` - Quantity float64 `json:"quantity"` - Note string `json:"note"` +type FlockDTO struct { + Id uint `json:"id"` + Name string `json:"name"` } -type ChickinSimpleDTO struct { - Id uint `json:"id"` - ProjectFlocId uint `json:"project_floc_id"` - ChickInDate time.Time `json:"chick_in_date"` - Quantity float64 `json:"quantity"` - Note string `json:"note"` - CreatedBy uint `json:"created_by"` +type KandangDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type ProductCategoryDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type AreaDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type FcrDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type LocationDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + +type ProjectFlockDTO struct { + Id uint `json:"id"` + Period int `json:"period"` + Flock *FlockDTO `json:"flock"` + ProductCategory *ProductCategoryDTO `json:"product_category"` + Area *AreaDTO `json:"area"` + Fcr *FcrDTO `json:"fcr"` + Location *LocationDTO `json:"location"` +} + +type ProjectFlockKandangDTO struct { + Id uint `json:"id"` + ProjectFlock *ProjectFlockDTO `json:"project_flock"` + Kandang *KandangDTO `json:"kandang"` } type UserBaseDTO struct { @@ -30,56 +59,159 @@ type UserBaseDTO struct { Name string `json:"name"` } +type ChickinBaseDTO struct { + Id uint `json:"id"` + ProjectFlocKandangId uint `json:"project_floc_kandang_id"` + ChickInDate time.Time `json:"chick_in_date"` + Quantity float64 `json:"quantity"` + Note string `json:"note"` +} + +type ChickinSimpleDTO struct { + Id uint `json:"id"` + ProjectFlocKandangId uint `json:"project_floc_kandang_id"` + ChickInDate time.Time `json:"chick_in_date"` + Quantity float64 `json:"quantity"` + Note string `json:"note"` + CreatedBy uint `json:"created_by"` +} + type ChickinListDTO struct { ChickinBaseDTO - ProjectFlock *ProjectFlockDTO `json:"project_flock"` - CreatedUser *UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` -} - -type ProjectFlockDTO struct { - Id uint `json:"id"` - Period int `json:"period"` - FlockId uint `json:"flock_id"` - FlockName string `json:"flock_name"` -} - -// === Mapper Functions === - -func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { - return ProjectFlockDTO{ - Id: e.Id, - Period: e.Period, - FlockId: e.FlockId, - FlockName: e.Flock.Name, - } + ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"` + CreatedUser *UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type ChickinDetailDTO struct { ChickinListDTO } -// === Mapper Functions === +// === Mapper Functions (ordered) === + +func ToFlockDTO(e entity.Flock) FlockDTO { + return FlockDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToKandangDTO(e entity.Kandang) KandangDTO { + return KandangDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToProductCategoryDTO(e entity.ProductCategory) ProductCategoryDTO { + return ProductCategoryDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToAreaDTO(e entity.Area) AreaDTO { + return AreaDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToFcrDTO(e entity.Fcr) FcrDTO { + return FcrDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToLocationDTO(e entity.Location) LocationDTO { + return LocationDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { + var flock *FlockDTO + if e.Flock.Id != 0 { + mapped := ToFlockDTO(e.Flock) + flock = &mapped + } + var productCategory *ProductCategoryDTO + if e.ProductCategory.Id != 0 { + mapped := ToProductCategoryDTO(e.ProductCategory) + productCategory = &mapped + } + var area *AreaDTO + if e.Area.Id != 0 { + mapped := ToAreaDTO(e.Area) + area = &mapped + } + var fcr *FcrDTO + if e.Fcr.Id != 0 { + mapped := ToFcrDTO(e.Fcr) + fcr = &mapped + } + var location *LocationDTO + if e.Location.Id != 0 { + mapped := ToLocationDTO(e.Location) + location = &mapped + } + return ProjectFlockDTO{ + Id: e.Id, + Period: e.Period, + Flock: flock, + ProductCategory: productCategory, + Area: area, + Fcr: fcr, + Location: location, + } +} + +func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangDTO { + var pf *ProjectFlockDTO + if e.ProjectFlock.Id != 0 { + mapped := ToProjectFlockDTO(e.ProjectFlock) + pf = &mapped + } + var kandang *KandangDTO + if e.Kandang.Id != 0 { + mapped := ToKandangDTO(e.Kandang) + kandang = &mapped + } + return ProjectFlockKandangDTO{ + Id: e.Id, + ProjectFlock: pf, + Kandang: kandang, + } +} + +func ToUserBaseDTO(e entity.User) UserBaseDTO { + return UserBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { return ChickinBaseDTO{ - Id: e.Id, - ProjectFlocId: e.ProjectFlocId, - ChickInDate: e.ChickInDate, - Quantity: e.Quantity, - Note: e.Note, + Id: e.Id, + ProjectFlocKandangId: e.ProjectFlocKandangId, + ChickInDate: e.ChickInDate, + Quantity: e.Quantity, + Note: e.Note, } } func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO { return ChickinSimpleDTO{ - Id: e.Id, - ProjectFlocId: e.ProjectFlocId, - ChickInDate: e.ChickInDate, - Quantity: e.Quantity, - Note: e.Note, - CreatedBy: e.CreatedBy, + Id: e.Id, + ProjectFlocKandangId: e.ProjectFlocKandangId, + ChickInDate: e.ChickInDate, + Quantity: e.Quantity, + Note: e.Note, + CreatedBy: e.CreatedBy, } } @@ -89,19 +221,17 @@ func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO { mapped := ToUserBaseDTO(e.CreatedUser) createdUser = &mapped } - - var projectFlock *ProjectFlockDTO - if e.ProjectFlock.Id != 0 { - mapped := ToProjectFlockDTO(e.ProjectFlock) - projectFlock = &mapped + var pfk *ProjectFlockKandangDTO + if e.ProjectFlockKandang.Id != 0 { + mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang) + pfk = &mapped } - return ChickinListDTO{ - ChickinBaseDTO: ToChickinBaseDTO(e), - ProjectFlock: projectFlock, - CreatedAt: e.CreatedAt, - UpdatedAt: e.UpdatedAt, - CreatedUser: createdUser, + ChickinBaseDTO: ToChickinBaseDTO(e), + ProjectFlockKandang: pfk, + CreatedUser: createdUser, + CreatedAt: e.CreatedAt, + UpdatedAt: e.UpdatedAt, } } @@ -126,10 +256,3 @@ func ToChickinDetailDTO(e entity.ProjectChickin) ChickinDetailDTO { ChickinListDTO: ToChickinListDTO(e), } } - -func ToUserBaseDTO(e entity.User) UserBaseDTO { - return UserBaseDTO{ - Id: e.Id, - Name: e.Name, - } -} diff --git a/internal/modules/production/chickins/module.go b/internal/modules/production/chickins/module.go index 30146724..abfc56ca 100644 --- a/internal/modules/production/chickins/module.go +++ b/internal/modules/production/chickins/module.go @@ -25,12 +25,13 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * kandangRepo := rKandang.NewKandangRepository(db) auditlogrepo := rAuditLog.NewAuditLogRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db) + projectflockkandangrepo := rProjectFlock.NewProjectFlockKandangRepository(db) projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) userRepo := rUser.NewUserRepository(db) - chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, auditlogrepo, validate) + chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, auditlogrepo, projectflockkandangrepo, validate) userService := sUser.NewUserService(userRepo, validate) ChickinRoutes(router, userService, chickinService) diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 75dc0242..fbb692fa 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -8,6 +8,7 @@ import ( KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories" + rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" @@ -30,34 +31,41 @@ type ChickinService interface { } type chickinService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.ProjectChickinRepository - KandangRepo KandangRepo.KandangRepository - WarehouseRepo rWarehouse.WarehouseRepository - ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository - ProjectFlockRepo rProjectFlock.ProjectflockRepository - AuditLogRepo AuditLogRepo.AuditLogRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ProjectChickinRepository + KandangRepo KandangRepo.KandangRepository + WarehouseRepo rWarehouse.WarehouseRepository + ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository + ProjectFlockRepo rProjectFlock.ProjectflockRepository + AuditLogRepo AuditLogRepo.AuditLogRepository + ProjectflockKandangRepo rProjectFlockKandang.ProjectFlockKandangRepository } -func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, auditLogRepo AuditLogRepo.AuditLogRepository, validate *validator.Validate) ChickinService { +func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, auditLogRepo AuditLogRepo.AuditLogRepository, projectflockkandangRepo rProjectFlockKandang.ProjectFlockKandangRepository, validate *validator.Validate) ChickinService { return &chickinService{ - Log: utils.Log, - Validate: validate, - Repository: repo, - KandangRepo: kandangRepo, - WarehouseRepo: warehouseRepo, - ProductWarehouseRepo: productWarehouseRepo, - ProjectFlockRepo: projectFlockRepo, - AuditLogRepo: auditLogRepo, + Log: utils.Log, + Validate: validate, + Repository: repo, + KandangRepo: kandangRepo, + WarehouseRepo: warehouseRepo, + ProductWarehouseRepo: productWarehouseRepo, + ProjectFlockRepo: projectFlockRepo, + AuditLogRepo: auditLogRepo, + ProjectflockKandangRepo: projectflockkandangRepo, } } func (s chickinService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). - Preload("ProjectFlock"). - Preload("ProjectFlock.ProductCategory") + Preload("ProjectFlockKandang.Kandang"). + Preload("ProjectFlockKandang.ProjectFlock"). + Preload("ProjectFlockKandang.ProjectFlock.Flock"). + Preload("ProjectFlockKandang.ProjectFlock.ProductCategory"). + Preload("ProjectFlockKandang.ProjectFlock.Area"). + Preload("ProjectFlockKandang.ProjectFlock.Fcr"). + Preload("ProjectFlockKandang.ProjectFlock.Location") } @@ -101,25 +109,27 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit } // ambil salah satu kandang dari project_floc_id dari kandang repository - kandang, err := s.KandangRepo.GetFirstByProjectFlockID(c.Context(), 1) + projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), 1) if err != nil { - s.Log.Errorf("Failed to get kandang: %+v", err) + s.Log.Errorf("Failed to get projectflock kandang: %+v", err) return nil, err } // ambil warehouse dari kandangid - warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandang.Id) + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId) if err != nil { s.Log.Errorf("Failed to get warehouse: %+v", err) return nil, err } + // getprojectflock id with relation projectFlock, err := s.ProjectFlockRepo.GetByID( c.Context(), - req.ProjectFlockId, + projectflockkandang.ProjectFlockId, func(db *gorm.DB) *gorm.DB { return db.Preload("ProductCategory") }, ) + if err != nil { s.Log.Errorf("Failed to get project flock: %+v", err) return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found") @@ -153,11 +163,11 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit } newChickin := &entity.ProjectChickin{ - ProjectFlocId: req.ProjectFlockId, - ChickInDate: chickinDate, - Quantity: productWarehouse.Quantity, - Note: "", - CreatedBy: 1, //todo: ganti dengan + ProjectFlocKandangId: projectflockkandang.ProjectFlockId, + ChickInDate: chickinDate, + Quantity: productWarehouse.Quantity, + Note: "", + CreatedBy: 1, //todo: ganti dengan } err = s.Repository.CreateOne(c.Context(), newChickin, nil) @@ -183,7 +193,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit stockAvailability := &entity.StockAvailability{ EntityType: entity.EntityTypeProjectFlockKandang, ReservedQuantity: productWarehouse.Quantity, - EntityId: req.ProjectFlockId, //todo: nanti pakek projct flock kandang id + EntityId: projectflockkandang.Id, ProductId: productWarehouse.ProductId, } @@ -218,7 +228,8 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit } } - return nil, nil + + return s.GetOne(c, newChickin.Id) } func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectChickin, error) { @@ -276,7 +287,7 @@ func (s *chickinService) Approve(c *fiber.Ctx, id uint) error { // get stock avaibility untuk di update var stockAvailability entity.StockAvailability err = s.ProductWarehouseRepo.DB().WithContext(c.Context()). - Where("entity_type = ? AND entity_id = ? ", entity.EntityTypeProjectFlockKandang, chickin.ProjectFlocId). + Where("entity_type = ? AND entity_id = ? ", entity.EntityTypeProjectFlockKandang, chickin.ProjectFlocKandangId). First(&stockAvailability).Error if err != nil { s.Log.Errorf("Failed to get stock availability: %+v", err) @@ -288,7 +299,5 @@ func (s *chickinService) Approve(c *fiber.Ctx, id uint) error { newReservedQuantity = 0 } - - return nil } diff --git a/internal/modules/production/chickins/validations/chickin.validation.go b/internal/modules/production/chickins/validations/chickin.validation.go index 152b3f22..b57950b0 100644 --- a/internal/modules/production/chickins/validations/chickin.validation.go +++ b/internal/modules/production/chickins/validations/chickin.validation.go @@ -1,8 +1,8 @@ package validation type Create struct { - ProjectFlockId uint `json:"project_flock_id" validate:"required,number,min=1"` - ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"` + ProjectFlockKandangId uint `json:"project_flock_kandang_id" validate:"required,number,min=1"` + ChickInDate string `json:"chick_in_date" validate:"required,datetime=2006-01-02"` } type Update struct { 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 9b89a399..9999e1a8 100644 --- a/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go +++ b/internal/modules/production/project_flocks/repositories/projectflock_kandang.repository.go @@ -9,6 +9,7 @@ import ( ) type ProjectFlockKandangRepository interface { + GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error MarkDetached(ctx context.Context, projectFlockID uint, kandangIDs []uint, detachedAt time.Time) error GetAll(ctx context.Context) ([]entity.ProjectFlockKandang, error) @@ -62,3 +63,16 @@ func (r *projectFlockKandangRepositoryImpl) WithTx(tx *gorm.DB) ProjectFlockKand func (r *projectFlockKandangRepositoryImpl) DB() *gorm.DB { return r.db } + +func (r *projectFlockKandangRepositoryImpl) GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error) { + record := new(entity.ProjectFlockKandang) + if err := r.db.WithContext(ctx). + Preload("ProjectFlock"). + Preload("ProjectFlock.Flock"). + Preload("Kandang"). + Preload("CreatedUser"). + First(record, id).Error; err != nil { + return nil, err + } + return record, nil +} From 7b99b395293d85e56896c8ec236ef60bcb44061e Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 20 Oct 2025 11:25:42 +0700 Subject: [PATCH 06/14] feat(BE-117): implement CRUD endpoints for project --- ...0649_create_project_chick_ins_table.up.sql | 8 +- ...e_project_flock_populations_table.down.sql | 1 + ...ate_project_flock_populations_table.up.sql | 36 +++++ internal/entities/project_chickin.go | 20 +-- internal/entities/project_flock_population.go | 22 +++ .../inventory/transfers/dto/transfer.dto.go | 52 ++++--- .../transfers/services/transfer.service.go | 4 +- .../controllers/chickin.controller.go | 8 +- .../production/chickins/dto/chickin.dto.go | 26 ++-- .../modules/production/chickins/module.go | 3 +- .../chickins/services/chickin.service.go | 130 +++++++++--------- .../validations/chickin.validation.go | 6 +- .../production/project_flocks/module.go | 1 + .../project_flock_population_repository.go | 35 +++++ 14 files changed, 233 insertions(+), 119 deletions(-) create mode 100644 internal/database/migrations/20251020022357_create_project_flock_populations_table.down.sql create mode 100644 internal/database/migrations/20251020022357_create_project_flock_populations_table.up.sql create mode 100644 internal/entities/project_flock_population.go create mode 100644 internal/modules/production/project_flocks/repositories/project_flock_population_repository.go diff --git a/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql b/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql index 04475e21..25d3476d 100644 --- a/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql +++ b/internal/database/migrations/20251018120649_create_project_chick_ins_table.up.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS project_chickins ( id BIGSERIAL PRIMARY KEY, - project_floc_kandang_id BIGINT NOT NULL, + project_flock_kandang_id BIGINT NOT NULL, chick_in_date DATE NOT NULL, quantity NUMERIC(15, 3) NOT NULL, note TEXT, @@ -15,8 +15,8 @@ DO $$ BEGIN IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN ALTER TABLE project_chickins - ADD CONSTRAINT fk_project_floc_kandang_id - FOREIGN KEY (project_floc_kandang_id) + ADD CONSTRAINT fk_project_flock_kandang_id + FOREIGN KEY (project_flock_kandang_id) REFERENCES project_flock_kandangs(id) ON DELETE RESTRICT ON UPDATE CASCADE; END IF; @@ -31,6 +31,6 @@ BEGIN END $$; -- INDEXES -CREATE INDEX IF NOT EXISTS idx_project_chickins_project_floc_kandang_id ON project_chickins (project_floc_kandang_id); +CREATE INDEX IF NOT EXISTS idx_project_chickins_project_flock_kandang_id ON project_chickins (project_flock_kandang_id); CREATE INDEX IF NOT EXISTS idx_project_chickins_created_by ON project_chickins (created_by); \ No newline at end of file diff --git a/internal/database/migrations/20251020022357_create_project_flock_populations_table.down.sql b/internal/database/migrations/20251020022357_create_project_flock_populations_table.down.sql new file mode 100644 index 00000000..8fa11576 --- /dev/null +++ b/internal/database/migrations/20251020022357_create_project_flock_populations_table.down.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS project_flock_populations; \ No newline at end of file diff --git a/internal/database/migrations/20251020022357_create_project_flock_populations_table.up.sql b/internal/database/migrations/20251020022357_create_project_flock_populations_table.up.sql new file mode 100644 index 00000000..82b3e9a7 --- /dev/null +++ b/internal/database/migrations/20251020022357_create_project_flock_populations_table.up.sql @@ -0,0 +1,36 @@ +CREATE TABLE IF NOT EXISTS project_flock_populations ( + id BIGSERIAL PRIMARY KEY, + project_flock_kandang_id BIGINT NOT NULL, + initial_quantity NUMERIC(15, 3) NOT NULL, + current_quantity NUMERIC(15, 3) NOT NULL, + reserved_quantity NUMERIC(15, 3), + created_by BIGINT NOT NULL, + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + deleted_at TIMESTAMPTZ +); + +-- FOREIGN KEYS (dijalankan setelah semua tabel parent ada) +DO $$ +BEGIN + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flock_kandangs') THEN + ALTER TABLE project_flock_populations + ADD CONSTRAINT fk_project_flock_kandang_id + FOREIGN KEY (project_flock_kandang_id) + REFERENCES project_flock_kandangs(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; + + IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'users') THEN + ALTER TABLE project_flock_populations + ADD CONSTRAINT fk_created_by + FOREIGN KEY (created_by) + REFERENCES users(id) + ON DELETE RESTRICT ON UPDATE CASCADE; + END IF; +END $$; + +-- INDEXES +CREATE INDEX IF NOT EXISTS idx_project_flock_populations_project_flock_kandang_id ON project_flock_populations (project_flock_kandang_id); + +CREATE INDEX IF NOT EXISTS idx_project_flock_populations_created_by ON project_flock_populations (created_by); \ No newline at end of file diff --git a/internal/entities/project_chickin.go b/internal/entities/project_chickin.go index 07536187..95a658c8 100644 --- a/internal/entities/project_chickin.go +++ b/internal/entities/project_chickin.go @@ -9,16 +9,16 @@ import ( const () type ProjectChickin struct { - Id uint `gorm:"primaryKey"` - ProjectFlocKandangId uint `gorm:"not null"` - ChickInDate time.Time `gorm:"not null"` - Quantity float64 `gorm:"not null"` - Note 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:"-"` + 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"` + 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:ProjectFlocKandangId;references:Id"` + ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;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 new file mode 100644 index 00000000..184ace65 --- /dev/null +++ b/internal/entities/project_flock_population.go @@ -0,0 +1,22 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +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"` + + ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` + CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` +} diff --git a/internal/modules/inventory/transfers/dto/transfer.dto.go b/internal/modules/inventory/transfers/dto/transfer.dto.go index 217e5038..82269852 100644 --- a/internal/modules/inventory/transfers/dto/transfer.dto.go +++ b/internal/modules/inventory/transfers/dto/transfer.dto.go @@ -23,6 +23,11 @@ type WarehouseSimpleDTO struct { Name string `json:"name"` } +type ProductSimpleDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + type AreaDTO struct { Id uint `json:"id"` Name string `json:"name"` @@ -33,6 +38,11 @@ type LocationDTO struct { Name string `json:"name"` } +type SuplierSimpleDTO struct { + Id uint `json:"id"` + Name string `json:"name"` +} + type WarehouseDetailDTO struct { Id uint `json:"id"` Name string `json:"name"` @@ -57,17 +67,15 @@ type TransferDetailDTO struct { // Detail produk type TransferDetailItemDTO struct { - Id uint64 `json:"id"` - ProductId uint64 `json:"product_id"` - Quantity float64 `json:"quantity"` - BeforeQuantity float64 `json:"before_quantity"` - AfterQuantity float64 `json:"after_quantity"` + Id uint64 `json:"id"` + Proudct ProductSimpleDTO `json:"product"` + Quantity float64 `json:"quantity"` } // Delivery ekspedisi type TransferDeliveryDTO struct { Id uint64 `json:"id"` - SupplierId uint64 `json:"supplier_id"` + Suplier SuplierSimpleDTO `json:"suplier"` VehiclePlate string `json:"vehicle_plate"` DriverName string `json:"driver_name"` DocumentNumber string `json:"document_number"` @@ -146,9 +154,12 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { var details []TransferDetailItemDTO for _, d := range e.Details { details = append(details, TransferDetailItemDTO{ - Id: d.Id, - ProductId: d.ProductId, - Quantity: d.Quantity, + Id: d.Id, + Proudct: ProductSimpleDTO{ + Id: d.Product.Id, + Name: d.Product.Name, + }, + Quantity: d.Quantity, }) } // Map deliveries @@ -164,8 +175,11 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { }) } deliveries = append(deliveries, TransferDeliveryDTO{ - Id: del.Id, - SupplierId: del.SupplierId, + Id: del.Id, + Suplier: SuplierSimpleDTO{ + Id: del.Supplier.Id, + Name: del.Supplier.Name, + }, VehiclePlate: del.VehiclePlate, DriverName: del.DriverName, DocumentNumber: del.DocumentNumber, @@ -198,17 +212,23 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO { var details []TransferDetailItemDTO for _, d := range e.Details { details = append(details, TransferDetailItemDTO{ - Id: d.Id, - ProductId: d.ProductId, - Quantity: d.Quantity, + Id: d.Id, + Proudct: ProductSimpleDTO{ + Id: d.Product.Id, + Name: d.Product.Name, + }, + Quantity: d.Quantity, }) } // Map deliveries var deliveries []TransferDeliveryDTO for _, del := range e.Deliveries { deliveries = append(deliveries, TransferDeliveryDTO{ - Id: del.Id, - SupplierId: del.SupplierId, + Id: del.Id, + Suplier: SuplierSimpleDTO{ + Id: del.Supplier.Id, + Name: del.Supplier.Name, + }, VehiclePlate: del.VehiclePlate, DriverName: del.DriverName, DocumentNumber: del.DocumentNumber, diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index bdc8abf6..dbb4694b 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -60,7 +60,9 @@ func (s transferService) withRelations(db *gorm.DB) *gorm.DB { Preload("ToWarehouse.Location"). Preload("ToWarehouse.Area"). Preload("Details"). - Preload("Deliveries.Items") + Preload("Details.Product"). + Preload("Deliveries.Items"). + Preload("Deliveries.Supplier") } func (s transferService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.StockTransfer, int64, error) { diff --git a/internal/modules/production/chickins/controllers/chickin.controller.go b/internal/modules/production/chickins/controllers/chickin.controller.go index 6514f8c8..fadcbc3e 100644 --- a/internal/modules/production/chickins/controllers/chickin.controller.go +++ b/internal/modules/production/chickins/controllers/chickin.controller.go @@ -24,9 +24,9 @@ func NewChickinController(chickinService service.ChickinService) *ChickinControl func (u *ChickinController) GetAll(c *fiber.Ctx) error { query := &validation.Query{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - Search: c.Query("search", ""), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + ProjectFlockKandangId: uint(c.QueryInt("project_flock_kandang_id", 0)), } result, totalResults, err := u.ChickinService.GetAll(c, query) @@ -88,7 +88,7 @@ func (u *ChickinController) CreateOne(c *fiber.Ctx) error { Code: fiber.StatusCreated, Status: "success", Message: "Create chickin successfully", - Data: result, + Data: dto.ToChickinListDTO(*result), }) } diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index c89caa0e..9fd29f3c 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -68,12 +68,12 @@ type ChickinBaseDTO struct { } type ChickinSimpleDTO struct { - Id uint `json:"id"` - ProjectFlocKandangId uint `json:"project_floc_kandang_id"` - ChickInDate time.Time `json:"chick_in_date"` - Quantity float64 `json:"quantity"` - Note string `json:"note"` - CreatedBy uint `json:"created_by"` + 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"` + CreatedBy uint `json:"created_by"` } type ChickinListDTO struct { @@ -197,7 +197,7 @@ func ToUserBaseDTO(e entity.User) UserBaseDTO { func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { return ChickinBaseDTO{ Id: e.Id, - ProjectFlocKandangId: e.ProjectFlocKandangId, + ProjectFlocKandangId: e.ProjectFlockKandangId, ChickInDate: e.ChickInDate, Quantity: e.Quantity, Note: e.Note, @@ -206,12 +206,12 @@ func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO { return ChickinSimpleDTO{ - Id: e.Id, - ProjectFlocKandangId: e.ProjectFlocKandangId, - ChickInDate: e.ChickInDate, - Quantity: e.Quantity, - Note: e.Note, - CreatedBy: e.CreatedBy, + Id: e.Id, + ProjectFlockKandangId: e.ProjectFlockKandangId, + ChickInDate: e.ChickInDate, + Quantity: e.Quantity, + Note: e.Note, + CreatedBy: e.CreatedBy, } } diff --git a/internal/modules/production/chickins/module.go b/internal/modules/production/chickins/module.go index abfc56ca..116e2fbb 100644 --- a/internal/modules/production/chickins/module.go +++ b/internal/modules/production/chickins/module.go @@ -26,12 +26,13 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate * auditlogrepo := rAuditLog.NewAuditLogRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db) projectflockkandangrepo := rProjectFlock.NewProjectFlockKandangRepository(db) + projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db) projectFlockRepo := rProjectFlock.NewProjectflockRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) userRepo := rUser.NewUserRepository(db) - chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, auditlogrepo, projectflockkandangrepo, validate) + chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, auditlogrepo, projectflockkandangrepo, projectflockpopulationrepo, validate) userService := sUser.NewUserService(userRepo, validate) ChickinRoutes(router, userService, chickinService) diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index fbb692fa..a11b21f7 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -31,28 +31,30 @@ type ChickinService interface { } type chickinService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.ProjectChickinRepository - KandangRepo KandangRepo.KandangRepository - WarehouseRepo rWarehouse.WarehouseRepository - ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository - ProjectFlockRepo rProjectFlock.ProjectflockRepository - AuditLogRepo AuditLogRepo.AuditLogRepository - ProjectflockKandangRepo rProjectFlockKandang.ProjectFlockKandangRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ProjectChickinRepository + KandangRepo KandangRepo.KandangRepository + WarehouseRepo rWarehouse.WarehouseRepository + ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository + ProjectFlockRepo rProjectFlock.ProjectflockRepository + AuditLogRepo AuditLogRepo.AuditLogRepository + ProjectflockKandangRepo rProjectFlockKandang.ProjectFlockKandangRepository + ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository } -func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, auditLogRepo AuditLogRepo.AuditLogRepository, projectflockkandangRepo rProjectFlockKandang.ProjectFlockKandangRepository, validate *validator.Validate) ChickinService { +func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, auditLogRepo AuditLogRepo.AuditLogRepository, projectflockkandangRepo rProjectFlockKandang.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, validate *validator.Validate) ChickinService { return &chickinService{ - Log: utils.Log, - Validate: validate, - Repository: repo, - KandangRepo: kandangRepo, - WarehouseRepo: warehouseRepo, - ProductWarehouseRepo: productWarehouseRepo, - ProjectFlockRepo: projectFlockRepo, - AuditLogRepo: auditLogRepo, - ProjectflockKandangRepo: projectflockkandangRepo, + Log: utils.Log, + Validate: validate, + Repository: repo, + KandangRepo: kandangRepo, + WarehouseRepo: warehouseRepo, + ProductWarehouseRepo: productWarehouseRepo, + ProjectFlockRepo: projectFlockRepo, + AuditLogRepo: auditLogRepo, + ProjectflockKandangRepo: projectflockkandangRepo, + ProjectflockPopulationRepo: projectflockpopulationRepo, } } @@ -78,8 +80,9 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity chickins, 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+"%") + + if params.ProjectFlockKandangId != 0 { + return db.Where("project_flock_kandang_id = ?", params.ProjectFlockKandangId) } return db.Order("created_at DESC").Order("updated_at DESC") }) @@ -163,11 +166,11 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit } newChickin := &entity.ProjectChickin{ - ProjectFlocKandangId: projectflockkandang.ProjectFlockId, - ChickInDate: chickinDate, - Quantity: productWarehouse.Quantity, - Note: "", - CreatedBy: 1, //todo: ganti dengan + ProjectFlockKandangId: projectflockkandang.ProjectFlockId, + ChickInDate: chickinDate, + Quantity: productWarehouse.Quantity, + Note: "", + CreatedBy: 1, //todo: ganti dengan } err = s.Repository.CreateOne(c.Context(), newChickin, nil) @@ -188,45 +191,37 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) return nil, err } - - // masukan check apakah stock availability ada, jika ada update, jika tidak buat baru - stockAvailability := &entity.StockAvailability{ - EntityType: entity.EntityTypeProjectFlockKandang, - ReservedQuantity: productWarehouse.Quantity, - EntityId: projectflockkandang.Id, - ProductId: productWarehouse.ProductId, + // masukan data nya ke project flock population + // check apakah sudah ada + 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 { + // update quantity - var existingStockAvailability entity.StockAvailability - err = s.ProductWarehouseRepo.DB().WithContext(c.Context()). - Where("entity_type = ? AND entity_id = ? AND product_id = ?", stockAvailability.EntityType, stockAvailability.EntityId, stockAvailability.ProductId). - First(&existingStockAvailability).Error - - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - // buat baru - stockAvailability.ReservedQuantity = newChickin.Quantity - stockAvailability.Quantity = 0 - err = s.ProductWarehouseRepo.DB().WithContext(c.Context()).Create(stockAvailability).Error - if err != nil { - s.Log.Errorf("Failed to create stock availability: %+v", err) - return nil, err - } - } else { - s.Log.Errorf("Failed to get stock availability: %+v", err) + 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 { - // update existing - newQuantity := existingStockAvailability.ReservedQuantity + newChickin.Quantity - err = s.ProductWarehouseRepo.DB().WithContext(c.Context()). - Model(&existingStockAvailability). - Update("reserved_quantity", newQuantity).Error + // create new population + 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 update stock availability: %+v", err) + s.Log.Errorf("Failed to create project flock population: %+v", err) return nil, err } - } return s.GetOne(c, newChickin.Id) @@ -283,20 +278,21 @@ func (s *chickinService) Approve(c *fiber.Ctx, id uint) error { return err } - //pindahkan stock dari reserved ke actual stock - // get stock avaibility untuk di update - var stockAvailability entity.StockAvailability - err = s.ProductWarehouseRepo.DB().WithContext(c.Context()). - Where("entity_type = ? AND entity_id = ? ", entity.EntityTypeProjectFlockKandang, chickin.ProjectFlocKandangId). - First(&stockAvailability).Error + //pindahkan stock dari reserved ke actual stock pada table project flock population + population, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId) if err != nil { - s.Log.Errorf("Failed to get stock availability: %+v", err) + s.Log.Errorf("Failed to get project flock population: %+v", err) return err } - newReservedQuantity := stockAvailability.ReservedQuantity - chickin.Quantity - if newReservedQuantity < 0 { - newReservedQuantity = 0 + 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 diff --git a/internal/modules/production/chickins/validations/chickin.validation.go b/internal/modules/production/chickins/validations/chickin.validation.go index b57950b0..c122c100 100644 --- a/internal/modules/production/chickins/validations/chickin.validation.go +++ b/internal/modules/production/chickins/validations/chickin.validation.go @@ -10,7 +10,7 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` - Search string `query:"search" validate:"omitempty,max=50"` + Page int `query:"page" validate:"omitempty,number,min=1"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + ProjectFlockKandangId uint `query:"project_flock_kandang_id" validate:"omitempty,number,min=1"` } diff --git a/internal/modules/production/project_flocks/module.go b/internal/modules/production/project_flocks/module.go index 5f1afbe3..5b91ab13 100644 --- a/internal/modules/production/project_flocks/module.go +++ b/internal/modules/production/project_flocks/module.go @@ -8,6 +8,7 @@ import ( rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" + sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" 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 new file mode 100644 index 00000000..cb4b0d5f --- /dev/null +++ b/internal/modules/production/project_flocks/repositories/project_flock_population_repository.go @@ -0,0 +1,35 @@ +package repository + +import ( + "context" + + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type ProjectFlockPopulationRepository interface { + repository.BaseRepository[entity.ProjectFlockPopulation] + GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) +} + +type projectFlockPopulationRepositoryImpl struct { + *repository.BaseRepositoryImpl[entity.ProjectFlockPopulation] +} + +func NewProjectFlockPopulationRepository(db *gorm.DB) ProjectFlockPopulationRepository { + return &projectFlockPopulationRepositoryImpl{ + BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockPopulation](db), + } +} + +func (r *projectFlockPopulationRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (*entity.ProjectFlockPopulation, error) { + var record entity.ProjectFlockPopulation + err := r.DB().WithContext(ctx). + Where("project_flock_kandang_id = ?", projectFlockKandangID). + First(&record).Error + if err != nil { + return nil, err + } + return &record, nil +} From 748c959dbe53263998054b441010ff616d18a923 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 20 Oct 2025 11:36:38 +0700 Subject: [PATCH 07/14] FIX[BE]: fix json wrong json field name --- internal/modules/inventory/transfers/dto/transfer.dto.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/modules/inventory/transfers/dto/transfer.dto.go b/internal/modules/inventory/transfers/dto/transfer.dto.go index 82269852..cb85af94 100644 --- a/internal/modules/inventory/transfers/dto/transfer.dto.go +++ b/internal/modules/inventory/transfers/dto/transfer.dto.go @@ -38,7 +38,7 @@ type LocationDTO struct { Name string `json:"name"` } -type SuplierSimpleDTO struct { +type SupplierSimpleDTO struct { Id uint `json:"id"` Name string `json:"name"` } @@ -75,7 +75,7 @@ type TransferDetailItemDTO struct { // Delivery ekspedisi type TransferDeliveryDTO struct { Id uint64 `json:"id"` - Suplier SuplierSimpleDTO `json:"suplier"` + Supplier SupplierSimpleDTO `json:"supplier"` VehiclePlate string `json:"vehicle_plate"` DriverName string `json:"driver_name"` DocumentNumber string `json:"document_number"` @@ -176,7 +176,7 @@ func ToTransferListDTO(e entity.StockTransfer) TransferListDTO { } deliveries = append(deliveries, TransferDeliveryDTO{ Id: del.Id, - Suplier: SuplierSimpleDTO{ + Supplier: SupplierSimpleDTO{ Id: del.Supplier.Id, Name: del.Supplier.Name, }, @@ -225,7 +225,7 @@ func ToTransferDetailDTO(e entity.StockTransfer) TransferDetailDTO { for _, del := range e.Deliveries { deliveries = append(deliveries, TransferDeliveryDTO{ Id: del.Id, - Suplier: SuplierSimpleDTO{ + Supplier: SupplierSimpleDTO{ Id: del.Supplier.Id, Name: del.Supplier.Name, }, From a1f579f61651d2e92df7322be1e40a29d488a647 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Mon, 20 Oct 2025 12:55:19 +0700 Subject: [PATCH 08/14] feat(BE-119,135): add seeding and API documentation - Implement project data seeding logic - Add API documentation using Hoppscotch --- internal/database/seed/seeder.go | 78 +++++++++++++- .../chickins/services/chickin.service.go | 101 ++++++++++++++---- 2 files changed, 156 insertions(+), 23 deletions(-) diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index afa2a308..aa70b084 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -92,6 +92,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 + } fmt.Println("✅ Master data seeding completed") return nil @@ -981,6 +984,7 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error { {ProductID: 1, WarehouseID: 1, Quantity: 100}, {ProductID: 2, WarehouseID: 2, Quantity: 200}, {ProductID: 2, WarehouseID: 1, Quantity: 300}, + {ProductID: 1, WarehouseID: 3, Quantity: 5000}, } for _, seed := range seeds { @@ -1005,8 +1009,7 @@ func seedProductWarehouse(tx *gorm.DB, createdBy uint) error { } func seedTransferStock(tx *gorm.DB, createdBy uint) error { - // Seeder Transfer Stock - // 1. Insert StockTransfer (header) + transfer := entity.StockTransfer{ FromWarehouseId: 1, ToWarehouseId: 2, @@ -1019,7 +1022,6 @@ func seedTransferStock(tx *gorm.DB, createdBy uint) error { return err } - // 2. Insert StockTransferDetail (detail) details := []entity.StockTransferDetail{ { StockTransferId: transfer.Id, @@ -1038,7 +1040,6 @@ func seedTransferStock(tx *gorm.DB, createdBy uint) error { } } - // 3. Insert StockTransferDelivery (delivery) deliveries := []entity.StockTransferDelivery{ { StockTransferId: transfer.Id, @@ -1082,6 +1083,75 @@ 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"}, + } + + 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 + } + + // Update/Insert ProjectFlockPopulation + var population entity.ProjectFlockPopulation + err = tx.Where("project_flock_kandang_id = ?", seed.ProjectFlockKandangId).First(&population).Error + if errors.Is(err, gorm.ErrRecordNotFound) { + 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 + } + } + } + + return nil +} + func ptr[T any](v T) *T { return &v } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index a11b21f7..f866e96d 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -77,16 +77,13 @@ 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 { return db.Where("project_flock_kandang_id = ?", params.ProjectFlockKandangId) } return db.Order("created_at DESC").Order("updated_at DESC") }) - if err != nil { s.Log.Errorf("Failed to get chickins: %+v", err) return nil, 0, err @@ -95,6 +92,7 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity } func (s chickinService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectChickin, error) { + chickin, err := s.Repository.GetByID(c.Context(), id, s.withRelations) if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found") @@ -111,20 +109,18 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, err } - // ambil salah satu kandang dari project_floc_id dari kandang repository projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), 1) if err != nil { s.Log.Errorf("Failed to get projectflock kandang: %+v", err) return nil, err } - // ambil warehouse dari 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 } - // getprojectflock id with relation projectFlock, err := s.ProjectFlockRepo.GetByID( c.Context(), projectflockkandang.ProjectFlockId, @@ -132,20 +128,19 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return db.Preload("ProductCategory") }, ) - if err != nil { s.Log.Errorf("Failed to get project flock: %+v", err) return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found") } - // ambil quantity + var productWarehouse entity.ProductWarehouse - err = s.ProductWarehouseRepo.DB().WithContext(c.Context()). + err = s.ProductWarehouseRepo.DB(). + WithContext(c.Context()). Joins("JOIN products ON products.id = product_warehouses.product_id"). Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", projectFlock.ProductCategory.Code, warehouse.Id). Order("created_at DESC"). First(&productWarehouse).Error - if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse") @@ -158,13 +153,11 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient product quantity in warehouse") } - // masukan ke chic in 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.ProjectFlockId, ChickInDate: chickinDate, @@ -172,14 +165,12 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit Note: "", CreatedBy: 1, //todo: ganti dengan } - err = s.Repository.CreateOne(c.Context(), newChickin, nil) if err != nil { s.Log.Errorf("Failed to create chickin: %+v", err) return nil, err } - // Kurangi quantity di product warehouse updatedQuantity := productWarehouse.Quantity - newChickin.Quantity if updatedQuantity < 0 { updatedQuantity = 0 @@ -191,15 +182,13 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) return nil, err } - // masukan data nya ke project flock population - // check apakah sudah ada + 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 { - // update quantity err = s.ProjectflockPopulationRepo.PatchOne(c.Context(), existingPopulation.Id, map[string]any{ "reserved_quantity": newChickin.Quantity + existingPopulation.ReservedQuantity, @@ -209,7 +198,6 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, err } } else { - // create new population newPopulation := &entity.ProjectFlockPopulation{ ProjectFlockKandangId: req.ProjectFlockKandangId, InitialQuantity: 0, @@ -253,6 +241,31 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) } func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { + // todo: cek apakah chickin sudah di approve atau belum + + chickin, err := s.Repository.GetByID(c.Context(), id, nil) + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Chickin not found") + } + 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, + }, nil) + if err != nil { + s.Log.Errorf("Failed to update project flock population: %+v", err) + return err + } + if err := s.Repository.DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Chickin not found") @@ -260,11 +273,62 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { s.Log.Errorf("Failed to delete chickin: %+v", err) return err } + + projectflockkandang, err := s.ProjectflockKandangRepo.GetByID(c.Context(), population.ProjectFlockKandangId) + if err != nil { + s.Log.Errorf("Failed to get projectflock kandang: %+v", err) + return err + } + warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), projectflockkandang.KandangId) + if err != nil { + s.Log.Errorf("Failed to get warehouse: %+v", err) + return err + } + + projectFlock, err := s.ProjectFlockRepo.GetByID( + c.Context(), + projectflockkandang.ProjectFlockId, + func(db *gorm.DB) *gorm.DB { + return db.Preload("ProductCategory") + }, + ) + + if err != nil { + s.Log.Errorf("Failed to get project flock: %+v", err) + return fiber.NewError(fiber.StatusNotFound, "Project Flock not found") + } + var productWarehouse entity.ProductWarehouse + err = s.ProductWarehouseRepo.DB().WithContext(c.Context()). + Joins("JOIN products ON products.id = product_warehouses.product_id"). + Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). + Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", projectFlock.ProductCategory.Code, warehouse.Id). + Order("created_at DESC"). + First(&productWarehouse).Error + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse") + } + s.Log.Errorf("Failed to get product warehouse: %+v", err) + return err + } + + updatedQuantity := productWarehouse.Quantity + chickin.Quantity + err = s.ProductWarehouseRepo.PatchOne(c.Context(), productWarehouse.Id, map[string]any{ + "quantity": updatedQuantity, + }, nil) + if err != nil { + s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) + return err + } + return 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, @@ -278,7 +342,6 @@ func (s *chickinService) Approve(c *fiber.Ctx, id uint) error { return err } - //pindahkan stock dari reserved ke actual stock pada table project flock population population, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), chickin.ProjectFlockKandangId) if err != nil { s.Log.Errorf("Failed to get project flock population: %+v", err) From 542e5033606c74acc43688f7c0eb0b10aa5775aa Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 21 Oct 2025 09:01:02 +0700 Subject: [PATCH 09/14] fix[BE]: change dummy document path on transfer create --- .../modules/inventory/transfers/services/transfer.service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index dbb4694b..90642f6c 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -210,7 +210,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques SupplierId: uint64(delivery.SupplierID), VehiclePlate: delivery.VehiclePlate, DriverName: delivery.DriverName, - DocumentPath: "dummy duls", // todo: tunggu ada aws baru proses + DocumentPath: "https://tourism.gov.in/sites/default/files/2019-04/dummy-pdf_2.pdf", // todo: tunggu ada aws baru proses ShippingCostItem: delivery.DeliveryCostPerItem, ShippingCostTotal: delivery.DeliveryCost, }) From 1afbdea4ffb2a66471f167f27b7d32d9436eff50 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 21 Oct 2025 10:20:34 +0700 Subject: [PATCH 10/14] fix[BE]: fix logic pengambilan quatity untuk chick in dan penggunaan helper common --- ...create_stock_availabilities_table.down.sql | 1 - ...6_create_stock_availabilities_table.up.sql | 15 ------ ...019141014_create_audit_logs_table.down.sql | 1 - ...51019141014_create_audit_logs_table.up.sql | 13 ----- .../controllers/adjustment.controller.go | 4 +- .../services/adjustment.service.go | 28 +++-------- .../validations/adjustment.validation.go | 4 +- .../chickins/services/chickin.service.go | 49 ++++++++++--------- 8 files changed, 39 insertions(+), 76 deletions(-) delete mode 100644 internal/database/migrations/20251019040246_create_stock_availabilities_table.down.sql delete mode 100644 internal/database/migrations/20251019040246_create_stock_availabilities_table.up.sql delete mode 100644 internal/database/migrations/20251019141014_create_audit_logs_table.down.sql delete mode 100644 internal/database/migrations/20251019141014_create_audit_logs_table.up.sql diff --git a/internal/database/migrations/20251019040246_create_stock_availabilities_table.down.sql b/internal/database/migrations/20251019040246_create_stock_availabilities_table.down.sql deleted file mode 100644 index 1d50d98b..00000000 --- a/internal/database/migrations/20251019040246_create_stock_availabilities_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS stock_availabilities; \ No newline at end of file diff --git a/internal/database/migrations/20251019040246_create_stock_availabilities_table.up.sql b/internal/database/migrations/20251019040246_create_stock_availabilities_table.up.sql deleted file mode 100644 index bce6f7e6..00000000 --- a/internal/database/migrations/20251019040246_create_stock_availabilities_table.up.sql +++ /dev/null @@ -1,15 +0,0 @@ -CREATE TABLE stock_availabilities ( - id BIGSERIAL PRIMARY KEY, - entity_type VARCHAR(50) NOT NULL, - entity_id BIGINT NOT NULL, - product_id BIGINT, - quantity NUMERIC(15, 3) NOT NULL DEFAULT 0, - reserved_quantity NUMERIC(15, 3) NOT NULL DEFAULT 0, - unit VARCHAR(20), - last_updated TIMESTAMPTZ DEFAULT now(), - created_at TIMESTAMPTZ DEFAULT now(), - deleted_at TIMESTAMPTZ -); - -ALTER TABLE stock_availabilities -ADD CONSTRAINT fk_product_id FOREIGN KEY (product_id) REFERENCES products (id); \ No newline at end of file diff --git a/internal/database/migrations/20251019141014_create_audit_logs_table.down.sql b/internal/database/migrations/20251019141014_create_audit_logs_table.down.sql deleted file mode 100644 index 4cf6b411..00000000 --- a/internal/database/migrations/20251019141014_create_audit_logs_table.down.sql +++ /dev/null @@ -1 +0,0 @@ -DROP TABLE IF EXISTS audit_logs; \ No newline at end of file diff --git a/internal/database/migrations/20251019141014_create_audit_logs_table.up.sql b/internal/database/migrations/20251019141014_create_audit_logs_table.up.sql deleted file mode 100644 index 13731dcc..00000000 --- a/internal/database/migrations/20251019141014_create_audit_logs_table.up.sql +++ /dev/null @@ -1,13 +0,0 @@ -CREATE TABLE audit_logs ( - id BIGSERIAL PRIMARY KEY, - table_name VARCHAR(100) NOT NULL, - record_id BIGINT NOT NULL, - action VARCHAR(30) NOT NULL, - before_data JSONB, - after_data JSONB, - changed_by BIGINT, - created_at TIMESTAMPTZ DEFAULT now() -); - -ALTER TABLE audit_logs -ADD CONSTRAINT fk_changed_by FOREIGN KEY (changed_by) REFERENCES users (id); \ No newline at end of file diff --git a/internal/modules/inventory/adjustments/controllers/adjustment.controller.go b/internal/modules/inventory/adjustments/controllers/adjustment.controller.go index dc3df0a9..617a1b5f 100644 --- a/internal/modules/inventory/adjustments/controllers/adjustment.controller.go +++ b/internal/modules/inventory/adjustments/controllers/adjustment.controller.go @@ -49,8 +49,8 @@ func (u *AdjustmentController) AdjustmentHistory(c *fiber.Ctx) error { query := &validation.Query{ Page: c.QueryInt("page", 1), Limit: c.QueryInt("limit", 10), - ProductID: c.QueryInt("product_id", 0), - WarehouseID: c.QueryInt("warehouse_id", 0), + ProductID: uint(c.QueryInt("product_id", 0)), + WarehouseID: uint(c.QueryInt("warehouse_id", 0)), TransactionType: c.Query("transaction_type", ""), } diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index 69654b85..7a2d06bc 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -4,6 +4,8 @@ import ( "errors" "strings" + common "gitlab.com/mbugroup/lti-api.git/internal/common/service" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations" ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" @@ -77,22 +79,11 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } ctx := c.Context() - isProductExist, err := s.ProductRepo.IdExists(c.Context(), uint(req.ProductID)) - if err != nil { - s.Log.Errorf("Failed to check product existence: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product") - } - if !isProductExist { - return nil, fiber.NewError(fiber.StatusNotFound, "Product not found") - } - - isWarehouseExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(req.WarehouseID)) - if err != nil { - s.Log.Errorf("Failed to check warehouse existence: %+v", err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse") - } - if !isWarehouseExist { - return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found") + if err := common.EnsureRelations(c.Context(), + common.RelationCheck{Name: "Product", ID: &req.ProductID, Exists: s.ProductRepo.IdExists}, + common.RelationCheck{Name: "Warehouse", ID: &req.WarehouseID, Exists: s.WarehouseRepo.IdExists}, + ); err != nil { + return nil, err } if req.Quantity <= 0 { @@ -118,6 +109,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e Quantity: 0, CreatedBy: 1, // TODO: should Get from auth middleware } + if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil { s.Log.Errorf("Failed to create product warehouse: %+v", err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create product warehouse") @@ -126,7 +118,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e } err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { - productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID)) if err != nil { s.Log.Errorf("Failed to get product warehouse: %+v", err) @@ -159,14 +150,12 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e s.Log.Errorf("Failed to create stock log: %+v", err) return err } - s.Log.Infof("Stock log created: %+v", newLog.Id) productWarehouse.Quantity = afterQuantity if err := s.ProductWarehouseRepo.WithTx(tx).UpdateOne(ctx, productWarehouse.Id, productWarehouse, nil); err != nil { s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) return err } - s.Log.Infof("Product warehouse quantity updated: %+v", productWarehouse.Id) createdLogId = newLog.Id return nil @@ -184,7 +173,6 @@ func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Qu if err := s.Validate.Struct(query); err != nil { return nil, 0, err } - offset := (query.Page - 1) * query.Limit isWarehousesExist, err := s.WarehouseRepo.IdExists(c.Context(), uint(query.WarehouseID)) diff --git a/internal/modules/inventory/adjustments/validations/adjustment.validation.go b/internal/modules/inventory/adjustments/validations/adjustment.validation.go index 7d2385cc..2e7259f2 100644 --- a/internal/modules/inventory/adjustments/validations/adjustment.validation.go +++ b/internal/modules/inventory/adjustments/validations/adjustment.validation.go @@ -11,7 +11,7 @@ type Create struct { type Query struct { Page int `query:"page" validate:"omitempty,min=1"` Limit int `query:"limit" validate:"omitempty,min=1,max=100"` - ProductID int `query:"product_id" validate:"omitempty,min=0"` - WarehouseID int `query:"warehouse_id" validate:"omitempty,min=0"` + ProductID uint `query:"product_id" validate:"omitempty,min=0"` + WarehouseID uint `query:"warehouse_id" validate:"omitempty,min=0"` TransactionType string `query:"transaction_type" validate:"omitempty,oneof=increase decrease"` } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index f866e96d..64fe1e97 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -132,27 +132,33 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit s.Log.Errorf("Failed to get project flock: %+v", err) return nil, fiber.NewError(fiber.StatusNotFound, "Project Flock not found") } - - var productWarehouse entity.ProductWarehouse + var productWarehouses []entity.ProductWarehouse err = s.ProductWarehouseRepo.DB(). WithContext(c.Context()). Joins("JOIN products ON products.id = product_warehouses.product_id"). Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", projectFlock.ProductCategory.Code, warehouse.Id). Order("created_at DESC"). - First(&productWarehouse).Error + Find(&productWarehouses).Error if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, 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) + s.Log.Errorf("Failed to get product warehouses: %+v", err) return nil, err } - - if productWarehouse.Quantity < 1 { - return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient product quantity in warehouse") + if len(productWarehouses) == 0 { + return nil, fiber.NewError(fiber.StatusNotFound, "Product Warehouse not found for the given Project Flock and Warehouse") } + // Jumlahkan semua quantity DOC + totalQuantity := 0.0 + for _, pw := range productWarehouses { + totalQuantity += pw.Quantity + } + + if totalQuantity < 1 { + return nil, fiber.NewError(fiber.StatusBadRequest, "Insufficient quantity in Product Warehouses") + } + + // Buat satu chickin dengan total quantity chickinDate, err := utils.ParseDateString(req.ChickInDate) if err != nil { s.Log.Errorf("Failed to parse chickin date: %+v", err) @@ -161,9 +167,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit newChickin := &entity.ProjectChickin{ ProjectFlockKandangId: projectflockkandang.ProjectFlockId, ChickInDate: chickinDate, - Quantity: productWarehouse.Quantity, + Quantity: totalQuantity, Note: "", - CreatedBy: 1, //todo: ganti dengan + CreatedBy: 1, //todo: ganti dengan user login } err = s.Repository.CreateOne(c.Context(), newChickin, nil) if err != nil { @@ -171,16 +177,15 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit return nil, err } - updatedQuantity := productWarehouse.Quantity - newChickin.Quantity - if updatedQuantity < 0 { - updatedQuantity = 0 - } - err = s.ProductWarehouseRepo.PatchOne(c.Context(), productWarehouse.Id, map[string]any{ - "quantity": updatedQuantity, - }, nil) - if err != nil { - s.Log.Errorf("Failed to update product warehouse quantity: %+v", err) - return nil, err + // 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 + } } existingPopulation, err := s.ProjectflockPopulationRepo.GetByProjectFlockKandangID(c.Context(), req.ProjectFlockKandangId) From b1b63d266a343aeb16094eef6020e7a228c1b480 Mon Sep 17 00:00:00 2001 From: aguhh18 Date: Tue, 21 Oct 2025 11:48:54 +0700 Subject: [PATCH 11/14] fix[BE]: menggunakan base dto dari dto utama entity ketimbang buat simple dto baru --- .../production/chickins/dto/chickin.dto.go | 178 +++++++----------- .../chickins/services/chickin.service.go | 6 +- 2 files changed, 73 insertions(+), 111 deletions(-) diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 9fd29f3c..96115b58 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -4,68 +4,42 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + areaBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" + fcrBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto" + flockBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" + kandangBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" + locationBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" + productCategoryBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto" + userBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) // === DTO Structs (ordered) === -type FlockDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - -type KandangDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - -type ProductCategoryDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - -type AreaDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - -type FcrDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - -type LocationDTO struct { - Id uint `json:"id"` - Name string `json:"name"` +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"` } type ProjectFlockDTO struct { - Id uint `json:"id"` - Period int `json:"period"` - Flock *FlockDTO `json:"flock"` - ProductCategory *ProductCategoryDTO `json:"product_category"` - Area *AreaDTO `json:"area"` - Fcr *FcrDTO `json:"fcr"` - Location *LocationDTO `json:"location"` + Id uint `json:"id"` + Period int `json:"period"` + Flock *flockBaseDTO.FlockBaseDTO `json:"flock"` + ProductCategory *productCategoryBaseDTO.ProductCategoryBaseDTO `json:"product_category"` + Area *areaBaseDTO.AreaBaseDTO `json:"area"` + Fcr *fcrBaseDTO.FcrBaseDTO `json:"fcr"` + Location *locationBaseDTO.LocationBaseDTO `json:"location"` } type ProjectFlockKandangDTO struct { - Id uint `json:"id"` - ProjectFlock *ProjectFlockDTO `json:"project_flock"` - Kandang *KandangDTO `json:"kandang"` + Id uint `json:"id"` + ProjectFlock *ProjectFlockDTO `json:"project_flock"` + Kandang *kandangBaseDTO.KandangBaseDTO `json:"kandang"` } -type UserBaseDTO struct { - Id uint `json:"id"` - Name string `json:"name"` -} - -type ChickinBaseDTO struct { - Id uint `json:"id"` - ProjectFlocKandangId uint `json:"project_floc_kandang_id"` - ChickInDate time.Time `json:"chick_in_date"` - Quantity float64 `json:"quantity"` - Note string `json:"note"` -} +// gunakan base DTO dari package users type ChickinSimpleDTO struct { Id uint `json:"id"` @@ -78,10 +52,10 @@ type ChickinSimpleDTO struct { type ChickinListDTO struct { ChickinBaseDTO - ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"` - CreatedUser *UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ProjectFlockKandang *ProjectFlockKandangDTO `json:"project_flock_kandang"` + CreatedUser *userBaseDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type ChickinDetailDTO struct { @@ -90,72 +64,58 @@ type ChickinDetailDTO struct { // === Mapper Functions (ordered) === -func ToFlockDTO(e entity.Flock) FlockDTO { - return FlockDTO{ - Id: e.Id, - Name: e.Name, - } +func ToFlockDTO(e entity.Flock) flockBaseDTO.FlockBaseDTO { + return flockBaseDTO.ToFlockBaseDTO(e) } -func ToKandangDTO(e entity.Kandang) KandangDTO { - return KandangDTO{ - Id: e.Id, - Name: e.Name, - } +func ToKandangDTO(e entity.Kandang) kandangBaseDTO.KandangBaseDTO { + return kandangBaseDTO.ToKandangBaseDTO(e) } -func ToProductCategoryDTO(e entity.ProductCategory) ProductCategoryDTO { - return ProductCategoryDTO{ - Id: e.Id, - Name: e.Name, - } +func ToProductCategoryDTO(e entity.ProductCategory) productCategoryBaseDTO.ProductCategoryBaseDTO { + return productCategoryBaseDTO.ToProductCategoryBaseDTO(e) } -func ToAreaDTO(e entity.Area) AreaDTO { - return AreaDTO{ - Id: e.Id, - Name: e.Name, - } +func ToAreaDTO(e entity.Area) areaBaseDTO.AreaBaseDTO { + return areaBaseDTO.ToAreaBaseDTO(e) } -func ToFcrDTO(e entity.Fcr) FcrDTO { - return FcrDTO{ - Id: e.Id, - Name: e.Name, - } +func ToFcrDTO(e entity.Fcr) fcrBaseDTO.FcrBaseDTO { + return fcrBaseDTO.ToFcrBaseDTO(e) } -func ToLocationDTO(e entity.Location) LocationDTO { - return LocationDTO{ - Id: e.Id, - Name: e.Name, - } +func ToLocationDTO(e entity.Location) locationBaseDTO.LocationBaseDTO { + return locationBaseDTO.ToLocationBaseDTO(e) +} + +func ToUserBaseDTO(e entity.User) userBaseDTO.UserBaseDTO { + return userBaseDTO.ToUserBaseDTO(e) } func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { - var flock *FlockDTO + var flock *flockBaseDTO.FlockBaseDTO if e.Flock.Id != 0 { - mapped := ToFlockDTO(e.Flock) + mapped := flockBaseDTO.ToFlockBaseDTO(e.Flock) flock = &mapped } - var productCategory *ProductCategoryDTO + var productCategory *productCategoryBaseDTO.ProductCategoryBaseDTO if e.ProductCategory.Id != 0 { - mapped := ToProductCategoryDTO(e.ProductCategory) + mapped := productCategoryBaseDTO.ToProductCategoryBaseDTO(e.ProductCategory) productCategory = &mapped } - var area *AreaDTO + var area *areaBaseDTO.AreaBaseDTO if e.Area.Id != 0 { - mapped := ToAreaDTO(e.Area) + mapped := areaBaseDTO.ToAreaBaseDTO(e.Area) area = &mapped } - var fcr *FcrDTO + var fcr *fcrBaseDTO.FcrBaseDTO if e.Fcr.Id != 0 { - mapped := ToFcrDTO(e.Fcr) + mapped := fcrBaseDTO.ToFcrBaseDTO(e.Fcr) fcr = &mapped } - var location *LocationDTO + var location *locationBaseDTO.LocationBaseDTO if e.Location.Id != 0 { - mapped := ToLocationDTO(e.Location) + mapped := locationBaseDTO.ToLocationBaseDTO(e.Location) location = &mapped } return ProjectFlockDTO{ @@ -175,9 +135,9 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD mapped := ToProjectFlockDTO(e.ProjectFlock) pf = &mapped } - var kandang *KandangDTO + var kandang *kandangBaseDTO.KandangBaseDTO if e.Kandang.Id != 0 { - mapped := ToKandangDTO(e.Kandang) + mapped := kandangBaseDTO.ToKandangBaseDTO(e.Kandang) kandang = &mapped } return ProjectFlockKandangDTO{ @@ -187,20 +147,18 @@ func ToProjectFlockKandangDTO(e entity.ProjectFlockKandang) ProjectFlockKandangD } } -func ToUserBaseDTO(e entity.User) UserBaseDTO { - return UserBaseDTO{ - Id: e.Id, - Name: e.Name, - } -} - func ToChickinBaseDTO(e entity.ProjectChickin) ChickinBaseDTO { + var pfk *ProjectFlockKandangDTO + if e.ProjectFlockKandang.Id != 0 { + mapped := ToProjectFlockKandangDTO(e.ProjectFlockKandang) + pfk = &mapped + } return ChickinBaseDTO{ - Id: e.Id, - ProjectFlocKandangId: e.ProjectFlockKandangId, - ChickInDate: e.ChickInDate, - Quantity: e.Quantity, - Note: e.Note, + Id: e.Id, + ProjectFlockKandang: pfk, + ChickInDate: e.ChickInDate, + Quantity: e.Quantity, + Note: e.Note, } } @@ -216,9 +174,9 @@ func ToChickinSimpleDTO(e entity.ProjectChickin) ChickinSimpleDTO { } func ToChickinListDTO(e entity.ProjectChickin) ChickinListDTO { - var createdUser *UserBaseDTO + var createdUser *userBaseDTO.UserBaseDTO if e.CreatedUser.Id != 0 { - mapped := ToUserBaseDTO(e.CreatedUser) + mapped := userBaseDTO.ToUserBaseDTO(e.CreatedUser) createdUser = &mapped } var pfk *ProjectFlockKandangDTO diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 64fe1e97..46bc8069 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -62,12 +62,16 @@ func (s chickinService) withRelations(db *gorm.DB) *gorm.DB { return db. Preload("CreatedUser"). Preload("ProjectFlockKandang.Kandang"). + Preload("ProjectFlockKandang.Kandang.Location"). + Preload("ProjectFlockKandang.Kandang.Location.Area"). + Preload("ProjectFlockKandang.Kandang.Pic"). Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock.Flock"). Preload("ProjectFlockKandang.ProjectFlock.ProductCategory"). Preload("ProjectFlockKandang.ProjectFlock.Area"). Preload("ProjectFlockKandang.ProjectFlock.Fcr"). - Preload("ProjectFlockKandang.ProjectFlock.Location") + Preload("ProjectFlockKandang.ProjectFlock.Location"). + Preload("ProjectFlockKandang.ProjectFlock.Location.Area") } From 55b14f5fc7ec0e0a7f165a35f7eac4411bf075ef Mon Sep 17 00:00:00 2001 From: "Hafizh A. Y" Date: Tue, 21 Oct 2025 13:56:30 +0700 Subject: [PATCH 12/14] feat(BE): approval_workflow, adjusment project_flocks, common, and migration --- .../repository/common.approval.repository..go | 106 +++++++ ...epository.go => common.base.repository.go} | 0 ...helpers.go => common.exists.repository.go} | 0 .../common/service/common.approval.service.go | 234 ++++++++++++++++ ...relation.go => common.relation.service.go} | 0 ...idation.go => common.custom.validation.go} | 0 ...1015162158_create_approvals_table.down.sql | 2 + ...251015162158_create_approvals_table.up.sql | 12 + ..._rename_approval_status_to_action.down.sql | 18 ++ ...55_rename_approval_status_to_action.up.sql | 14 + internal/database/seed/seeder.go | 57 ++++ internal/entities/approval.go | 28 ++ internal/entities/projectfloc.go | 28 -- internal/entities/projectflock.go | 29 ++ .../controllers/approval.controller.go | 100 +++++++ .../modules/approvals/dto/approval.dto.go | 122 ++++++++ internal/modules/approvals/module.go | 25 ++ internal/modules/approvals/route.go | 19 ++ .../validations/approval.validation.go | 10 + .../repositories/constant.repository.go | 49 ++++ .../controllers/projectflock.controller.go | 27 ++ .../project_flocks/dto/projectflock.dto.go | 107 ++++--- .../production/project_flocks/module.go | 13 +- .../production/project_flocks/route.go | 1 + .../services/projectflock.service.go | 261 ++++++++++++------ .../validations/projectflock.validation.go | 5 + internal/route/route.go | 2 + .../utils/approvals/util.approval_workflow.go | 243 ++++++++++++++++ internal/utils/constant.go | 22 +- tools/templates/validation.tmpl | 4 +- 30 files changed, 1379 insertions(+), 159 deletions(-) create mode 100644 internal/common/repository/common.approval.repository..go rename internal/common/repository/{repository.go => common.base.repository.go} (100%) rename internal/common/repository/{helpers.go => common.exists.repository.go} (100%) create mode 100644 internal/common/service/common.approval.service.go rename internal/common/service/{relation.go => common.relation.service.go} (100%) rename internal/common/validation/{custom_validation.go => common.custom.validation.go} (100%) create mode 100644 internal/database/migrations/20251015162158_create_approvals_table.down.sql create mode 100644 internal/database/migrations/20251015162158_create_approvals_table.up.sql create mode 100644 internal/database/migrations/20251017071755_rename_approval_status_to_action.down.sql create mode 100644 internal/database/migrations/20251017071755_rename_approval_status_to_action.up.sql create mode 100644 internal/entities/approval.go delete mode 100644 internal/entities/projectfloc.go create mode 100644 internal/entities/projectflock.go create mode 100644 internal/modules/approvals/controllers/approval.controller.go create mode 100644 internal/modules/approvals/dto/approval.dto.go create mode 100644 internal/modules/approvals/module.go create mode 100644 internal/modules/approvals/route.go create mode 100644 internal/modules/approvals/validations/approval.validation.go create mode 100644 internal/utils/approvals/util.approval_workflow.go diff --git a/internal/common/repository/common.approval.repository..go b/internal/common/repository/common.approval.repository..go new file mode 100644 index 00000000..7f1c27ae --- /dev/null +++ b/internal/common/repository/common.approval.repository..go @@ -0,0 +1,106 @@ +package repository + +import ( + "context" + "errors" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + "gorm.io/gorm" +) + +type ApprovalRepository interface { + BaseRepository[entity.Approval] + FindByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error) + LatestByTarget(ctx context.Context, workflow string, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error) + LatestByTargets(ctx context.Context, workflow string, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]entity.Approval, error) +} + +type approvalRepositoryImpl struct { + *BaseRepositoryImpl[entity.Approval] +} + +func NewApprovalRepository(db *gorm.DB) ApprovalRepository { + return &approvalRepositoryImpl{ + BaseRepositoryImpl: NewBaseRepository[entity.Approval](db), + } +} + +func (r *approvalRepositoryImpl) FindByTarget( + ctx context.Context, + workflow string, + approvableID uint, + modifier func(*gorm.DB) *gorm.DB, +) ([]entity.Approval, error) { + var approvals []entity.Approval + + q := r.DB().WithContext(ctx).Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID) + if modifier != nil { + q = modifier(q) + } + + if err := q.Order("action_at ASC").Find(&approvals).Error; err != nil { + return nil, err + } + return approvals, nil +} + +func (r *approvalRepositoryImpl) LatestByTarget( + ctx context.Context, + workflow string, + approvableID uint, + modifier func(*gorm.DB) *gorm.DB, +) (*entity.Approval, error) { + var approval entity.Approval + + q := r.DB().WithContext(ctx). + Where("approvable_type = ? AND approvable_id = ?", workflow, approvableID). + Order("action_at DESC") + + if modifier != nil { + q = modifier(q) + } + + if err := q.Limit(1).First(&approval).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, nil + } + return nil, err + } + + return &approval, nil +} + +func (r *approvalRepositoryImpl) LatestByTargets( + ctx context.Context, + workflow string, + approvableIDs []uint, + modifier func(*gorm.DB) *gorm.DB, +) (map[uint]entity.Approval, error) { + if len(approvableIDs) == 0 { + return nil, nil + } + + result := make(map[uint]entity.Approval, len(approvableIDs)) + + q := r.DB().WithContext(ctx). + Where("approvable_type = ? AND approvable_id IN ?", workflow, approvableIDs). + Order("action_at DESC") + + if modifier != nil { + q = modifier(q) + } + + var approvals []entity.Approval + if err := q.Find(&approvals).Error; err != nil { + return nil, err + } + + for _, approval := range approvals { + if _, exists := result[approval.ApprovableId]; exists { + continue + } + result[approval.ApprovableId] = approval + } + + return result, nil +} diff --git a/internal/common/repository/repository.go b/internal/common/repository/common.base.repository.go similarity index 100% rename from internal/common/repository/repository.go rename to internal/common/repository/common.base.repository.go diff --git a/internal/common/repository/helpers.go b/internal/common/repository/common.exists.repository.go similarity index 100% rename from internal/common/repository/helpers.go rename to internal/common/repository/common.exists.repository.go diff --git a/internal/common/service/common.approval.service.go b/internal/common/service/common.approval.service.go new file mode 100644 index 00000000..569a7cc6 --- /dev/null +++ b/internal/common/service/common.approval.service.go @@ -0,0 +1,234 @@ +package service + +import ( + "context" + "strings" + + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" + "gorm.io/gorm" +) + +type ApprovalService interface { + RegisterWorkflowSteps(workflow approvalutils.ApprovalWorkflowKey, steps map[approvalutils.ApprovalStep]string) error + WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string + WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool) + CreateApproval(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, step approvalutils.ApprovalStep, action *entity.ApprovalAction, actorID uint, note *string) (*entity.Approval, error) + List(ctx context.Context, module string, approvableID *uint, page, limit int, search string) ([]entity.Approval, int64, error) + ListByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error) + LatestByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error) + LatestByTargets(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]*entity.Approval, error) +} + +type approvalService struct { + repo commonRepo.ApprovalRepository +} + +func NewApprovalService(repo commonRepo.ApprovalRepository) ApprovalService { + return &approvalService{repo: repo} +} + +func (s *approvalService) RegisterWorkflowSteps(workflow approvalutils.ApprovalWorkflowKey, steps map[approvalutils.ApprovalStep]string) error { + return approvalutils.RegisterWorkflowSteps(workflow, steps) +} + +func (s *approvalService) WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string { + return approvalutils.WorkflowSteps(workflow) +} + +func (s *approvalService) WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool) { + return approvalutils.ApprovalStepName(workflow, step) +} + +func (s *approvalService) CreateApproval( + ctx context.Context, + workflow approvalutils.ApprovalWorkflowKey, + approvableID uint, + step approvalutils.ApprovalStep, + action *entity.ApprovalAction, + actorID uint, + note *string, +) (*entity.Approval, error) { + record, err := approvalutils.NewApproval(workflow, approvableID, step, action, actorID, note) + if err != nil { + return nil, err + } + + if err := s.repo.CreateOne(ctx, record, nil); err != nil { + return nil, err + } + + s.decorateApproval(workflow, record) + + return record, nil +} + +func (s *approvalService) List( + ctx context.Context, + module string, + approvableID *uint, + page, limit int, + search string, +) ([]entity.Approval, int64, error) { + module = strings.TrimSpace(strings.ToUpper(module)) + search = strings.TrimSpace(search) + + if limit <= 0 { + limit = 10 + } + if page <= 0 { + page = 1 + } + + offset := (page - 1) * limit + + records, total, err := s.repo.GetAll( + ctx, + offset, + limit, + func(db *gorm.DB) *gorm.DB { + query := db. + Where("approvable_type = ?", module). + Order("action_at DESC"). + Preload("ActionUser") + + if approvableID != nil { + query = query.Where("approvable_id = ?", *approvableID) + } + + if search != "" { + like := "%" + strings.ToLower(search) + "%" + query = query.Where("(LOWER(step_name) LIKE ? OR LOWER(action) LIKE ? OR LOWER(notes) LIKE ?)", like, like, like) + } + + return query + }, + ) + if err != nil { + if s.isApprovalTableMissing(err) { + return nil, 0, nil + } + return nil, 0, err + } + if len(records) == 0 { + return nil, total, nil + } + + workflow := approvalutils.ApprovalWorkflowKey(module) + for i := range records { + s.decorateApproval(workflow, &records[i]) + } + + return records, total, nil +} + +func (s *approvalService) ListByTarget( + ctx context.Context, + workflow approvalutils.ApprovalWorkflowKey, + approvableID uint, + modifier func(*gorm.DB) *gorm.DB, +) ([]entity.Approval, error) { + records, err := s.repo.FindByTarget(ctx, workflow.String(), approvableID, modifier) + if err != nil { + if s.isApprovalTableMissing(err) { + return nil, nil + } + return nil, err + } + + for i := range records { + s.decorateApproval(workflow, &records[i]) + } + + return records, nil +} + +func (s *approvalService) LatestByTarget( + ctx context.Context, + workflow approvalutils.ApprovalWorkflowKey, + approvableID uint, + modifier func(*gorm.DB) *gorm.DB, +) (*entity.Approval, error) { + record, err := s.repo.LatestByTarget(ctx, workflow.String(), approvableID, modifier) + if err != nil { + if s.isApprovalTableMissing(err) { + return nil, nil + } + return nil, err + } + if record == nil { + return nil, nil + } + s.decorateApproval(workflow, record) + return record, nil +} + +func (s *approvalService) LatestByTargets( + ctx context.Context, + workflow approvalutils.ApprovalWorkflowKey, + approvableIDs []uint, + modifier func(*gorm.DB) *gorm.DB, +) (map[uint]*entity.Approval, error) { + records, err := s.repo.LatestByTargets(ctx, workflow.String(), approvableIDs, modifier) + if err != nil { + if s.isApprovalTableMissing(err) { + return nil, nil + } + return nil, err + } + if len(records) == 0 { + return nil, nil + } + + result := make(map[uint]*entity.Approval, len(records)) + for approvableID, approval := range records { + approvalCopy := approval + s.decorateApproval(workflow, &approvalCopy) + result[approvableID] = &approvalCopy + } + + return result, nil +} + +func (s *approvalService) decorateApproval(workflow approvalutils.ApprovalWorkflowKey, approval *entity.Approval) { + if approval == nil { + return + } + currentName := strings.TrimSpace(approval.StepName) + if currentName == "" { + if name, ok := approvalutils.ApprovalStepName(workflow, approvalutils.ApprovalStep(approval.StepNumber)); ok { + approval.StepName = name + } + } else { + approval.StepName = currentName + } +} + +func (s *approvalService) isApprovalTableMissing(err error) bool { + if err == nil { + return false + } + + errMsg := strings.ToLower(err.Error()) + + if strings.Contains(errMsg, "no such table: approvals") { + return true + } + + schemaIssues := []string{ + `relation "approvals" does not exist`, + `column "step_name" does not exist`, + `column "step_number" does not exist`, + `column "action" does not exist`, + `column "status" does not exist`, + `column "step" does not exist`, + } + for _, issue := range schemaIssues { + if strings.Contains(errMsg, issue) { + return true + } + } + + return false +} diff --git a/internal/common/service/relation.go b/internal/common/service/common.relation.service.go similarity index 100% rename from internal/common/service/relation.go rename to internal/common/service/common.relation.service.go diff --git a/internal/common/validation/custom_validation.go b/internal/common/validation/common.custom.validation.go similarity index 100% rename from internal/common/validation/custom_validation.go rename to internal/common/validation/common.custom.validation.go diff --git a/internal/database/migrations/20251015162158_create_approvals_table.down.sql b/internal/database/migrations/20251015162158_create_approvals_table.down.sql new file mode 100644 index 00000000..0ad38d2b --- /dev/null +++ b/internal/database/migrations/20251015162158_create_approvals_table.down.sql @@ -0,0 +1,2 @@ +DROP INDEX IF EXISTS approvals_approvable_lookup; +DROP TABLE IF EXISTS approvals; diff --git a/internal/database/migrations/20251015162158_create_approvals_table.up.sql b/internal/database/migrations/20251015162158_create_approvals_table.up.sql new file mode 100644 index 00000000..50154f33 --- /dev/null +++ b/internal/database/migrations/20251015162158_create_approvals_table.up.sql @@ -0,0 +1,12 @@ +CREATE TABLE approvals ( + id BIGSERIAL PRIMARY KEY, + approvable_type VARCHAR(50) NOT NULL, + approvable_id BIGINT NOT NULL, + step SMALLINT NOT NULL, + status VARCHAR(20) NOT NULL, + notes TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL, + action_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE +); + +CREATE INDEX approvals_approvable_lookup ON approvals (approvable_type, approvable_id); diff --git a/internal/database/migrations/20251017071755_rename_approval_status_to_action.down.sql b/internal/database/migrations/20251017071755_rename_approval_status_to_action.down.sql new file mode 100644 index 00000000..cca2f08b --- /dev/null +++ b/internal/database/migrations/20251017071755_rename_approval_status_to_action.down.sql @@ -0,0 +1,18 @@ +ALTER TABLE approvals + RENAME COLUMN action TO status; + +UPDATE approvals +SET status = 'PENDING' +WHERE status IS NULL; + +ALTER TABLE approvals + ALTER COLUMN status SET NOT NULL; + +ALTER TABLE approvals + RENAME COLUMN step_number TO step; + +ALTER TABLE approvals + DROP COLUMN step_name; + +ALTER TABLE approvals + RENAME COLUMN action_at TO created_at; diff --git a/internal/database/migrations/20251017071755_rename_approval_status_to_action.up.sql b/internal/database/migrations/20251017071755_rename_approval_status_to_action.up.sql new file mode 100644 index 00000000..4d27cd27 --- /dev/null +++ b/internal/database/migrations/20251017071755_rename_approval_status_to_action.up.sql @@ -0,0 +1,14 @@ +ALTER TABLE approvals + RENAME COLUMN status TO action; + +ALTER TABLE approvals + ALTER COLUMN action DROP NOT NULL; + +ALTER TABLE approvals + RENAME COLUMN step TO step_number; + +ALTER TABLE approvals + ADD COLUMN step_name VARCHAR NOT NULL; + +ALTER TABLE approvals + RENAME COLUMN created_at TO action_at; diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index e3a0b8bc..7bebf4f3 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -8,6 +8,7 @@ import ( entity "gitlab.com/mbugroup/lti-api.git/internal/entities" "gitlab.com/mbugroup/lti-api.git/internal/utils" + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" "gorm.io/gorm" ) @@ -322,12 +323,68 @@ func seedProjectFlocks(tx *gorm.DB, createdBy uint, flocks, areas, fcrs, locatio return nil, err } } + + if err := ensureProjectFlockApprovals(tx, projectFlock.Id, createdBy); err != nil { + return nil, err + } result[seed.Key] = projectFlock.Id } return result, nil } +func ensureProjectFlockApprovals(tx *gorm.DB, projectFlockID uint, actorID uint) error { + if projectFlockID == 0 || actorID == 0 { + return nil + } + + workflow := utils.ApprovalWorkflowProjectFlock.String() + + steps := []struct { + step approvalutils.ApprovalStep + action entity.ApprovalAction + }{ + {step: utils.ProjectFlockStepPengajuan, action: entity.ApprovalActionCreated}, + {step: utils.ProjectFlockStepAktif, action: entity.ApprovalActionApproved}, + } + + for _, cfg := range steps { + var count int64 + if err := tx.Model(&entity.Approval{}). + Where("approvable_type = ? AND approvable_id = ? AND step_number = ?", workflow, projectFlockID, uint16(cfg.step)). + Count(&count).Error; err != nil { + return err + } + if count > 0 { + continue + } + + stepName, ok := utils.ProjectFlockApprovalSteps[cfg.step] + if !ok || strings.TrimSpace(stepName) == "" { + stepName = fmt.Sprintf("Step %d", cfg.step) + } + + var actionPtr *entity.ApprovalAction + action := cfg.action + actionPtr = &action + + record := entity.Approval{ + ApprovableType: workflow, + ApprovableId: projectFlockID, + StepNumber: uint16(cfg.step), + StepName: stepName, + Action: actionPtr, + ActionBy: uintPtr(actorID), + } + + if err := tx.Create(&record).Error; err != nil { + return err + } + } + + return nil +} + func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users map[string]uint, projectFlocks map[string]uint) (map[string]uint, error) { seeds := []struct { Name string diff --git a/internal/entities/approval.go b/internal/entities/approval.go new file mode 100644 index 00000000..87dc7b0a --- /dev/null +++ b/internal/entities/approval.go @@ -0,0 +1,28 @@ +package entities + +import ( + "time" +) + +type ApprovalAction string + +const ( + ApprovalActionApproved ApprovalAction = "APPROVED" + ApprovalActionRejected ApprovalAction = "REJECTED" + ApprovalActionCreated ApprovalAction = "CREATED" + ApprovalActionUpdated ApprovalAction = "UPDATED" +) + +type Approval struct { + Id uint `gorm:"primaryKey"` + ApprovableType string `gorm:"size:50;not null;index:approvals_approvable_lookup,priority:1"` + ApprovableId uint `gorm:"not null;index:approvals_approvable_lookup,priority:2"` + StepNumber uint16 `gorm:"not null"` + StepName string `gorm:"not null"` + Action *ApprovalAction `gorm:"type:VARCHAR(20)"` + Notes *string `gorm:"type:text"` + ActionAt time.Time `gorm:"autoCreateTime"` + ActionBy *uint `gorm:"index"` + + ActionUser *User `gorm:"foreignKey:ActionBy;references:Id"` +} diff --git a/internal/entities/projectfloc.go b/internal/entities/projectfloc.go deleted file mode 100644 index 47362d42..00000000 --- a/internal/entities/projectfloc.go +++ /dev/null @@ -1,28 +0,0 @@ -package entities - -import ( - "time" - - "gorm.io/gorm" -) - -type ProjectFlock struct { - Id uint `gorm:"primaryKey"` - FlockId uint `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"` - AreaId uint `gorm:"not null"` - Category string `gorm:"type:varchar(20);not null"` - FcrId uint `gorm:"not null"` - LocationId uint `gorm:"not null"` - Period int `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"` - CreatedBy uint `gorm:"not null"` - CreatedAt time.Time `gorm:"autoCreateTime"` - UpdatedAt time.Time `gorm:"autoUpdateTime"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Flock Flock `gorm:"foreignKey:FlockId;references:Id"` - Area Area `gorm:"foreignKey:AreaId;references:Id"` - Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"` - Location Location `gorm:"foreignKey:LocationId;references:Id"` - CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` - Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"` - KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"` -} diff --git a/internal/entities/projectflock.go b/internal/entities/projectflock.go new file mode 100644 index 00000000..e5c9ea82 --- /dev/null +++ b/internal/entities/projectflock.go @@ -0,0 +1,29 @@ +package entities + +import ( + "time" + + "gorm.io/gorm" +) + +type ProjectFlock struct { + Id uint `gorm:"primaryKey"` + FlockId uint `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"` + AreaId uint `gorm:"not null"` + Category string `gorm:"type:varchar(20);not null"` + FcrId uint `gorm:"not null"` + LocationId uint `gorm:"not null"` + Period int `gorm:"not null;uniqueIndex:project_flocks_flock_period_unique"` + CreatedBy uint `gorm:"not null"` + CreatedAt time.Time `gorm:"autoCreateTime"` + UpdatedAt time.Time `gorm:"autoUpdateTime"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Flock Flock `gorm:"foreignKey:FlockId;references:Id"` + Area Area `gorm:"foreignKey:AreaId;references:Id"` + Fcr Fcr `gorm:"foreignKey:FcrId;references:Id"` + Location Location `gorm:"foreignKey:LocationId;references:Id"` + CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` + Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"` + KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"` +} diff --git a/internal/modules/approvals/controllers/approval.controller.go b/internal/modules/approvals/controllers/approval.controller.go new file mode 100644 index 00000000..fd0baa6e --- /dev/null +++ b/internal/modules/approvals/controllers/approval.controller.go @@ -0,0 +1,100 @@ +package controller + +import ( + "math" + "strconv" + "strings" + + "github.com/gofiber/fiber/v2" + common "gitlab.com/mbugroup/lti-api.git/internal/common/service" + "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/validations" + "gitlab.com/mbugroup/lti-api.git/internal/response" +) + +type ApprovalController struct { + ApprovalService common.ApprovalService +} + +func NewApprovalController(approvalService common.ApprovalService) *ApprovalController { + return &ApprovalController{ + ApprovalService: approvalService, + } +} + +func (u *ApprovalController) GetAll(c *fiber.Ctx) error { + moduleName := strings.TrimSpace(c.Query("module_name", "")) + if moduleName == "" { + return fiber.NewError(fiber.StatusBadRequest, "`module_name` is required") + } + + moduleIDParam := strings.TrimSpace(c.Query("module_id", "")) + var moduleID *uint + if moduleIDParam != "" { + value, err := strconv.ParseUint(moduleIDParam, 10, 64) + if err != nil || value == 0 { + return fiber.NewError(fiber.StatusBadRequest, "module_id must be a positive integer") + } + id := uint(value) + moduleID = &id + } + + groupByStep := c.QueryBool("group_step_number", false) + + page := c.QueryInt("page", 1) + limit := c.QueryInt("limit", 10) + search := strings.TrimSpace(c.Query("search", "")) + + query := &validation.Query{ + ModuleName: moduleName, + ModuleId: moduleID, + GroupByStep: groupByStep, + Page: page, + Limit: limit, + Search: search, + } + + records, totalResults, err := u.ApprovalService.List( + c.Context(), + query.ModuleName, + query.ModuleId, + query.Page, + query.Limit, + query.Search, + ) + if err != nil { + return err + } + + if query.GroupByStep { + data := dto.ToApprovalGroupDTOs(records) + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.ApprovalGroupDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get All approvals successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: data, + }) + } + + flat := dto.ToApprovalDTOs(records) + return c.Status(fiber.StatusOK). + JSON(response.SuccessWithPaginate[dto.ApprovalBaseDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Get All approvals successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: flat, + }) +} diff --git a/internal/modules/approvals/dto/approval.dto.go b/internal/modules/approvals/dto/approval.dto.go new file mode 100644 index 00000000..085c367c --- /dev/null +++ b/internal/modules/approvals/dto/approval.dto.go @@ -0,0 +1,122 @@ +package dto + +import ( + "sort" + "strings" + "time" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" +) + +type ApprovalBaseDTO struct { + StepNumber uint16 `json:"step_number"` + StepName string `json:"step_name"` + Action *string `json:"action"` + Notes *string `json:"notes"` + ActionBy userDTO.UserBaseDTO `json:"action_by"` + ActionAt time.Time `json:"action_at"` +} + +type ApprovalGroupDTO struct { + StepNumber uint16 `json:"step_number"` + StepName string `json:"step_name"` + Approvals []ApprovalBaseDTO `json:"approvals"` +} + +func ToApprovalDTO(e entity.Approval) ApprovalBaseDTO { + dto := ApprovalBaseDTO{ + Notes: e.Notes, + } + + if e.StepNumber > 0 { + stepCopy := uint16(e.StepNumber) + dto.StepNumber = stepCopy + } + + stepName := strings.TrimSpace(e.StepName) + if stepName == "" && e.ApprovableType != "" && e.StepNumber > 0 { + if label, ok := approvalutils.ApprovalStepName(approvalutils.ApprovalWorkflowKey(e.ApprovableType), approvalutils.ApprovalStep(e.StepNumber)); ok { + stepName = label + } + } + dto.StepName = stepName + + if e.Action != nil { + value := strings.TrimSpace(string(*e.Action)) + if value != "" { + valueCopy := value + dto.Action = &valueCopy + } + } + + if e.ActionUser != nil && e.ActionUser.Id != 0 { + user := userDTO.ToUserBaseDTO(*e.ActionUser) + dto.ActionBy = user + } else if e.ActionBy != nil && *e.ActionBy != 0 { + dto.ActionBy = userDTO.UserBaseDTO{ + Id: *e.ActionBy, + IdUser: int64(*e.ActionBy), + } + } + + if !e.ActionAt.IsZero() { + at := e.ActionAt + dto.ActionAt = at + } + + return dto +} + +func ToApprovalDTOs(items []entity.Approval) []ApprovalBaseDTO { + result := make([]ApprovalBaseDTO, len(items)) + for i, item := range items { + result[i] = ToApprovalDTO(item) + } + return result +} + +func ToApprovalGroupDTOs(items []entity.Approval) []ApprovalGroupDTO { + if len(items) == 0 { + return nil + } + + type groupAccumulator struct { + StepName string + Approvals []ApprovalBaseDTO + } + + groups := make(map[uint16]*groupAccumulator) + order := make([]uint16, 0) + for _, item := range items { + step := item.StepNumber + acc, exists := groups[step] + if !exists { + stepName := strings.TrimSpace(item.StepName) + if stepName == "" && item.ApprovableType != "" && item.StepNumber > 0 { + if label, ok := approvalutils.ApprovalStepName(approvalutils.ApprovalWorkflowKey(item.ApprovableType), approvalutils.ApprovalStep(item.StepNumber)); ok { + stepName = label + } + } + acc = &groupAccumulator{StepName: stepName} + groups[step] = acc + order = append(order, step) + } + acc.Approvals = append(acc.Approvals, ToApprovalDTO(item)) + } + + sort.Slice(order, func(i, j int) bool { return order[i] < order[j] }) + + result := make([]ApprovalGroupDTO, len(order)) + for i, step := range order { + acc := groups[step] + result[i] = ApprovalGroupDTO{ + StepNumber: step, + StepName: acc.StepName, + Approvals: acc.Approvals, + } + } + + return result +} diff --git a/internal/modules/approvals/module.go b/internal/modules/approvals/module.go new file mode 100644 index 00000000..8cf52f73 --- /dev/null +++ b/internal/modules/approvals/module.go @@ -0,0 +1,25 @@ +package approvals + +import ( + "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" + + rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" + sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +type ApprovalModule struct{} + +func (ApprovalModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) { + approvalRepo := commonRepo.NewApprovalRepository(db) + userRepo := rUser.NewUserRepository(db) + + approvalService := commonSvc.NewApprovalService(approvalRepo) + userService := sUser.NewUserService(userRepo, validate) + + ApprovalRoutes(router, userService, approvalService) +} diff --git a/internal/modules/approvals/route.go b/internal/modules/approvals/route.go new file mode 100644 index 00000000..b7d66abd --- /dev/null +++ b/internal/modules/approvals/route.go @@ -0,0 +1,19 @@ +package approvals + +import ( + // m "gitlab.com/mbugroup/lti-api.git/internal/middleware" + "github.com/gofiber/fiber/v2" + + common "gitlab.com/mbugroup/lti-api.git/internal/common/service" + controller "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/controllers" + user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" +) + +func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalService) { + _ = u + ctrl := controller.NewApprovalController(s) + + route := v1.Group("/approvals") + + route.Get("/", ctrl.GetAll) +} diff --git a/internal/modules/approvals/validations/approval.validation.go b/internal/modules/approvals/validations/approval.validation.go new file mode 100644 index 00000000..7338550e --- /dev/null +++ b/internal/modules/approvals/validations/approval.validation.go @@ -0,0 +1,10 @@ +package validation + +type Query struct { + ModuleName string `json:"module_name" validate:"required_strict"` + ModuleId *uint `json:"module_id,omitempty" validate:"omitempty,gt=0"` + GroupByStep bool `json:"group_by_step"` + Page int `query:"page" validate:"omitempty,number,min=1"` + 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/modules/constants/repositories/constant.repository.go b/internal/modules/constants/repositories/constant.repository.go index 7b85ce20..4b44d553 100644 --- a/internal/modules/constants/repositories/constant.repository.go +++ b/internal/modules/constants/repositories/constant.repository.go @@ -1,9 +1,13 @@ package repository import ( + "sort" + "strconv" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" utils "gitlab.com/mbugroup/lti-api.git/internal/utils" + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" "gorm.io/gorm" ) @@ -26,6 +30,50 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} { for f := range utils.AllFlagTypes() { flagList = append(flagList, string(f)) } + sort.Strings(flagList) + + type approvalStepConstant struct { + StepNumber uint16 `json:"step_number"` + StepName string `json:"step_name"` + } + + workflowConstants := approvalutils.WorkflowConstants() + workflowKeys := make([]string, 0, len(workflowConstants)) + for key := range workflowConstants { + workflowKeys = append(workflowKeys, key) + } + sort.Strings(workflowKeys) + + approvalWorkflows := make([]map[string]interface{}, 0, len(workflowKeys)) + for _, key := range workflowKeys { + stepMap := workflowConstants[key] + if len(stepMap) == 0 { + continue + } + + stepList := make([]approvalStepConstant, 0, len(stepMap)) + for stepStr, label := range stepMap { + stepNum, err := strconv.ParseUint(stepStr, 10, 16) + if err != nil || stepNum == 0 { + continue + } + stepList = append(stepList, approvalStepConstant{ + StepNumber: uint16(stepNum), + StepName: label, + }) + } + if len(stepList) == 0 { + continue + } + sort.Slice(stepList, func(i, j int) bool { + return stepList[i].StepNumber < stepList[j].StepNumber + }) + + approvalWorkflows = append(approvalWorkflows, map[string]interface{}{ + "key": key, + "steps": stepList, + }) + } return map[string]interface{}{ "flags": flagList, @@ -42,5 +90,6 @@ func (r *ConstantRepositoryImpl) GetConstants() map[string]interface{} { "BISNIS", "INDIVIDUAL", }, + "approval_workflows": approvalWorkflows, } } diff --git a/internal/modules/production/project_flocks/controllers/projectflock.controller.go b/internal/modules/production/project_flocks/controllers/projectflock.controller.go index a1f2e263..31d0b9f0 100644 --- a/internal/modules/production/project_flocks/controllers/projectflock.controller.go +++ b/internal/modules/production/project_flocks/controllers/projectflock.controller.go @@ -190,6 +190,33 @@ func (u *ProjectflockController) DeleteOne(c *fiber.Ctx) error { }) } +func (u *ProjectflockController) Approval(c *fiber.Ctx) error { + param := c.Params("id") + + id, err := strconv.Atoi(param) + if err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid Id") + } + + req := new(validation.Approve) + if err := c.BodyParser(req); err != nil { + return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") + } + + result, err := u.ProjectflockService.Approval(c, uint(id), req) + if err != nil { + return err + } + + return c.Status(fiber.StatusOK). + JSON(response.Success{ + Code: fiber.StatusOK, + Status: "success", + Message: "Submit projectflock approval successfully", + Data: dto.ToProjectFlockListDTO(*result), + }) +} + func (u *ProjectflockController) GetFlockPeriodSummary(c *fiber.Ctx) error { param := c.Params("flock_id") diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index fcf3d50c..e58c13ac 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -4,12 +4,15 @@ import ( "time" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + approvalDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/dto" areaDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/dto" fcrDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/fcrs/dto" flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" locationDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" + "gitlab.com/mbugroup/lti-api.git/internal/utils" + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" ) type ProjectFlockBaseDTO struct { @@ -22,55 +25,25 @@ type ProjectFlockBaseDTO struct { Location *locationDTO.LocationBaseDTO `json:"location"` } -func ToProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO { - var flock *flockDTO.FlockBaseDTO - if e.Flock.Id != 0 { - mapped := flockDTO.ToFlockBaseDTO(e.Flock) - flock = &mapped - } - - var area *areaDTO.AreaBaseDTO - if e.Area.Id != 0 { - mapped := areaDTO.ToAreaBaseDTO(e.Area) - area = &mapped - } - - var fcr *fcrDTO.FcrBaseDTO - if e.Fcr.Id != 0 { - mapped := fcrDTO.ToFcrBaseDTO(e.Fcr) - fcr = &mapped - } - - var location *locationDTO.LocationBaseDTO - if e.Location.Id != 0 { - mapped := locationDTO.ToLocationBaseDTO(e.Location) - location = &mapped - } - - return ProjectFlockBaseDTO{ - Id: e.Id, - Period: e.Period, - Category: e.Category, - Flock: flock, - Area: area, - Fcr: fcr, - Location: location, - } -} - type ProjectFlockListDTO struct { ProjectFlockBaseDTO - Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"` - CreatedUser *userDTO.UserBaseDTO `json:"created_user"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + Flock *flockDTO.FlockBaseDTO `json:"flock,omitempty"` + Area *areaDTO.AreaBaseDTO `json:"area,omitempty"` + Category string `json:"category"` + Fcr *fcrDTO.FcrBaseDTO `json:"fcr,omitempty"` + Location *locationDTO.LocationBaseDTO `json:"location,omitempty"` + Kandangs []kandangDTO.KandangBaseDTO `json:"kandangs,omitempty"` + CreatedUser *userDTO.UserBaseDTO `json:"created_user"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + Approval approvalDTO.ApprovalBaseDTO `json:"approval"` } type ProjectFlockDetailDTO struct { ProjectFlockListDTO } -type FlockPeriodSummaryDTO struct { +type FlockPeriodDTO struct { Flock flockDTO.FlockBaseDTO `json:"flock"` NextPeriod int `json:"next_period"` } @@ -90,12 +63,19 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { } } + latestApproval := defaultProjectFlockLatestApproval(e) + if e.LatestApproval != nil { + snapshot := approvalDTO.ToApprovalDTO(*e.LatestApproval) + latestApproval = snapshot + } + return ProjectFlockListDTO{ ProjectFlockBaseDTO: ToProjectFlockBaseDTO(e), Kandangs: kandangSummaries, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, + Approval: latestApproval, } } @@ -113,9 +93,48 @@ func ToProjectFlockDetailDTO(e entity.ProjectFlock) ProjectFlockDetailDTO { } } -func ToFlockPeriodSummaryDTO(flock entity.Flock, next int) FlockPeriodSummaryDTO { - return FlockPeriodSummaryDTO{ - Flock: flockDTO.ToFlockBaseDTO(flock), +func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.ApprovalBaseDTO { + result := approvalDTO.ApprovalBaseDTO{} + + step := utils.ProjectFlockStepPengajuan + if step > 0 { + result.StepNumber = uint16(step) + if label, ok := approvalutils.ApprovalStepName(utils.ApprovalWorkflowProjectFlock, step); ok { + result.StepName = label + } else if label, ok := utils.ProjectFlockApprovalSteps[step]; ok { + result.StepName = label + } + } + if result.StepName == "" { + result.StepName = "Pengajuan" + } + + if !e.CreatedAt.IsZero() { + result.ActionAt = e.CreatedAt + } + + if e.CreatedUser.Id != 0 { + result.ActionBy = userDTO.ToUserBaseDTO(e.CreatedUser) + } else if e.CreatedBy != 0 { + result.ActionBy = userDTO.UserBaseDTO{ + Id: e.CreatedBy, + IdUser: int64(e.CreatedBy), + } + } + + return result +} + +func ToFlockSummaryDTO(e entity.Flock) flockDTO.FlockBaseDTO { + return flockDTO.FlockBaseDTO{ + Id: e.Id, + Name: e.Name, + } +} + +func ToFlockPeriodSummaryDTO(flock entity.Flock, next int) FlockPeriodDTO { + return FlockPeriodDTO{ + Flock: ToFlockSummaryDTO(flock), NextPeriod: next, } } diff --git a/internal/modules/production/project_flocks/module.go b/internal/modules/production/project_flocks/module.go index 5b91ab13..994eb4a4 100644 --- a/internal/modules/production/project_flocks/module.go +++ b/internal/modules/production/project_flocks/module.go @@ -1,8 +1,12 @@ package project_flocks import ( + "fmt" + "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" + commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" "gorm.io/gorm" rFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" @@ -10,6 +14,7 @@ import ( rProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" sProjectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" + utils "gitlab.com/mbugroup/lti-api.git/internal/utils" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" @@ -24,7 +29,13 @@ func (ProjectflockModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valid projectflockKandangRepo := rProjectflock.NewProjectFlockKandangRepository(db) userRepo := rUser.NewUserRepository(db) - projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, validate) + approvalRepo := commonRepo.NewApprovalRepository(db) + approvalService := commonSvc.NewApprovalService(approvalRepo) + if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlock, utils.ProjectFlockApprovalSteps); err != nil { + panic(fmt.Sprintf("failed to register project flock approval workflow: %v", err)) + } + + projectflockService := sProjectflock.NewProjectflockService(projectflockRepo, flockRepo, kandangRepo, projectflockKandangRepo, approvalService, validate) userService := sUser.NewUserService(userRepo, validate) ProjectflockRoutes(router, userService, projectflockService) diff --git a/internal/modules/production/project_flocks/route.go b/internal/modules/production/project_flocks/route.go index e5dbb48a..7282c020 100644 --- a/internal/modules/production/project_flocks/route.go +++ b/internal/modules/production/project_flocks/route.go @@ -25,5 +25,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj route.Get("/:id", ctrl.GetOne) route.Patch("/:id", ctrl.UpdateOne) route.Delete("/:id", ctrl.DeleteOne) + route.Post("/:id/approvals", ctrl.Approval) route.Get("/flocks/:flock_id/periods", ctrl.GetFlockPeriodSummary) } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 21941826..1a7526be 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -7,13 +7,14 @@ import ( "strings" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" - common "gitlab.com/mbugroup/lti-api.git/internal/common/service" + commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" kandangRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations" - "gitlab.com/mbugroup/lti-api.git/internal/utils" + utils "gitlab.com/mbugroup/lti-api.git/internal/utils" + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -28,15 +29,18 @@ type ProjectflockService interface { UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) DeleteOne(ctx *fiber.Ctx, id uint) error GetFlockPeriodSummary(ctx *fiber.Ctx, flockID uint) (*FlockPeriodSummary, error) + Approval(ctx *fiber.Ctx, id uint, req *validation.Approve) (*entity.ProjectFlock, error) } type projectflockService struct { - Log *logrus.Logger - Validate *validator.Validate - Repository repository.ProjectflockRepository - FlockRepo flockRepository.FlockRepository - KandangRepo kandangRepository.KandangRepository + Log *logrus.Logger + Validate *validator.Validate + Repository repository.ProjectflockRepository + FlockRepo flockRepository.FlockRepository + KandangRepo kandangRepository.KandangRepository PivotRepo repository.ProjectFlockKandangRepository + ApprovalSvc commonSvc.ApprovalService + approvalWorkflow approvalutils.ApprovalWorkflowKey } type FlockPeriodSummary struct { @@ -49,15 +53,18 @@ func NewProjectflockService( flockRepo flockRepository.FlockRepository, kandangRepo kandangRepository.KandangRepository, pivotRepo repository.ProjectFlockKandangRepository, + approvalSvc commonSvc.ApprovalService, validate *validator.Validate, ) ProjectflockService { return &projectflockService{ - Log: utils.Log, - Validate: validate, - Repository: repo, - FlockRepo: flockRepo, - KandangRepo: kandangRepo, + Log: utils.Log, + Validate: validate, + Repository: repo, + FlockRepo: flockRepo, + KandangRepo: kandangRepo, PivotRepo: pivotRepo, + ApprovalSvc: approvalSvc, + approvalWorkflow: utils.ApprovalWorkflowProjectFlock, } } @@ -68,7 +75,7 @@ func (s projectflockService) withRelations(db *gorm.DB) *gorm.DB { Preload("Area"). Preload("Fcr"). Preload("Location"). - Preload("Kandangs") + Preload("Kandangs.Location") } func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) { @@ -154,6 +161,27 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e s.Log.Errorf("Failed to get projectflocks: %+v", err) return nil, 0, err } + + if s.ApprovalSvc != nil && len(projectflocks) > 0 { + ids := make([]uint, len(projectflocks)) + for i, item := range projectflocks { + ids[i] = item.Id + } + + latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Unable to load latest approvals for projectflocks: %+v", err) + } else if len(latestMap) > 0 { + for i := range projectflocks { + if approval, ok := latestMap[projectflocks[i].Id]; ok { + projectflocks[i].LatestApproval = approval + } + } + } + } + return projectflocks, total, nil } @@ -166,6 +194,23 @@ func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock s.Log.Errorf("Failed get projectflock by id: %+v", err) return nil, err } + + if s.ApprovalSvc != nil { + approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, func(db *gorm.DB) *gorm.DB { + return db.Preload("ActionUser") + }) + if err != nil { + s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err) + } else if len(approvals) > 0 { + if projectflock.LatestApproval == nil { + latest := approvals[len(approvals)-1] + projectflock.LatestApproval = &latest + } + } else { + projectflock.LatestApproval = nil + } + } + return projectflock, nil } @@ -183,11 +228,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids is required") } - if err := common.EnsureRelations(c.Context(), - common.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())}, - common.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())}, - common.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())}, - common.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())}, + if err := commonSvc.EnsureRelations(c.Context(), + commonSvc.RelationCheck{Name: "Flock", ID: &req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB())}, + commonSvc.RelationCheck{Name: "Area", ID: &req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB())}, + commonSvc.RelationCheck{Name: "FCR", ID: &req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB())}, + commonSvc.RelationCheck{Name: "Location", ID: &req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB())}, ); err != nil { return nil, err } @@ -209,19 +254,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* } } - tx := s.Repository.DB().Begin() - if tx.Error != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") - } - - projectRepo := repository.NewProjectflockRepository(tx) - nextPeriod, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId) - if err != nil { - tx.Rollback() - s.Log.Errorf("Failed to determine next period for flock %d: %+v", req.FlockId, err) - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to determine next period") - } - createBody := &entity.ProjectFlock{ FlockId: req.FlockId, AreaId: req.AreaId, @@ -232,8 +264,60 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* CreatedBy: 1, } - if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil { - tx.Rollback() + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + projectRepo := repository.NewProjectflockRepository(tx) + // kandangRepo := kandangRepository.NewKandangRepository(tx) + + period, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId) + if err != nil { + return err + } + createBody.Period = period + + if err := projectRepo.CreateOne(c.Context(), createBody, nil); err != nil { + return err + } + + // kandangUpdates := make([]*entity.Kandang, len(kandangs)) + // for i := range kandangs { + // kandangs[i].ProjectFlockId = &createBody.Id + // kandangUpdates[i] = &kandangs[i] + // } + // if err := kandangRepo.UpdateMany( + // c.Context(), + // kandangUpdates, + // func(db *gorm.DB) *gorm.DB { + // return db.Select("project_flock_id") + // }, + // ); err != nil { + // return err + // } + + if err := tx.Model(&entity.Kandang{}). + Where("id IN ?", kandangIDs). + Updates(map[string]any{ + "project_flock_id": createBody.Id, + "status": string(utils.KandangStatusPengajuan), + }).Error; err != nil { + return err + } + + actorID := uint(1) //TODO: Change From Auth + action := entity.ApprovalActionCreated + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) + _, err = approvalSvcTx.CreateApproval( + c.Context(), + utils.ApprovalWorkflowProjectFlock, + createBody.Id, + utils.ProjectFlockStepPengajuan, + &action, + actorID, + nil, + ) + return err + }) + + if err != nil { if errors.Is(err, gorm.ErrDuplicatedKey) { return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists") } @@ -268,13 +352,14 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id s.Log.Errorf("Failed to fetch projectflock %d before update: %+v", id, err) return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } - updateBody := make(map[string]any) - var relationChecks []common.RelationCheck + hasBodyChanges := false + var relationChecks []commonSvc.RelationCheck if req.FlockId != nil { updateBody["flock_id"] = *req.FlockId - relationChecks = append(relationChecks, common.RelationCheck{ + hasBodyChanges = true + relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "Flock", ID: req.FlockId, Exists: relationExistsChecker[entity.Flock](s.Repository.DB()), @@ -282,7 +367,8 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } if req.AreaId != nil { updateBody["area_id"] = *req.AreaId - relationChecks = append(relationChecks, common.RelationCheck{ + hasBodyChanges = true + relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "Area", ID: req.AreaId, Exists: relationExistsChecker[entity.Area](s.Repository.DB()), @@ -297,7 +383,8 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } if req.FcrId != nil { updateBody["fcr_id"] = *req.FcrId - relationChecks = append(relationChecks, common.RelationCheck{ + hasBodyChanges = true + relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "FCR", ID: req.FcrId, Exists: relationExistsChecker[entity.Fcr](s.Repository.DB()), @@ -305,7 +392,8 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } if req.LocationId != nil { updateBody["location_id"] = *req.LocationId - relationChecks = append(relationChecks, common.RelationCheck{ + hasBodyChanges = true + relationChecks = append(relationChecks, commonSvc.RelationCheck{ Name: "Location", ID: req.LocationId, Exists: relationExistsChecker[entity.Location](s.Repository.DB()), @@ -313,16 +401,19 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } if req.Period != nil { updateBody["period"] = *req.Period + hasBodyChanges = true } if len(relationChecks) > 0 { - if err := common.EnsureRelations(c.Context(), relationChecks...); err != nil { + if err := commonSvc.EnsureRelations(c.Context(), relationChecks...); err != nil { return nil, err } } var newKandangIDs []uint + hasKandangChanges := false if req.KandangIds != nil { + hasKandangChanges = true if len(req.KandangIds) == 0 { return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids cannot be empty") } @@ -344,46 +435,47 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } } - tx := s.Repository.DB().Begin() - if tx.Error != nil { - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") + hasChanges := hasBodyChanges || hasKandangChanges + if !hasChanges { + return s.GetOne(c, id) } - projectRepo := repository.NewProjectflockRepository(tx) - if len(updateBody) > 0 { - if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil { - tx.Rollback() - if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { + projectRepo := repository.NewProjectflockRepository(tx) + + if len(updateBody) > 0 { + if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil { + return err } - s.Log.Errorf("Failed to update projectflock: %+v", err) - return nil, err - } - } - - if req.KandangIds != nil { - existingIDs := make(map[uint]struct{}, len(existing.Kandangs)) - for _, k := range existing.Kandangs { - existingIDs[k.Id] = struct{}{} - } - newSet := make(map[uint]struct{}, len(newKandangIDs)) - for _, id := range newKandangIDs { - newSet[id] = struct{}{} - } - - var toDetach []uint - for id := range existingIDs { - if _, ok := newSet[id]; !ok { - toDetach = append(toDetach, id) + } else { + if _, err := projectRepo.GetByID(c.Context(), id, nil); err != nil { + return err } } - var toAttach []uint - for id := range newSet { - if _, ok := existingIDs[id]; !ok { - toAttach = append(toAttach, id) + if req.KandangIds != nil { + existingIDs := make(map[uint]struct{}, len(existing.Kandangs)) + for _, k := range existing.Kandangs { + existingIDs[k.Id] = struct{}{} + } + newSet := make(map[uint]struct{}, len(newKandangIDs)) + for _, kid := range newKandangIDs { + newSet[kid] = struct{}{} + } + + var toDetach []uint + for kid := range existingIDs { + if _, ok := newSet[kid]; !ok { + toDetach = append(toDetach, kid) + } + } + + var toAttach []uint + for kid := range newSet { + if _, ok := existingIDs[kid]; !ok { + toAttach = append(toAttach, kid) + } } - } if len(toDetach) > 0 { if err := s.detachKandangs(c.Context(), tx, id, toDetach, false); err != nil { @@ -437,18 +529,21 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error { } } - if err := repository.NewProjectflockRepository(tx).DeleteOne(c.Context(), id); err != nil { - tx.Rollback() - if errors.Is(err, gorm.ErrRecordNotFound) { - return fiber.NewError(fiber.StatusNotFound, "Projectflock not found") + if err := repository.NewProjectflockRepository(tx).DeleteOne(c.Context(), id); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return fiber.NewError(fiber.StatusNotFound, "Projectflock not found") + } + return err } - s.Log.Errorf("Failed to delete projectflock: %+v", err) - return err - } - if err := tx.Commit().Error; err != nil { - tx.Rollback() - return fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction") + return nil + }) + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return fiberErr + } + s.Log.Errorf("Failed to delete projectflock %d: %+v", id, err) + return err } return nil diff --git a/internal/modules/production/project_flocks/validations/projectflock.validation.go b/internal/modules/production/project_flocks/validations/projectflock.validation.go index bbe957b6..d2ce7331 100644 --- a/internal/modules/production/project_flocks/validations/projectflock.validation.go +++ b/internal/modules/production/project_flocks/validations/projectflock.validation.go @@ -30,3 +30,8 @@ type Query struct { Period int `query:"period" validate:"omitempty,number,gt=0"` KandangIds []uint `query:"kandang_id" validate:"omitempty,dive,gt=0"` } + +type Approve struct { + Action string `json:"action" validate:"required_strict"` + Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"` +} diff --git a/internal/route/route.go b/internal/route/route.go index b1cd62a4..60f0fe26 100644 --- a/internal/route/route.go +++ b/internal/route/route.go @@ -13,6 +13,7 @@ import ( master "gitlab.com/mbugroup/lti-api.git/internal/modules/master" users "gitlab.com/mbugroup/lti-api.git/internal/modules/users" production "gitlab.com/mbugroup/lti-api.git/internal/modules/production" + approvals "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals" // MODULE IMPORTS ) @@ -28,6 +29,7 @@ func Routes(app *fiber.App, db *gorm.DB) { constants.ConstantModule{}, inventory.InventoryModule{}, production.ProductionModule{}, + approvals.ApprovalModule{}, // MODULE REGISTRY } diff --git a/internal/utils/approvals/util.approval_workflow.go b/internal/utils/approvals/util.approval_workflow.go new file mode 100644 index 00000000..78f1de8e --- /dev/null +++ b/internal/utils/approvals/util.approval_workflow.go @@ -0,0 +1,243 @@ +package approvals + +import ( + "errors" + "fmt" + "strings" + "sync" + + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" +) + +type ApprovalStep uint16 + +type ApprovalWorkflowKey string + +func (k ApprovalWorkflowKey) String() string { + return string(k) +} + +type NextStepCallback func(current ApprovalStep, decision entity.ApprovalAction) (ApprovalStep, bool) + +var ( + approvalActions = map[entity.ApprovalAction]struct{}{ + entity.ApprovalActionApproved: {}, + entity.ApprovalActionRejected: {}, + entity.ApprovalActionCreated: {}, + entity.ApprovalActionUpdated: {}, + } + + approvalWorkflows = make(map[ApprovalWorkflowKey]map[ApprovalStep]string) + approvalWorkflowsMu sync.RWMutex +) + +// WorkflowConstants prepares the registered workflows for exposure via constants endpoints. +func WorkflowConstants() map[string]map[string]string { + approvalWorkflowsMu.RLock() + defer approvalWorkflowsMu.RUnlock() + + if len(approvalWorkflows) == 0 { + return nil + } + + result := make(map[string]map[string]string, len(approvalWorkflows)) + for workflow, steps := range approvalWorkflows { + if len(steps) == 0 { + continue + } + stepMap := make(map[string]string, len(steps)) + for step, label := range steps { + stepMap[fmt.Sprintf("%d", step)] = label + } + result[workflow.String()] = stepMap + } + if len(result) == 0 { + return nil + } + return result +} + +// RegisterWorkflowSteps stores the available steps for a workflow key (usually matching approvable type). +func RegisterWorkflowSteps(workflow ApprovalWorkflowKey, steps map[ApprovalStep]string) error { + workflowStr := strings.TrimSpace(workflow.String()) + if workflowStr == "" { + return errors.New("workflow key is required") + } + if len(steps) == 0 { + return fmt.Errorf("no steps defined for workflow %q", workflowStr) + } + + copied := make(map[ApprovalStep]string, len(steps)) + for step, label := range steps { + if step == 0 { + return fmt.Errorf("workflow %q contains step 0 which is not allowed", workflowStr) + } + trimmed := strings.TrimSpace(label) + if trimmed == "" { + return fmt.Errorf("workflow %q contains empty label for step %d", workflowStr, step) + } + copied[step] = trimmed + } + + approvalWorkflowsMu.Lock() + defer approvalWorkflowsMu.Unlock() + approvalWorkflows[ApprovalWorkflowKey(workflowStr)] = copied + return nil +} + +// WorkflowSteps returns the steps registered for the given workflow key. +func WorkflowSteps(workflow ApprovalWorkflowKey) map[ApprovalStep]string { + approvalWorkflowsMu.RLock() + defer approvalWorkflowsMu.RUnlock() + + workflowStr := strings.TrimSpace(workflow.String()) + if workflowStr == "" { + return nil + } + + steps, ok := approvalWorkflows[ApprovalWorkflowKey(workflowStr)] + if !ok || len(steps) == 0 { + return nil + } + + copied := make(map[ApprovalStep]string, len(steps)) + for step, label := range steps { + copied[step] = label + } + return copied +} + +// ApprovalStepName fetches the label for the target step inside the workflow. +func ApprovalStepName(workflow ApprovalWorkflowKey, step ApprovalStep) (string, bool) { + steps := WorkflowSteps(workflow) + if len(steps) == 0 { + return "", false + } + label, ok := steps[step] + return label, ok +} + +// ValidateApprovalStep ensures the workflow contains the provided step. +func ValidateApprovalStep(workflow ApprovalWorkflowKey, step ApprovalStep) error { + if _, ok := ApprovalStepName(workflow, step); ok { + return nil + } + return fmt.Errorf("invalid approval step %d for workflow %s", step, workflow) +} + +// IsValidApprovalAction reports whether the action is supported. +func IsValidApprovalAction(action entity.ApprovalAction) bool { + _, ok := approvalActions[action] + return ok +} + +// NewApproval creates an approval record for the given approvable target. +func NewApproval(workflow ApprovalWorkflowKey, approvableId uint, step ApprovalStep, action *entity.ApprovalAction, actorId uint, note *string) (*entity.Approval, error) { + if approvableId == 0 { + return nil, errors.New("approvable id is required") + } + + workflowStr := strings.TrimSpace(workflow.String()) + if workflowStr == "" { + return nil, errors.New("approval workflow key is required") + } + + key := ApprovalWorkflowKey(workflowStr) + + if err := ValidateApprovalStep(key, step); err != nil { + return nil, err + } + + var actionPtr *entity.ApprovalAction + if action != nil { + if !IsValidApprovalAction(*action) { + return nil, fmt.Errorf("invalid approval action %q", *action) + } + actionCopy := *action + actionPtr = &actionCopy + } + + if actorId == 0 { + return nil, errors.New("actor id is required") + } + + var notes *string + if note != nil { + trimmed := strings.TrimSpace(*note) + if trimmed != "" { + notes = &trimmed + } + } + + actor := actorId + var stepName string + if label, ok := ApprovalStepName(key, step); ok { + labelCopy := label + stepName = labelCopy + } + + return &entity.Approval{ + ApprovableType: workflowStr, + ApprovableId: approvableId, + StepNumber: uint16(step), + StepName: stepName, + Action: actionPtr, + Notes: notes, + ActionBy: &actor, + }, nil +} + +// SetApprovalAction updates the approval action, notes, and optionally advances to another step. +func SetApprovalAction(approval *entity.Approval, action entity.ApprovalAction, actorId uint, note *string, nextStep NextStepCallback) error { + if approval == nil { + return errors.New("approval is nil") + } + if !IsValidApprovalAction(action) { + return fmt.Errorf("invalid approval action %q", action) + } + if actorId == 0 { + return errors.New("actor id is required for approval decision") + } + + act := action + approval.Action = &act + approval.ActionBy = &actorId + + if note != nil { + trimmed := strings.TrimSpace(*note) + if trimmed == "" { + approval.Notes = nil + } else { + approval.Notes = &trimmed + } + } else { + approval.Notes = nil + } + + if nextStep != nil { + current := ApprovalStep(approval.StepNumber) + if proposed, ok := nextStep(current, action); ok { + if err := ValidateApprovalStep(ApprovalWorkflowKey(approval.ApprovableType), proposed); err != nil { + return err + } + approval.StepNumber = uint16(proposed) + } + } + + if label, ok := ApprovalStepName(ApprovalWorkflowKey(approval.ApprovableType), ApprovalStep(approval.StepNumber)); ok { + labelCopy := label + approval.StepName = labelCopy + } + + return nil +} + +// Approve marks the approval as approved by the given actor, applying the optional step callback. +func Approve(approval *entity.Approval, actorId uint, note *string, nextStep NextStepCallback) error { + return SetApprovalAction(approval, entity.ApprovalActionApproved, actorId, note, nextStep) +} + +// Reject marks the approval as rejected by the given actor, applying the optional step callback. +func Reject(approval *entity.Approval, actorId uint, note *string, nextStep NextStepCallback) error { + return SetApprovalAction(approval, entity.ApprovalActionRejected, actorId, note, nextStep) +} diff --git a/internal/utils/constant.go b/internal/utils/constant.go index d780d2ae..5ab236b0 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -1,6 +1,10 @@ package utils -import "strings" +import ( + "strings" + + approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" +) // ------------------------------------------------------------------- // FlagType & Groups @@ -120,6 +124,22 @@ const ( ProjectFlockCategoryLaying ProjectFlockCategory = "LAYING" ) +// ------------------------------------------------------------------- +// Project Flock Approval +// ------------------------------------------------------------------- + +const ( + ApprovalWorkflowProjectFlock approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PROJECT_FLOCKS") + ProjectFlockStepPengajuan approvalutils.ApprovalStep = 1 + ProjectFlockStepAktif approvalutils.ApprovalStep = 2 +) + +// projectFlockApprovalSteps keeps the workflow step definitions for project flock approvals. +var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{ + ProjectFlockStepPengajuan: "Pengajuan", + ProjectFlockStepAktif: "Aktif", +} + // ------------------------------------------------------------------- // Validators // ------------------------------------------------------------------- diff --git a/tools/templates/validation.tmpl b/tools/templates/validation.tmpl index 3aa587eb..031b76c5 100644 --- a/tools/templates/validation.tmpl +++ b/tools/templates/validation.tmpl @@ -9,8 +9,8 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` + 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"` } {{end}} From e4799fa2ddb4256ae241ae836dda99cb53400d8e Mon Sep 17 00:00:00 2001 From: "Hafizh A. Y" Date: Tue, 21 Oct 2025 15:11:04 +0700 Subject: [PATCH 13/14] fix(BE): merge conflict --- internal/database/seed/seeder.go | 4 +- internal/entities/projectflock.go | 1 + .../repositories/kandang.repository.go | 8 + .../production/chickins/dto/chickin.dto.go | 39 ++- .../chickins/services/chickin.service.go | 9 +- .../project_flocks/dto/projectflock.dto.go | 38 ++- .../services/projectflock.service.go | 233 +++++++++++------- .../validations/projectflock.validation.go | 1 - 8 files changed, 216 insertions(+), 117 deletions(-) diff --git a/internal/database/seed/seeder.go b/internal/database/seed/seeder.go index 7bebf4f3..32c3b310 100644 --- a/internal/database/seed/seeder.go +++ b/internal/database/seed/seeder.go @@ -394,9 +394,9 @@ func seedKandangs(tx *gorm.DB, createdBy uint, locations map[string]uint, users ProjectFlockKey *string }{ {Name: "Singaparna 1", Status: utils.KandangStatusActive, Location: "Singaparna", PicKey: "admin", ProjectFlockKey: strPtr("Singaparna Period 1")}, - {Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin", ProjectFlockKey: strPtr("Singaparna Period 1")}, + {Name: "Singaparna 2", Status: utils.KandangStatusNonActive, Location: "Singaparna", PicKey: "admin"}, {Name: "Cikaum 1", Status: utils.KandangStatusActive, Location: "Cikaum", PicKey: "admin", ProjectFlockKey: strPtr("Cikaum Period 1")}, - {Name: "Cikaum 2", Status: utils.KandangStatusPengajuan, Location: "Cikaum", PicKey: "admin"}, + {Name: "Cikaum 2", Status: utils.KandangStatusNonActive, Location: "Cikaum", PicKey: "admin"}, } result := make(map[string]uint, len(seeds)) diff --git a/internal/entities/projectflock.go b/internal/entities/projectflock.go index e5c9ea82..c840892f 100644 --- a/internal/entities/projectflock.go +++ b/internal/entities/projectflock.go @@ -26,4 +26,5 @@ type ProjectFlock struct { CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` Kandangs []Kandang `gorm:"foreignKey:ProjectFlockId;references:Id"` KandangHistory []ProjectFlockKandang `gorm:"foreignKey:ProjectFlockId;references:Id"` + LatestApproval *Approval `gorm:"-" json:"-"` } diff --git a/internal/modules/master/kandangs/repositories/kandang.repository.go b/internal/modules/master/kandangs/repositories/kandang.repository.go index bcb03854..22546339 100644 --- a/internal/modules/master/kandangs/repositories/kandang.repository.go +++ b/internal/modules/master/kandangs/repositories/kandang.repository.go @@ -17,6 +17,7 @@ type KandangRepository interface { ProjectFlockExists(ctx context.Context, projectFlockID uint) (bool, error) GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.Kandang, error) HasActiveKandangForProjectFlock(ctx context.Context, projectFlockID uint, excludeID *uint) (bool, error) + UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error } type KandangRepositoryImpl struct { @@ -81,3 +82,10 @@ func (r *KandangRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, pr } return kandang, nil } + +func (r *KandangRepositoryImpl) UpdateStatusByProjectFlockID(ctx context.Context, projectFlockID uint, status utils.KandangStatus) error { + return r.db.WithContext(ctx). + Model(&entity.Kandang{}). + Where("project_flock_id = ?", projectFlockID). + Update("status", string(status)).Error +} diff --git a/internal/modules/production/chickins/dto/chickin.dto.go b/internal/modules/production/chickins/dto/chickin.dto.go index 96115b58..193257b6 100644 --- a/internal/modules/production/chickins/dto/chickin.dto.go +++ b/internal/modules/production/chickins/dto/chickin.dto.go @@ -9,7 +9,6 @@ import ( flockBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" kandangBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto" locationBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/dto" - productCategoryBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/product-categories/dto" userBaseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto" ) @@ -24,13 +23,13 @@ type ChickinBaseDTO struct { } type ProjectFlockDTO struct { - Id uint `json:"id"` - Period int `json:"period"` - Flock *flockBaseDTO.FlockBaseDTO `json:"flock"` - ProductCategory *productCategoryBaseDTO.ProductCategoryBaseDTO `json:"product_category"` - Area *areaBaseDTO.AreaBaseDTO `json:"area"` - Fcr *fcrBaseDTO.FcrBaseDTO `json:"fcr"` - Location *locationBaseDTO.LocationBaseDTO `json:"location"` + Id uint `json:"id"` + Period int `json:"period"` + Category string `json:"category"` + Flock *flockBaseDTO.FlockBaseDTO `json:"flock"` + Area *areaBaseDTO.AreaBaseDTO `json:"area"` + Fcr *fcrBaseDTO.FcrBaseDTO `json:"fcr"` + Location *locationBaseDTO.LocationBaseDTO `json:"location"` } type ProjectFlockKandangDTO struct { @@ -71,11 +70,6 @@ func ToFlockDTO(e entity.Flock) flockBaseDTO.FlockBaseDTO { func ToKandangDTO(e entity.Kandang) kandangBaseDTO.KandangBaseDTO { return kandangBaseDTO.ToKandangBaseDTO(e) } - -func ToProductCategoryDTO(e entity.ProductCategory) productCategoryBaseDTO.ProductCategoryBaseDTO { - return productCategoryBaseDTO.ToProductCategoryBaseDTO(e) -} - func ToAreaDTO(e entity.Area) areaBaseDTO.AreaBaseDTO { return areaBaseDTO.ToAreaBaseDTO(e) } @@ -98,11 +92,6 @@ func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { mapped := flockBaseDTO.ToFlockBaseDTO(e.Flock) flock = &mapped } - var productCategory *productCategoryBaseDTO.ProductCategoryBaseDTO - if e.ProductCategory.Id != 0 { - mapped := productCategoryBaseDTO.ToProductCategoryBaseDTO(e.ProductCategory) - productCategory = &mapped - } var area *areaBaseDTO.AreaBaseDTO if e.Area.Id != 0 { mapped := areaBaseDTO.ToAreaBaseDTO(e.Area) @@ -119,13 +108,13 @@ func ToProjectFlockDTO(e entity.ProjectFlock) ProjectFlockDTO { location = &mapped } return ProjectFlockDTO{ - Id: e.Id, - Period: e.Period, - Flock: flock, - ProductCategory: productCategory, - Area: area, - Fcr: fcr, - Location: location, + Id: e.Id, + Period: e.Period, + Category: e.Category, + Flock: flock, + Area: area, + Fcr: fcr, + Location: location, } } diff --git a/internal/modules/production/chickins/services/chickin.service.go b/internal/modules/production/chickins/services/chickin.service.go index 46bc8069..43105374 100644 --- a/internal/modules/production/chickins/services/chickin.service.go +++ b/internal/modules/production/chickins/services/chickin.service.go @@ -67,7 +67,6 @@ func (s chickinService) withRelations(db *gorm.DB) *gorm.DB { Preload("ProjectFlockKandang.Kandang.Pic"). Preload("ProjectFlockKandang.ProjectFlock"). Preload("ProjectFlockKandang.ProjectFlock.Flock"). - Preload("ProjectFlockKandang.ProjectFlock.ProductCategory"). Preload("ProjectFlockKandang.ProjectFlock.Area"). Preload("ProjectFlockKandang.ProjectFlock.Fcr"). Preload("ProjectFlockKandang.ProjectFlock.Location"). @@ -129,7 +128,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit c.Context(), projectflockkandang.ProjectFlockId, func(db *gorm.DB) *gorm.DB { - return db.Preload("ProductCategory") + return db }, ) if err != nil { @@ -141,7 +140,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit WithContext(c.Context()). Joins("JOIN products ON products.id = product_warehouses.product_id"). Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). - Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", projectFlock.ProductCategory.Code, warehouse.Id). + Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", projectFlock.Category, warehouse.Id). Order("created_at DESC"). Find(&productWarehouses).Error if err != nil { @@ -298,7 +297,7 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { c.Context(), projectflockkandang.ProjectFlockId, func(db *gorm.DB) *gorm.DB { - return db.Preload("ProductCategory") + return db }, ) @@ -310,7 +309,7 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error { err = s.ProductWarehouseRepo.DB().WithContext(c.Context()). Joins("JOIN products ON products.id = product_warehouses.product_id"). Joins("JOIN product_categories ON product_categories.id = products.product_category_id"). - Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", projectFlock.ProductCategory.Code, warehouse.Id). + Where("product_categories.code = ? AND product_warehouses.warehouse_id = ?", projectFlock.Category, warehouse.Id). Order("created_at DESC"). First(&productWarehouse).Error diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index e58c13ac..cb35eb0f 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -70,7 +70,7 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { } return ProjectFlockListDTO{ - ProjectFlockBaseDTO: ToProjectFlockBaseDTO(e), + ProjectFlockBaseDTO: createProjectFlockBaseDTO(e), Kandangs: kandangSummaries, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, @@ -125,6 +125,42 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv return result } +func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO { + var flock *flockDTO.FlockBaseDTO + if e.Flock.Id != 0 { + mapped := flockDTO.ToFlockBaseDTO(e.Flock) + flock = &mapped + } + + var area *areaDTO.AreaBaseDTO + if e.Area.Id != 0 { + mapped := areaDTO.ToAreaBaseDTO(e.Area) + area = &mapped + } + + var fcr *fcrDTO.FcrBaseDTO + if e.Fcr.Id != 0 { + mapped := fcrDTO.ToFcrBaseDTO(e.Fcr) + fcr = &mapped + } + + var location *locationDTO.LocationBaseDTO + if e.Location.Id != 0 { + mapped := locationDTO.ToLocationBaseDTO(e.Location) + location = &mapped + } + + return ProjectFlockBaseDTO{ + Id: e.Id, + Period: e.Period, + Category: e.Category, + Flock: flock, + Area: area, + Fcr: fcr, + Location: location, + } +} + func ToFlockSummaryDTO(e entity.Flock) flockDTO.FlockBaseDTO { return flockDTO.FlockBaseDTO{ Id: e.Id, diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 1a7526be..49401dd4 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -38,7 +38,7 @@ type projectflockService struct { Repository repository.ProjectflockRepository FlockRepo flockRepository.FlockRepository KandangRepo kandangRepository.KandangRepository - PivotRepo repository.ProjectFlockKandangRepository + PivotRepo repository.ProjectFlockKandangRepository ApprovalSvc commonSvc.ApprovalService approvalWorkflow approvalutils.ApprovalWorkflowKey } @@ -62,7 +62,7 @@ func NewProjectflockService( Repository: repo, FlockRepo: flockRepo, KandangRepo: kandangRepo, - PivotRepo: pivotRepo, + PivotRepo: pivotRepo, ApprovalSvc: approvalSvc, approvalWorkflow: utils.ApprovalWorkflowProjectFlock, } @@ -260,13 +260,11 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* Category: string(category), FcrId: req.FcrId, LocationId: req.LocationId, - Period: nextPeriod, CreatedBy: 1, } - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { - projectRepo := repository.NewProjectflockRepository(tx) - // kandangRepo := kandangRepository.NewKandangRepository(tx) + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + projectRepo := repository.NewProjectflockRepository(dbTransaction) period, err := projectRepo.GetNextPeriodForFlock(c.Context(), req.FlockId) if err != nil { @@ -278,33 +276,13 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* return err } - // kandangUpdates := make([]*entity.Kandang, len(kandangs)) - // for i := range kandangs { - // kandangs[i].ProjectFlockId = &createBody.Id - // kandangUpdates[i] = &kandangs[i] - // } - // if err := kandangRepo.UpdateMany( - // c.Context(), - // kandangUpdates, - // func(db *gorm.DB) *gorm.DB { - // return db.Select("project_flock_id") - // }, - // ); err != nil { - // return err - // } - - if err := tx.Model(&entity.Kandang{}). - Where("id IN ?", kandangIDs). - Updates(map[string]any{ - "project_flock_id": createBody.Id, - "status": string(utils.KandangStatusPengajuan), - }).Error; err != nil { + if err := s.attachKandangs(c.Context(), dbTransaction, createBody.Id, kandangIDs); err != nil { return err } actorID := uint(1) //TODO: Change From Auth action := entity.ApprovalActionCreated - approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) + approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) _, err = approvalSvcTx.CreateApproval( c.Context(), utils.ApprovalWorkflowProjectFlock, @@ -325,17 +303,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* return nil, err } - if err := s.attachKandangs(c.Context(), tx, createBody.Id, kandangIDs); err != nil { - tx.Rollback() - s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", createBody.Id, err) - return nil, err - } - - if err := tx.Commit().Error; err != nil { - tx.Rollback() - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction") - } - return s.GetOne(c, createBody.Id) } @@ -399,10 +366,6 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id Exists: relationExistsChecker[entity.Location](s.Repository.DB()), }) } - if req.Period != nil { - updateBody["period"] = *req.Period - hasBodyChanges = true - } if len(relationChecks) > 0 { if err := commonSvc.EnsureRelations(c.Context(), relationChecks...); err != nil { @@ -440,8 +403,8 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id return s.GetOne(c, id) } - err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { - projectRepo := repository.NewProjectflockRepository(tx) + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + projectRepo := repository.NewProjectflockRepository(dbTransaction) if len(updateBody) > 0 { if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil { @@ -477,26 +440,136 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id } } - if len(toDetach) > 0 { - if err := s.detachKandangs(c.Context(), tx, id, toDetach, false); err != nil { - tx.Rollback() - s.Log.Errorf("Failed to detach kandangs from projectflock %d: %+v", id, err) - return nil, err + if len(toDetach) > 0 { + if err := s.detachKandangs(c.Context(), dbTransaction, id, toDetach, true); err != nil { + return err + } + } + + if len(toAttach) > 0 { + if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach); err != nil { + return err + } } } - if len(toAttach) > 0 { - if err := s.attachKandangs(c.Context(), tx, id, toAttach); err != nil { - tx.Rollback() - s.Log.Errorf("Failed to attach kandangs to projectflock %d: %+v", id, err) - return nil, err + if hasChanges { + actorID := uint(1) //TODO: Change From Auth + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + if approvalSvc != nil { + latestBeforeReset, err := approvalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, nil) + if err != nil { + return err + } + shouldRecordUpdate := latestBeforeReset == nil || + latestBeforeReset.StepNumber != uint16(utils.ProjectFlockStepPengajuan) || + latestBeforeReset.Action == nil || + (latestBeforeReset.Action != nil && *latestBeforeReset.Action != entity.ApprovalActionUpdated) + + if shouldRecordUpdate { + action := entity.ApprovalActionUpdated + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowProjectFlock, + id, + utils.ProjectFlockStepPengajuan, + &action, + actorID, + nil, + ); err != nil { + return err + } + } } } + + return nil + }) + + if err != nil { + if fiberErr, ok := err.(*fiber.Error); ok { + return nil, fiberErr + } + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") + } + s.Log.Errorf("Failed to update projectflock %d: %+v", id, err) + return nil, err } - if err := tx.Commit().Error; err != nil { - tx.Rollback() - return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to commit transaction") + return s.GetOne(c, id) +} + +func (s projectflockService) Approval(c *fiber.Ctx, id uint, req *validation.Approve) (*entity.ProjectFlock, error) { + if err := s.Validate.Struct(req); err != nil { + return nil, err + } + + project, err := s.GetOne(c, id) + if err != nil { + s.Log.Errorf("Failed to fetch projectflock %d before approval: %+v", id, err) + return nil, err + } + + actorID := uint(1) // TODO: change from auth context + var action entity.ApprovalAction + switch strings.ToUpper(strings.TrimSpace(req.Action)) { + 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") + } + + step := utils.ProjectFlockStepPengajuan + if action == entity.ApprovalActionApproved { + step = utils.ProjectFlockStepAktif + } + + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) + if _, err := approvalSvc.CreateApproval( + c.Context(), + utils.ApprovalWorkflowProjectFlock, + project.Id, + step, + &action, + actorID, + req.Notes, + ); err != nil { + return err + } + + kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction) + switch action { + case entity.ApprovalActionApproved: + if err := kandangRepoTx.UpdateStatusByProjectFlockID( + c.Context(), + project.Id, + utils.KandangStatusActive, + ); err != nil { + return err + } + case entity.ApprovalActionRejected: + if err := kandangRepoTx.UpdateStatusByProjectFlockID( + c.Context(), + project.Id, + utils.KandangStatusNonActive, + ); err != nil { + return err + } + } + + return nil + }) + + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found") + } + s.Log.Errorf("Failed to record approval for projectflock %d: %+v", id, err) + return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval") } return s.GetOne(c, id) @@ -512,24 +585,18 @@ func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error { return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") } - tx := s.Repository.DB().Begin() - if tx.Error != nil { - return fiber.NewError(fiber.StatusInternalServerError, "Failed to start transaction") - } - - if len(existing.Kandangs) > 0 { - ids := make([]uint, len(existing.Kandangs)) - for i, k := range existing.Kandangs { - ids[i] = k.Id + err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { + if len(existing.Kandangs) > 0 { + ids := make([]uint, len(existing.Kandangs)) + for i, k := range existing.Kandangs { + ids[i] = k.Id + } + if err := s.detachKandangs(c.Context(), dbTransaction, id, ids, true); err != nil { + return err + } } - if err := s.detachKandangs(c.Context(), tx, id, ids, true); err != nil { - tx.Rollback() - s.Log.Errorf("Failed to detach kandangs before deleting projectflock %d: %+v", id, err) - return err - } - } - if err := repository.NewProjectflockRepository(tx).DeleteOne(c.Context(), id); err != nil { + if err := repository.NewProjectflockRepository(dbTransaction).DeleteOne(c.Context(), id); err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return fiber.NewError(fiber.StatusNotFound, "Projectflock not found") } @@ -627,12 +694,12 @@ func (s projectflockService) buildOrderExpressions(sortBy, sortOrder string) []s } } -func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, projectFlockID uint, kandangIDs []uint) error { +func (s projectflockService) attachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint) error { if len(kandangIDs) == 0 { return nil } - if err := tx.Model(&entity.Kandang{}). + if err := dbTransaction.Model(&entity.Kandang{}). Where("id IN ?", kandangIDs). Updates(map[string]any{ "project_flock_id": projectFlockID, @@ -641,7 +708,7 @@ func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, pr return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs") } - pivotRepo := s.pivotRepoWithTx(tx) + pivotRepo := s.pivotRepoWithTx(dbTransaction) records := make([]*entity.ProjectFlockKandang, len(kandangIDs)) for i, id := range kandangIDs { records[i] = &entity.ProjectFlockKandang{ @@ -655,7 +722,7 @@ func (s projectflockService) attachKandangs(ctx context.Context, tx *gorm.DB, pr return nil } -func (s projectflockService) detachKandangs(ctx context.Context, tx *gorm.DB, projectFlockID uint, kandangIDs []uint, resetStatus bool) error { +func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *gorm.DB, projectFlockID uint, kandangIDs []uint, resetStatus bool) error { if len(kandangIDs) == 0 { return nil } @@ -665,21 +732,21 @@ func (s projectflockService) detachKandangs(ctx context.Context, tx *gorm.DB, pr updates["status"] = string(utils.KandangStatusNonActive) } - if err := tx.Model(&entity.Kandang{}). + if err := dbTransaction.Model(&entity.Kandang{}). Where("id IN ?", kandangIDs). Updates(updates).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update kandangs") } - if err := s.pivotRepoWithTx(tx).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil { + if err := s.pivotRepoWithTx(dbTransaction).DeleteMany(ctx, projectFlockID, kandangIDs); err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to persist project flock history") } return nil } -func (s projectflockService) pivotRepoWithTx(tx *gorm.DB) repository.ProjectFlockKandangRepository { +func (s projectflockService) pivotRepoWithTx(dbTransaction *gorm.DB) repository.ProjectFlockKandangRepository { if s.PivotRepo == nil { - return repository.NewProjectFlockKandangRepository(tx) + return repository.NewProjectFlockKandangRepository(dbTransaction) } - return s.PivotRepo.WithTx(tx) + return s.PivotRepo.WithTx(dbTransaction) } diff --git a/internal/modules/production/project_flocks/validations/projectflock.validation.go b/internal/modules/production/project_flocks/validations/projectflock.validation.go index d2ce7331..00c9eab8 100644 --- a/internal/modules/production/project_flocks/validations/projectflock.validation.go +++ b/internal/modules/production/project_flocks/validations/projectflock.validation.go @@ -15,7 +15,6 @@ type Update struct { Category *string `json:"category,omitempty" validate:"omitempty,oneof=growing laying GROWING LAYING"` FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"` LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"` - Period *int `json:"period,omitempty" validate:"omitempty,number,gt=0"` KandangIds []uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,gt=0"` } From 445789edfe76da4be9824f0f339f92405ae77b5e Mon Sep 17 00:00:00 2001 From: ragilap Date: Tue, 21 Oct 2025 15:51:19 +0700 Subject: [PATCH 14/14] FIX(BE): category dto do not show --- .../project_flocks/dto/projectflock.dto.go | 43 +++---------------- .../services/projectflock.service.go | 13 +++--- .../validations/projectflock.validation.go | 6 +-- internal/utils/constant.go | 17 +++----- 4 files changed, 20 insertions(+), 59 deletions(-) diff --git a/internal/modules/production/project_flocks/dto/projectflock.dto.go b/internal/modules/production/project_flocks/dto/projectflock.dto.go index cb35eb0f..e639e968 100644 --- a/internal/modules/production/project_flocks/dto/projectflock.dto.go +++ b/internal/modules/production/project_flocks/dto/projectflock.dto.go @@ -16,13 +16,8 @@ import ( ) type ProjectFlockBaseDTO struct { - Id uint `json:"id"` - Period int `json:"period"` - Category string `json:"category"` - Flock *flockDTO.FlockBaseDTO `json:"flock"` - Area *areaDTO.AreaBaseDTO `json:"area"` - Fcr *fcrDTO.FcrBaseDTO `json:"fcr"` - Location *locationDTO.LocationBaseDTO `json:"location"` + Id uint `json:"id"` + Period int `json:"period"` } type ProjectFlockListDTO struct { @@ -72,6 +67,7 @@ func ToProjectFlockListDTO(e entity.ProjectFlock) ProjectFlockListDTO { return ProjectFlockListDTO{ ProjectFlockBaseDTO: createProjectFlockBaseDTO(e), Kandangs: kandangSummaries, + Category: e.Category, CreatedAt: e.CreatedAt, UpdatedAt: e.UpdatedAt, CreatedUser: createdUser, @@ -126,38 +122,9 @@ func defaultProjectFlockLatestApproval(e entity.ProjectFlock) approvalDTO.Approv } func createProjectFlockBaseDTO(e entity.ProjectFlock) ProjectFlockBaseDTO { - var flock *flockDTO.FlockBaseDTO - if e.Flock.Id != 0 { - mapped := flockDTO.ToFlockBaseDTO(e.Flock) - flock = &mapped - } - - var area *areaDTO.AreaBaseDTO - if e.Area.Id != 0 { - mapped := areaDTO.ToAreaBaseDTO(e.Area) - area = &mapped - } - - var fcr *fcrDTO.FcrBaseDTO - if e.Fcr.Id != 0 { - mapped := fcrDTO.ToFcrBaseDTO(e.Fcr) - fcr = &mapped - } - - var location *locationDTO.LocationBaseDTO - if e.Location.Id != 0 { - mapped := locationDTO.ToLocationBaseDTO(e.Location) - location = &mapped - } - return ProjectFlockBaseDTO{ - Id: e.Id, - Period: e.Period, - Category: e.Category, - Flock: flock, - Area: area, - Fcr: fcr, - Location: location, + Id: e.Id, + Period: e.Period, } } diff --git a/internal/modules/production/project_flocks/services/projectflock.service.go b/internal/modules/production/project_flocks/services/projectflock.service.go index 49401dd4..f6ebf6e7 100644 --- a/internal/modules/production/project_flocks/services/projectflock.service.go +++ b/internal/modules/production/project_flocks/services/projectflock.service.go @@ -219,8 +219,8 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* return nil, err } - category, ok := utils.NormalizeProjectFlockCategory(req.Category) - if !ok { + cat := strings.ToUpper(req.Category) + if !utils.IsValidProjectFlockCategory(cat) { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category") } @@ -257,7 +257,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (* createBody := &entity.ProjectFlock{ FlockId: req.FlockId, AreaId: req.AreaId, - Category: string(category), + Category: cat, FcrId: req.FcrId, LocationId: req.LocationId, CreatedBy: 1, @@ -342,11 +342,12 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id }) } if req.Category != nil { - if normalized, ok := utils.NormalizeProjectFlockCategory(*req.Category); ok { - updateBody["category"] = string(normalized) - } else { + cat := strings.ToUpper(*req.Category) + if !utils.IsValidProjectFlockCategory(cat) { return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category") } + + updateBody["category"] = cat } if req.FcrId != nil { updateBody["fcr_id"] = *req.FcrId diff --git a/internal/modules/production/project_flocks/validations/projectflock.validation.go b/internal/modules/production/project_flocks/validations/projectflock.validation.go index 00c9eab8..9ee066af 100644 --- a/internal/modules/production/project_flocks/validations/projectflock.validation.go +++ b/internal/modules/production/project_flocks/validations/projectflock.validation.go @@ -3,7 +3,7 @@ package validation type Create struct { FlockId uint `json:"flock_id" validate:"required_strict,number,gt=0"` AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"` - Category string `json:"category" validate:"required_strict,oneof=growing laying GROWING LAYING"` + Category string `json:"category" validate:"required_strict"` FcrId uint `json:"fcr_id" validate:"required_strict,number,gt=0"` LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"` KandangIds []uint `json:"kandang_ids" validate:"required,min=1,dive,gt=0"` @@ -12,7 +12,7 @@ type Create struct { type Update struct { FlockId *uint `json:"flock_id,omitempty" validate:"omitempty,number,gt=0"` AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"` - Category *string `json:"category,omitempty" validate:"omitempty,oneof=growing laying GROWING LAYING"` + Category *string `json:"category,omitempty" validate:"omitempty"` FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"` LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"` KandangIds []uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,gt=0"` @@ -22,7 +22,7 @@ type Query struct { Page int `query:"page" validate:"omitempty,number,min=1"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"` Search string `query:"search" validate:"omitempty,max=50"` - SortBy string `query:"sort_by" validate:"omitempty,oneof=area location kandangs period"` + SortBy string `query:"sort_by" validate:"omitempty"` SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"` AreaId uint `query:"area_id" validate:"omitempty,number,gt=0"` LocationId uint `query:"location_id" validate:"omitempty,number,gt=0"` diff --git a/internal/utils/constant.go b/internal/utils/constant.go index 5ab236b0..bdbc53b6 100644 --- a/internal/utils/constant.go +++ b/internal/utils/constant.go @@ -250,19 +250,12 @@ func IsValidCustomerSupplierType(v string) bool { return false } -func NormalizeProjectFlockCategory(v string) (ProjectFlockCategory, bool) { - normalized := ProjectFlockCategory(strings.ToUpper(strings.TrimSpace(v))) - switch normalized { - case ProjectFlockCategoryGrowing, ProjectFlockCategoryLaying: - return normalized, true - default: - return "", false - } -} - func IsValidProjectFlockCategory(v string) bool { - _, ok := NormalizeProjectFlockCategory(v) - return ok + switch ProjectFlockCategory(v) { + case ProjectFlockCategoryGrowing, ProjectFlockCategoryLaying: + return true + } + return false } func IsValidSupplierCategory(v string) bool {