From 4b147a3be76ded4f6eb97e2db86d9b913ba8c058 Mon Sep 17 00:00:00 2001 From: giovanni-ce Date: Mon, 8 Dec 2025 21:33:29 +0700 Subject: [PATCH 1/5] feat[BE-332]: add api get one tab sapronak --- internal/entities/kandang.go | 1 + .../controllers/closing.controller.go | 51 +++++ internal/modules/closings/dto/sapronak.dto.go | 26 +++ .../repositories/closing.repository.go | 187 ++++++++++++++++++ internal/modules/closings/route.go | 1 + .../closings/services/closing.service.go | 154 +++++++++++++++ .../validations/closing.validation.go | 15 +- .../recording_fifo_integration_test.go | 2 +- 8 files changed, 434 insertions(+), 3 deletions(-) create mode 100644 internal/modules/closings/dto/sapronak.dto.go diff --git a/internal/entities/kandang.go b/internal/entities/kandang.go index 7c083d95..e4db5655 100644 --- a/internal/entities/kandang.go +++ b/internal/entities/kandang.go @@ -20,5 +20,6 @@ type Kandang struct { CreatedUser User `gorm:"foreignKey:CreatedBy;references:Id"` Location Location `gorm:"foreignKey:LocationId;references:Id"` Pic User `gorm:"foreignKey:PicId;references:Id"` + Warehouses []Warehouse `gorm:"foreignKey:KandangId;references:Id"` ProjectFlockKandangs []ProjectFlockKandang `gorm:"foreignKey:KandangId;references:Id" json:"-"` } diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index 705a7b20..d025aa45 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -3,6 +3,7 @@ package controller import ( "math" "strconv" + "strings" "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto" service "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services" @@ -74,3 +75,53 @@ func (u *ClosingController) GetClosingSummary(c *fiber.Ctx) error { Data: result, }) } + +func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error { + param := c.Params("projectFlockId") + + id, err := strconv.Atoi(param) + if err != nil || id <= 0 { + return fiber.NewError(fiber.StatusBadRequest, "Invalid projectFlockId") + } + + query := &validation.SapronakQuery{ + Type: strings.ToLower(c.Query("type")), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + } + + if query.Page < 1 || query.Limit < 1 { + return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") + } + + if query.Type != validation.SapronakTypeIncoming && query.Type != validation.SapronakTypeOutgoing { + return fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing") + } + + result, totalResults, err := u.ClosingService.GetClosingSapronak(c, uint(id), query) + if err != nil { + return err + } + + resp := struct { + Code int `json:"code"` + Status string `json:"status"` + Message string `json:"message"` + Meta response.Meta `json:"meta"` + Data interface{} `json:"data"` + }{ + Code: fiber.StatusOK, + Status: "success", + Message: "Retrieved closing report (sapronak) successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: result, + } + + return c.Status(fiber.StatusOK). + JSON(resp) +} diff --git a/internal/modules/closings/dto/sapronak.dto.go b/internal/modules/closings/dto/sapronak.dto.go new file mode 100644 index 00000000..b83cb02d --- /dev/null +++ b/internal/modules/closings/dto/sapronak.dto.go @@ -0,0 +1,26 @@ +package dto + +import "time" + +type ClosingSapronakItemDTO struct { + Id uint64 `json:"id"` + Date string `json:"date"` + ReferenceNumber string `json:"reference_number"` + TransactionType string `json:"transaction_type"` + ProductName string `json:"product_name"` + ProductCategory string `json:"product_category"` + ProductSubCategory string `json:"product_sub_category"` + SourceWarehouse string `json:"source_warehouse"` + DestinationWarehouse string `json:"destination_warehouse,omitempty"` + Destination string `json:"destination,omitempty"` + Quantity float64 `json:"quantity"` + Unit string `json:"unit"` + FormattedQuantity string `json:"formatted_quantity"` + Notes string `json:"notes"` + SortDate time.Time `json:"-"` +} + +type ClosingSapronakDTO struct { + IncomingSapronak []ClosingSapronakItemDTO `json:"incoming_sapronak"` + OutgoingSapronak []ClosingSapronakItemDTO `json:"outgoing_sapronak"` +} diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index 946797fd..c81180b4 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -1,13 +1,20 @@ package repository import ( + "context" + "fmt" + "strings" + "time" + "gitlab.com/mbugroup/lti-api.git/internal/common/repository" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" + validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations" "gorm.io/gorm" ) type ClosingRepository interface { repository.BaseRepository[entity.ProjectFlock] + GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) } type ClosingRepositoryImpl struct { @@ -19,3 +26,183 @@ func NewClosingRepository(db *gorm.DB) ClosingRepository { BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlock](db), } } + +type SapronakRow struct { + Id uint64 `gorm:"column:id"` + SortDate time.Time `gorm:"column:sort_date"` + DateText string `gorm:"column:date_text"` + ReferenceNumber string `gorm:"column:reference_number"` + TransactionType string `gorm:"column:transaction_type"` + ProductName string `gorm:"column:product_name"` + ProductCategory string `gorm:"column:product_category"` + ProductSubCategory string `gorm:"column:product_sub_category"` + SourceWarehouse string `gorm:"column:source_warehouse"` + DestinationWarehouse string `gorm:"column:destination_warehouse"` + Destination string `gorm:"column:destination"` + Quantity float64 `gorm:"column:quantity"` + Unit string `gorm:"column:unit"` + Notes string `gorm:"column:notes"` +} + +type SapronakQueryParams struct { + Type string + WarehouseIDs []uint + ProjectFlockKandangIDs []uint + Limit int + Offset int +} + +func (r *ClosingRepositoryImpl) GetSapronak(ctx context.Context, params SapronakQueryParams) ([]SapronakRow, int64, error) { + db := r.DB().WithContext(ctx) + + var ( + unionParts []string + args []any + ) + + switch params.Type { + case validation.SapronakTypeIncoming: + if len(params.WarehouseIDs) == 0 { + return []SapronakRow{}, 0, nil + } + unionParts = append(unionParts, sapronakIncomingPurchasesSQL, sapronakIncomingTransfersSQL) + args = append(args, params.WarehouseIDs, params.WarehouseIDs) + case validation.SapronakTypeOutgoing: + if len(params.WarehouseIDs) > 0 { + unionParts = append(unionParts, sapronakOutgoingTransfersSQL) + args = append(args, params.WarehouseIDs) + } + if len(params.ProjectFlockKandangIDs) > 0 { + unionParts = append(unionParts, sapronakOutgoingMarketingsSQL) + args = append(args, params.ProjectFlockKandangIDs) + } + if len(unionParts) == 0 { + return []SapronakRow{}, 0, nil + } + default: + return nil, 0, fmt.Errorf("invalid sapronak type: %s", params.Type) + } + + unionSQL := strings.Join(unionParts, " UNION ALL ") + + var totalResults int64 + countSQL := fmt.Sprintf("SELECT COUNT(*) FROM (%s) AS combined", unionSQL) + if err := db.Raw(countSQL, args...).Scan(&totalResults).Error; err != nil { + return nil, 0, err + } + + dataArgs := append(append([]any{}, args...), params.Limit, params.Offset) + dataSQL := fmt.Sprintf("SELECT * FROM (%s) AS combined ORDER BY sort_date ASC, id ASC LIMIT ? OFFSET ?", unionSQL) + + var rows []SapronakRow + if err := db.Raw(dataSQL, dataArgs...).Scan(&rows).Error; err != nil { + return nil, 0, err + } + + return rows, totalResults, nil +} + +const ( + sapronakIncomingPurchasesSQL = ` +SELECT + CAST(pi.id AS BIGINT) AS id, + COALESCE(pi.received_date, '1970-01-01') AS sort_date, + COALESCE(TO_CHAR(pi.received_date, 'DD-Mon-YYYY'), '') AS date_text, + COALESCE(p.po_number, '') AS reference_number, + 'Purchase' AS transaction_type, + prod.name AS product_name, + pc.name AS product_category, + pc.name AS product_sub_category, + 'External Supplier' AS source_warehouse, + w.name AS destination_warehouse, + '' AS destination, + pi.total_qty AS quantity, + u.name AS unit, + COALESCE(p.notes, '') AS notes +FROM purchase_items pi +JOIN purchases p ON p.id = pi.purchase_id +JOIN products prod ON prod.id = pi.product_id +JOIN product_categories pc ON pc.id = prod.product_category_id +JOIN uoms u ON u.id = prod.uom_id +JOIN warehouses w ON w.id = pi.warehouse_id +WHERE pi.warehouse_id IN ? +` + + sapronakIncomingTransfersSQL = ` +SELECT + CAST(st.id AS BIGINT) AS id, + st.transfer_date AS sort_date, + TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text, + st.movement_number AS reference_number, + 'Internal Transfer In' AS transaction_type, + prod.name AS product_name, + pc.name AS product_category, + pc.name AS product_sub_category, + COALESCE(fw.name, '') AS source_warehouse, + COALESCE(tw.name, '') AS destination_warehouse, + '' AS destination, + std.quantity AS quantity, + u.name AS unit, + 'Stock Refill' AS notes +FROM stock_transfer_details std +JOIN stock_transfers st ON st.id = std.stock_transfer_id +LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id +LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id +JOIN products prod ON prod.id = std.product_id +JOIN product_categories pc ON pc.id = prod.product_category_id +JOIN uoms u ON u.id = prod.uom_id +WHERE st.to_warehouse_id IN ? +` + + sapronakOutgoingTransfersSQL = ` +SELECT + CAST(st.id AS BIGINT) AS id, + st.transfer_date AS sort_date, + TO_CHAR(st.transfer_date, 'DD-Mon-YYYY') AS date_text, + st.movement_number AS reference_number, + 'Internal Transfer Out' AS transaction_type, + prod.name AS product_name, + pc.name AS product_category, + pc.name AS product_sub_category, + COALESCE(fw.name, '') AS source_warehouse, + '' AS destination_warehouse, + COALESCE(tw.name, '') AS destination, + std.quantity AS quantity, + u.name AS unit, + 'Transfer to other unit' AS notes +FROM stock_transfer_details std +JOIN stock_transfers st ON st.id = std.stock_transfer_id +LEFT JOIN warehouses fw ON fw.id = st.from_warehouse_id +LEFT JOIN warehouses tw ON tw.id = st.to_warehouse_id +JOIN products prod ON prod.id = std.product_id +JOIN product_categories pc ON pc.id = prod.product_category_id +JOIN uoms u ON u.id = prod.uom_id +WHERE st.from_warehouse_id IN ? +` + + sapronakOutgoingMarketingsSQL = ` +SELECT + CAST(mp.id AS BIGINT) AS id, + m.so_date AS sort_date, + TO_CHAR(m.so_date, 'DD-Mon-YYYY') AS date_text, + m.so_number AS reference_number, + 'Trading Sales' AS transaction_type, + prod.name AS product_name, + pc.name AS product_category, + pc.name AS product_sub_category, + w.name AS source_warehouse, + '' AS destination_warehouse, + 'RETAIL CUSTOMER' AS destination, + mp.qty AS quantity, + u.name AS unit, + m.notes AS notes +FROM marketing_products mp +JOIN marketings m ON m.id = mp.marketing_id +JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id +JOIN products prod ON prod.id = pw.product_id +JOIN product_categories pc ON pc.id = prod.product_category_id +JOIN uoms u ON u.id = prod.uom_id +JOIN warehouses w ON w.id = pw.warehouse_id +WHERE pw.project_flock_kandang_id IN ? +` +) diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index acc6f8b2..bea32155 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -22,4 +22,5 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService route.Get("/", ctrl.GetAll) route.Get("/:projectFlockId", ctrl.GetClosingSummary) + route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak) } diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index d024789d..a689a2ea 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -3,6 +3,7 @@ package service import ( "context" "errors" + "strconv" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" entity "gitlab.com/mbugroup/lti-api.git/internal/entities" @@ -21,6 +22,7 @@ import ( type ClosingService interface { GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) + GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) (*dto.ClosingSapronakDTO, int64, error) } type closingService struct { @@ -96,6 +98,158 @@ func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*d return &summary, nil } +func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) (*dto.ClosingSapronakDTO, int64, error) { + if projectFlockID == 0 { + return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") + } + + if params == nil { + params = &validation.SapronakQuery{} + } + + if params.Page == 0 { + params.Page = 1 + } + if params.Limit == 0 { + params.Limit = 10 + } + + if err := s.Validate.Struct(params); err != nil { + return nil, 0, fiber.NewError(fiber.StatusBadRequest, err.Error()) + } + + if params.Type != validation.SapronakTypeIncoming && params.Type != validation.SapronakTypeOutgoing { + return nil, 0, fiber.NewError(fiber.StatusBadRequest, "type must be either incoming or outgoing") + } + + if _, err := s.Repository.GetByID(c.Context(), projectFlockID, nil); err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, 0, fiber.NewError(fiber.StatusNotFound, "Project flock tidak ditemukan") + } + s.Log.Errorf("Failed get project flock %d for sapronak closing: %+v", projectFlockID, err) + return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock") + } + + warehouseIDs, err := s.getWarehouseIDsByProjectFlock(c.Context(), projectFlockID) + if err != nil { + s.Log.Errorf("Failed to fetch warehouses for project flock %d: %+v", projectFlockID, err) + return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch warehouses for project flock") + } + + var projectFlockKandangIDs []uint + if params.Type == validation.SapronakTypeOutgoing { + projectFlockKandangIDs, err = s.getProjectFlockKandangIDs(c.Context(), projectFlockID) + if err != nil { + s.Log.Errorf("Failed to fetch project flock kandang IDs for project flock %d: %+v", projectFlockID, err) + return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock kandang") + } + } + + offset := (params.Page - 1) * params.Limit + rows, totalResults, err := s.Repository.GetSapronak(c.Context(), repository.SapronakQueryParams{ + Type: params.Type, + WarehouseIDs: warehouseIDs, + ProjectFlockKandangIDs: projectFlockKandangIDs, + Limit: params.Limit, + Offset: offset, + }) + if err != nil { + s.Log.Errorf("Failed to fetch sapronak %s for project flock %d: %+v", params.Type, projectFlockID, err) + return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sapronak data") + } + + items := make([]dto.ClosingSapronakItemDTO, 0, len(rows)) + for _, row := range rows { + dateStr := row.DateText + if dateStr == "" && !row.SortDate.IsZero() { + dateStr = row.SortDate.Format("02-Jan-2006") + } + items = append(items, dto.ClosingSapronakItemDTO{ + Id: row.Id, + Date: dateStr, + ReferenceNumber: row.ReferenceNumber, + TransactionType: row.TransactionType, + ProductName: row.ProductName, + ProductCategory: row.ProductCategory, + ProductSubCategory: row.ProductSubCategory, + SourceWarehouse: row.SourceWarehouse, + DestinationWarehouse: row.DestinationWarehouse, + Destination: row.Destination, + Quantity: row.Quantity, + Unit: row.Unit, + FormattedQuantity: formatQuantity(row.Quantity, row.Unit), + Notes: row.Notes, + SortDate: row.SortDate, + }) + } + + result := dto.ClosingSapronakDTO{ + IncomingSapronak: []dto.ClosingSapronakItemDTO{}, + OutgoingSapronak: []dto.ClosingSapronakItemDTO{}, + } + + if params.Type == validation.SapronakTypeIncoming { + result.IncomingSapronak = items + } else { + result.OutgoingSapronak = items + } + + return &result, totalResults, nil +} + +func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) { + var kandangIDs []uint + db := s.Repository.DB().WithContext(ctx) + + if err := db.Model(&entity.ProjectFlockKandang{}). + Where("project_flock_id = ?", projectFlockID). + Pluck("kandang_id", &kandangIDs).Error; err != nil { + return nil, err + } + + if len(kandangIDs) == 0 { + return []uint{}, nil + } + + var warehouses []entity.Warehouse + if err := db.Where("kandang_id IN ?", kandangIDs).Find(&warehouses).Error; err != nil { + return nil, err + } + + unique := make(map[uint]struct{}) + for _, warehouse := range warehouses { + unique[warehouse.Id] = struct{}{} + } + + ids := make([]uint, 0, len(unique)) + for id := range unique { + ids = append(ids, id) + } + + return ids, nil +} + +func (s closingService) getProjectFlockKandangIDs(ctx context.Context, projectFlockID uint) ([]uint, error) { + var ids []uint + err := s.Repository.DB().WithContext(ctx). + Model(&entity.ProjectFlockKandang{}). + Where("project_flock_id = ?", projectFlockID). + Pluck("id", &ids).Error + if err != nil { + return nil, err + } + + return ids, nil +} + +func formatQuantity(qty float64, uom string) string { + qtyStr := strconv.FormatFloat(qty, 'f', -1, 64) + if uom == "" { + return qtyStr + } + return qtyStr + " " + uom +} + func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID uint) (string, string, error) { if s.ApprovalSvc == nil { return "", "Belum Selesai", nil diff --git a/internal/modules/closings/validations/closing.validation.go b/internal/modules/closings/validations/closing.validation.go index 7d16d3ee..9b17b00d 100644 --- a/internal/modules/closings/validations/closing.validation.go +++ b/internal/modules/closings/validations/closing.validation.go @@ -1,11 +1,11 @@ package validation type Create struct { - Name string `json:"name" validate:"required_strict,min=3"` + Name string `json:"name" validate:"required_strict,min=3"` } type Update struct { - Name *string `json:"name,omitempty" validate:"omitempty"` + Name *string `json:"name,omitempty" validate:"omitempty"` } type Query struct { @@ -13,3 +13,14 @@ type Query struct { Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Search string `query:"search" validate:"omitempty,max=50"` } + +const ( + SapronakTypeIncoming = "incoming" + SapronakTypeOutgoing = "outgoing" +) + +type SapronakQuery struct { + Type string `query:"type" validate:"required,oneof=incoming outgoing"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` +} diff --git a/test/integration/production/recordings/recording_fifo_integration_test.go b/test/integration/production/recordings/recording_fifo_integration_test.go index a845e1a2..dd5f7d53 100644 --- a/test/integration/production/recordings/recording_fifo_integration_test.go +++ b/test/integration/production/recordings/recording_fifo_integration_test.go @@ -263,7 +263,7 @@ func createProductWarehouseRow(t *testing.T, db *gorm.DB, qty float64) entity.Pr ProductId: 1, WarehouseId: 1, Quantity: qty, - CreatedBy: 1, + // CreatedBy: 1, } if err := db.Create(&pw).Error; err != nil { t.Fatalf("create product warehouse: %v", err) From 26f2f3ccbf40190aa5bdff462b40bca2431dae6d Mon Sep 17 00:00:00 2001 From: giovanni-ce Date: Mon, 8 Dec 2025 22:02:02 +0700 Subject: [PATCH 2/5] adjust response get one general information closing --- internal/modules/closings/dto/closing.dto.go | 52 ++++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/internal/modules/closings/dto/closing.dto.go b/internal/modules/closings/dto/closing.dto.go index 6a280312..22654549 100644 --- a/internal/modules/closings/dto/closing.dto.go +++ b/internal/modules/closings/dto/closing.dto.go @@ -28,19 +28,19 @@ type ClosingDetailDTO struct { } type ClosingSummaryDTO struct { - LocationID uint `json:"location_id"` - Periode int `json:"periode"` - JenisProduk string `json:"jenis_produk"` - LabelPopulasi string `json:"label_populasi"` - JumlahPopulasi int `json:"jumlah_populasi"` - JumlahPopulasiFormatted string `json:"jumlah_populasi_formatted"` - JenisProject string `json:"jenis_project"` - KandangAktif int `json:"kandang_aktif"` - KandangAktifFormatted string `json:"kandang_aktif_formatted"` - StatusPembayaranPenjualan string `json:"status_pembayaran_penjualan"` - StatusPembayaranMitra string `json:"status_pembayaran_mitra"` - StatusProject string `json:"status_project"` - StatusClosing string `json:"status_closing"` + FlockID uint `json:"flock_id"` + Period int `json:"period"` + // JenisProduk string `json:"jenis_produk"` + // LabelPopulasi string `json:"label_populasi"` + Population int `json:"population"` + PopulationFormatted string `json:"population_formatted"` + ProjectType string `json:"project_type"` + ActiveHouseCount int `json:"active_house_count"` + ActiveHouseLabel string `json:"active_house_label"` + SalesPaymentStatus string `json:"sales_payment_status"` + // StatusPembayaranMitra string `json:"status_pembayaran_mitra"` + StatusProject string `json:"project_status"` + StatusClosing string `json:"closing_status"` } func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosing string) ClosingSummaryDTO { @@ -52,19 +52,19 @@ func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosi populationInt := int(population) return ClosingSummaryDTO{ - LocationID: project.LocationId, - Periode: period, - JenisProduk: project.Category, - LabelPopulasi: "", - JumlahPopulasi: populationInt, - JumlahPopulasiFormatted: fmt.Sprintf("%d Ekor", populationInt), - JenisProject: "", - KandangAktif: kandangCount, - KandangAktifFormatted: fmt.Sprintf("%d Kandang", kandangCount), - StatusPembayaranPenjualan: "Tempo", - StatusPembayaranMitra: "", - StatusProject: statusProject, - StatusClosing: statusClosing, + FlockID: project.Id, + Period: period, + // JenisProduk: project.Category, + // LabelPopulasi: "", + Population: populationInt, + PopulationFormatted: fmt.Sprintf("%d Ekor", populationInt), + ProjectType: project.Category, + ActiveHouseCount: kandangCount, + ActiveHouseLabel: fmt.Sprintf("%d Kandang", kandangCount), + SalesPaymentStatus: "Tempo", + // StatusPembayaranMitra: "", + StatusProject: statusProject, + StatusClosing: statusClosing, } } From 536e76d4811bbf5b201c9f4aa2fecfe2ee33f9df Mon Sep 17 00:00:00 2001 From: giovanni-ce Date: Tue, 9 Dec 2025 09:19:50 +0700 Subject: [PATCH 3/5] feat[BE-298]: add api get all list closing --- .../controllers/closing.controller.go | 6 ++--- .../closings/services/closing.service.go | 22 ++++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index 60af9e2a..ecbded41 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -40,17 +40,17 @@ func (u *ClosingController) GetAll(c *fiber.Ctx) error { } return c.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.ClosingListDTO]{ + JSON(response.SuccessWithPaginate[dto.ClosingSummaryDTO]{ Code: fiber.StatusOK, Status: "success", - Message: "Get all closings successfully", + Message: "Retrieved closing projects list successfully", Meta: response.Meta{ Page: query.Page, Limit: query.Limit, TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), TotalResults: totalResults, }, - Data: dto.ToClosingListDTOs(result), + Data: result, }) } diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index d3ab26e6..d59d2339 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -23,7 +23,7 @@ import ( ) type ClosingService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingSummaryDTO, int64, error) GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error) GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) @@ -62,7 +62,7 @@ func (s closingService) withClosingRelations(db *gorm.DB) *gorm.DB { Preload("KandangHistory.Chickins") } -func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) { +func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ClosingSummaryDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } @@ -70,9 +70,9 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity offset := (params.Page - 1) * params.Limit closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { - db = s.withRelations(db) + db = s.withClosingRelations(db) if params.Search != "" { - return db.Where("name LIKE ?", "%"+params.Search+"%") + return db.Where("flock_name LIKE ?", "%"+params.Search+"%") } return db.Order("created_at DESC").Order("updated_at DESC") }) @@ -81,7 +81,19 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity s.Log.Errorf("Failed to get closings: %+v", err) return nil, 0, err } - return closings, total, nil + + result := make([]dto.ClosingSummaryDTO, 0, len(closings)) + for _, closing := range closings { + statusProject, statusClosing, err := s.getApprovalStatuses(c.Context(), closing.Id) + if err != nil { + s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", closing.Id, err) + return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status") + } + + result = append(result, dto.ToClosingSummaryDTO(closing, statusProject, statusClosing)) + } + + return result, total, nil } func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) { From 4a2a80916fae6e44480d79ea6f992dc155c653ec Mon Sep 17 00:00:00 2001 From: giovanni-ce Date: Tue, 9 Dec 2025 16:23:05 +0700 Subject: [PATCH 4/5] adjust response api get all closing, response api get closing tab sapronak --- .../controllers/closing.controller.go | 34 ++++++---------- internal/modules/closings/dto/closing.dto.go | 34 ++++++++++++++++ internal/modules/closings/dto/sapronak.dto.go | 30 +++++++------- .../repositories/closing.repository.go | 24 +++++++++-- internal/modules/closings/route.go | 2 +- .../closings/services/closing.service.go | 40 +++++++------------ 6 files changed, 98 insertions(+), 66 deletions(-) diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index ecbded41..7b294d1e 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -40,7 +40,7 @@ func (u *ClosingController) GetAll(c *fiber.Ctx) error { } return c.Status(fiber.StatusOK). - JSON(response.SuccessWithPaginate[dto.ClosingSummaryDTO]{ + JSON(response.SuccessWithPaginate[dto.ClosingListItemDTO]{ Code: fiber.StatusOK, Status: "success", Message: "Retrieved closing projects list successfully", @@ -152,25 +152,17 @@ func (u *ClosingController) GetClosingSapronak(c *fiber.Ctx) error { return err } - resp := struct { - Code int `json:"code"` - Status string `json:"status"` - Message string `json:"message"` - Meta response.Meta `json:"meta"` - Data interface{} `json:"data"` - }{ - Code: fiber.StatusOK, - Status: "success", - Message: "Retrieved closing report (sapronak) successfully", - Meta: response.Meta{ - Page: query.Page, - Limit: query.Limit, - TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), - TotalResults: totalResults, - }, - Data: result, - } - return c.Status(fiber.StatusOK). - JSON(resp) + JSON(response.SuccessWithPaginate[dto.ClosingSapronakItemDTO]{ + Code: fiber.StatusOK, + Status: "success", + Message: "Retrieved closing report (sapronak) successfully", + Meta: response.Meta{ + Page: query.Page, + Limit: query.Limit, + TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))), + TotalResults: totalResults, + }, + Data: result, + }) } diff --git a/internal/modules/closings/dto/closing.dto.go b/internal/modules/closings/dto/closing.dto.go index 22654549..1f1cb492 100644 --- a/internal/modules/closings/dto/closing.dto.go +++ b/internal/modules/closings/dto/closing.dto.go @@ -27,6 +27,21 @@ type ClosingDetailDTO struct { ClosingListDTO } +type ClosingListItemDTO struct { + Id uint `json:"id"` + LocationID uint `json:"location_id"` + LocationName string `json:"location_name"` + ProjectCategory string `json:"project_category"` + Period int `json:"period"` + ClosingDate string `json:"closing_date"` + ShedLabel string `json:"shed_label"` + ShedCount int `json:"shed_count"` + SalesPaidAmount int64 `json:"sales_paid_amount"` + SalesRemainingAmount int64 `json:"sales_remaining_amount"` + SalesPaymentStatus string `json:"sales_payment_status"` + ProjectStatus string `json:"project_status"` +} + type ClosingSummaryDTO struct { FlockID uint `json:"flock_id"` Period int `json:"period"` @@ -68,6 +83,25 @@ func ToClosingSummaryDTO(project entity.ProjectFlock, statusProject, statusClosi } } +func ToClosingListItemDTO(project entity.ProjectFlock, projectStatus string) ClosingListItemDTO { + shedCount := len(project.KandangHistory) + + return ClosingListItemDTO{ + Id: project.Id, + LocationID: project.LocationId, + LocationName: project.Location.Name, + ProjectCategory: project.Category, + Period: maxPeriod(project.KandangHistory), + ClosingDate: "17-Nov-2025", + ShedLabel: fmt.Sprintf("%d Kandang", shedCount), + ShedCount: shedCount, + SalesPaidAmount: 21993726, + SalesRemainingAmount: 11075919, + SalesPaymentStatus: "Lunas", + ProjectStatus: projectStatus, + } +} + func maxPeriod(history []entity.ProjectFlockKandang) int { max := 0 for _, h := range history { diff --git a/internal/modules/closings/dto/sapronak.dto.go b/internal/modules/closings/dto/sapronak.dto.go index b83cb02d..50fc67cc 100644 --- a/internal/modules/closings/dto/sapronak.dto.go +++ b/internal/modules/closings/dto/sapronak.dto.go @@ -3,21 +3,21 @@ package dto import "time" type ClosingSapronakItemDTO struct { - Id uint64 `json:"id"` - Date string `json:"date"` - ReferenceNumber string `json:"reference_number"` - TransactionType string `json:"transaction_type"` - ProductName string `json:"product_name"` - ProductCategory string `json:"product_category"` - ProductSubCategory string `json:"product_sub_category"` - SourceWarehouse string `json:"source_warehouse"` - DestinationWarehouse string `json:"destination_warehouse,omitempty"` - Destination string `json:"destination,omitempty"` - Quantity float64 `json:"quantity"` - Unit string `json:"unit"` - FormattedQuantity string `json:"formatted_quantity"` - Notes string `json:"notes"` - SortDate time.Time `json:"-"` + Id uint64 `json:"id"` + Date string `json:"date"` + ReferenceNumber string `json:"reference_number"` + TransactionType string `json:"transaction_type"` + ProductName string `json:"product_name"` + ProductCategory string `json:"product_category"` + ProductSubCategory string `json:"product_sub_category"` + SourceWarehouse string `json:"source_warehouse"` + DestinationWarehouse string `json:"destination_warehouse,omitempty"` + // Destination string `json:"destination,omitempty"` + Quantity float64 `json:"quantity"` + Unit string `json:"unit"` + FormattedQuantity string `json:"formatted_quantity"` + Notes string `json:"notes"` + SortDate time.Time `json:"-"` } type ClosingSapronakDTO struct { diff --git a/internal/modules/closings/repositories/closing.repository.go b/internal/modules/closings/repositories/closing.repository.go index c81180b4..fe555378 100644 --- a/internal/modules/closings/repositories/closing.repository.go +++ b/internal/modules/closings/repositories/closing.repository.go @@ -112,7 +112,11 @@ SELECT 'Purchase' AS transaction_type, prod.name AS product_name, pc.name AS product_category, - pc.name AS product_sub_category, + COALESCE(( + SELECT string_agg(f.name, ' ') + FROM flags f + WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id + ), '') AS product_sub_category, 'External Supplier' AS source_warehouse, w.name AS destination_warehouse, '' AS destination, @@ -137,7 +141,11 @@ SELECT 'Internal Transfer In' AS transaction_type, prod.name AS product_name, pc.name AS product_category, - pc.name AS product_sub_category, + COALESCE(( + SELECT string_agg(f.name, ' ') + FROM flags f + WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id + ), '') AS product_sub_category, COALESCE(fw.name, '') AS source_warehouse, COALESCE(tw.name, '') AS destination_warehouse, '' AS destination, @@ -163,7 +171,11 @@ SELECT 'Internal Transfer Out' AS transaction_type, prod.name AS product_name, pc.name AS product_category, - pc.name AS product_sub_category, + COALESCE(( + SELECT string_agg(f.name, ' ') + FROM flags f + WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id + ), '') AS product_sub_category, COALESCE(fw.name, '') AS source_warehouse, '' AS destination_warehouse, COALESCE(tw.name, '') AS destination, @@ -189,7 +201,11 @@ SELECT 'Trading Sales' AS transaction_type, prod.name AS product_name, pc.name AS product_category, - pc.name AS product_sub_category, + COALESCE(( + SELECT string_agg(f.name, ' ') + FROM flags f + WHERE f.flagable_type = 'products' AND f.flagable_id = prod.id + ), '') AS product_sub_category, w.name AS source_warehouse, '' AS destination_warehouse, 'RETAIL CUSTOMER' AS destination, diff --git a/internal/modules/closings/route.go b/internal/modules/closings/route.go index f04c14c4..8634df29 100644 --- a/internal/modules/closings/route.go +++ b/internal/modules/closings/route.go @@ -12,7 +12,7 @@ import ( func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService) { ctrl := controller.NewClosingController(s) - route := v1.Group("/closing") + route := v1.Group("/closings") // route.Get("/", m.Auth(u), ctrl.GetAll) // route.Post("/", m.Auth(u), ctrl.CreateOne) diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index d59d2339..6640a6bd 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -23,11 +23,11 @@ import ( ) type ClosingService interface { - GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingSummaryDTO, int64, error) + GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error) GetProjectFlockByID(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error) GetPenjualan(ctx *fiber.Ctx, projectFlockID uint) ([]entity.MarketingDeliveryProduct, error) GetClosingSummary(ctx *fiber.Ctx, projectFlockID uint) (*dto.ClosingSummaryDTO, error) - GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) (*dto.ClosingSapronakDTO, int64, error) + GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) } type closingService struct { @@ -58,11 +58,12 @@ func (s closingService) withRelations(db *gorm.DB) *gorm.DB { func (s closingService) withClosingRelations(db *gorm.DB) *gorm.DB { return s.withRelations(db). + Preload("Location"). Preload("KandangHistory"). Preload("KandangHistory.Chickins") } -func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ClosingSummaryDTO, int64, error) { +func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.ClosingListItemDTO, int64, error) { if err := s.Validate.Struct(params); err != nil { return nil, 0, err } @@ -82,15 +83,15 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl return nil, 0, err } - result := make([]dto.ClosingSummaryDTO, 0, len(closings)) + result := make([]dto.ClosingListItemDTO, 0, len(closings)) for _, closing := range closings { - statusProject, statusClosing, err := s.getApprovalStatuses(c.Context(), closing.Id) + statusProject, _, err := s.getApprovalStatuses(c.Context(), closing.Id) if err != nil { s.Log.Errorf("Failed to retrieve approval statuses for project flock %d: %+v", closing.Id, err) return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval status") } - result = append(result, dto.ToClosingSummaryDTO(closing, statusProject, statusClosing)) + result = append(result, dto.ToClosingListItemDTO(closing, statusProject)) } return result, total, nil @@ -158,7 +159,7 @@ func (s closingService) GetClosingSummary(c *fiber.Ctx, projectFlockID uint) (*d return &summary, nil } -func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) (*dto.ClosingSapronakDTO, int64, error) { +func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error) { if projectFlockID == 0 { return nil, 0, fiber.NewError(fiber.StatusBadRequest, "Invalid project flock id") } @@ -234,27 +235,16 @@ func (s closingService) GetClosingSapronak(c *fiber.Ctx, projectFlockID uint, pa ProductSubCategory: row.ProductSubCategory, SourceWarehouse: row.SourceWarehouse, DestinationWarehouse: row.DestinationWarehouse, - Destination: row.Destination, - Quantity: row.Quantity, - Unit: row.Unit, - FormattedQuantity: formatQuantity(row.Quantity, row.Unit), - Notes: row.Notes, - SortDate: row.SortDate, + // Destination: row.Destination, + Quantity: row.Quantity, + Unit: row.Unit, + FormattedQuantity: formatQuantity(row.Quantity, row.Unit), + Notes: row.Notes, + SortDate: row.SortDate, }) } - result := dto.ClosingSapronakDTO{ - IncomingSapronak: []dto.ClosingSapronakItemDTO{}, - OutgoingSapronak: []dto.ClosingSapronakItemDTO{}, - } - - if params.Type == validation.SapronakTypeIncoming { - result.IncomingSapronak = items - } else { - result.OutgoingSapronak = items - } - - return &result, totalResults, nil + return items, totalResults, nil } func (s closingService) getWarehouseIDsByProjectFlock(ctx context.Context, projectFlockID uint) ([]uint, error) { From 79d488c979383deda1341070c607efcf3ea50ea2 Mon Sep 17 00:00:00 2001 From: giovanni-ce Date: Wed, 10 Dec 2025 11:22:12 +0700 Subject: [PATCH 5/5] adjust create product warehouse at adjustment and transfer --- .../modules/inventory/adjustments/module.go | 4 +- .../services/adjustment.service.go | 69 ++++++++++++++----- .../modules/inventory/transfers/module.go | 6 +- .../transfers/services/transfer.service.go | 50 ++++++++++++-- 4 files changed, 105 insertions(+), 24 deletions(-) diff --git a/internal/modules/inventory/adjustments/module.go b/internal/modules/inventory/adjustments/module.go index b3e12676..c4ca6129 100644 --- a/internal/modules/inventory/adjustments/module.go +++ b/internal/modules/inventory/adjustments/module.go @@ -9,6 +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" + rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" @@ -21,10 +22,11 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat stockLogsRepo := rStockLogs.NewStockLogRepository(db) warehouseRepo := rWarehouse.NewWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) + projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) userRepo := rUser.NewUserRepository(db) productRepo := rproduct.NewProductRepository(db) - adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate) + adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate, projectFlockKandangRepo) userService := sUser.NewUserService(userRepo, validate) AdjustmentRoutes(router, userService, adjustmentService) diff --git a/internal/modules/inventory/adjustments/services/adjustment.service.go b/internal/modules/inventory/adjustments/services/adjustment.service.go index be4ae7a2..39ed5b19 100644 --- a/internal/modules/inventory/adjustments/services/adjustment.service.go +++ b/internal/modules/inventory/adjustments/services/adjustment.service.go @@ -1,7 +1,9 @@ package service import ( + "context" "errors" + "fmt" "strings" common "gitlab.com/mbugroup/lti-api.git/internal/common/service" @@ -11,6 +13,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" + projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" "gorm.io/gorm" @@ -27,22 +30,24 @@ type AdjustmentService interface { } type adjustmentService struct { - Log *logrus.Logger - Validate *validator.Validate - StockLogsRepository stockLogsRepo.StockLogRepository - WarehouseRepo warehouseRepo.WarehouseRepository - ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository - ProductRepo productRepo.ProductRepository + Log *logrus.Logger + Validate *validator.Validate + StockLogsRepository stockLogsRepo.StockLogRepository + WarehouseRepo warehouseRepo.WarehouseRepository + ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository + ProductRepo productRepo.ProductRepository + ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository } -func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService { +func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) AdjustmentService { return &adjustmentService{ - Log: utils.Log, - Validate: validate, - StockLogsRepository: stockLogsRepo, - WarehouseRepo: warehouseRepo, - ProductWarehouseRepo: productWarehouseRepo, - ProductRepo: productRepo, + Log: utils.Log, + Validate: validate, + StockLogsRepository: stockLogsRepo, + WarehouseRepo: warehouseRepo, + ProductWarehouseRepo: productWarehouseRepo, + ProductRepo: productRepo, + ProjectFlockKandangRepo: projectFlockKandangRepo, } } @@ -105,11 +110,15 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse") } if !isProductWarehouseExist { - + projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.WarehouseID)) + if err != nil { + return nil, err + } newPW := &entity.ProductWarehouse{ - ProductId: uint(req.ProductID), - WarehouseId: uint(req.WarehouseID), - Quantity: 0, + ProductId: uint(req.ProductID), + WarehouseId: uint(req.WarehouseID), + Quantity: 0, + ProjectFlockKandangId: &projectFlockKandangID, // CreatedBy: 1, // TODO: should Get from auth middleware } @@ -170,6 +179,32 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e return s.GetOne(c, createdLogId) } +func (s *adjustmentService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) { + warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID)) + } + s.Log.Errorf("Failed to get warehouse %d: %+v", warehouseID, err) + return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang") + } + + if warehouse.KandangId == nil || *warehouse.KandangId == 0 { + return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID)) + } + + projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId)) + } + s.Log.Errorf("Failed to get active project flock for kandang %d: %+v", *warehouse.KandangId, err) + return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang") + } + + return uint(projectFlockKandang.Id), nil +} + func (s *adjustmentService) AdjustmentHistory(c *fiber.Ctx, query *validation.Query) ([]*entity.StockLog, int64, error) { if err := s.Validate.Struct(query); err != nil { return nil, 0, err diff --git a/internal/modules/inventory/transfers/module.go b/internal/modules/inventory/transfers/module.go index 734f0f03..19a0ded6 100644 --- a/internal/modules/inventory/transfers/module.go +++ b/internal/modules/inventory/transfers/module.go @@ -9,6 +9,8 @@ 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" + rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" + rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/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" @@ -25,8 +27,10 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate supplierRepo := rSupplier.NewSupplierRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) userRepo := rUser.NewUserRepository(db) + warehouseRepo := rWarehouse.NewWarehouseRepository(db) + projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db) - transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo) + transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo) userService := sUser.NewUserService(userRepo, validate) TransferRoutes(router, userService, transferService) diff --git a/internal/modules/inventory/transfers/services/transfer.service.go b/internal/modules/inventory/transfers/services/transfer.service.go index ef273664..a0edad0a 100644 --- a/internal/modules/inventory/transfers/services/transfer.service.go +++ b/internal/modules/inventory/transfers/services/transfer.service.go @@ -1,17 +1,21 @@ package service import ( + "context" "errors" "fmt" + "strings" + entity "gitlab.com/mbugroup/lti-api.git/internal/entities" m "gitlab.com/mbugroup/lti-api.git/internal/middleware" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" 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" + warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" + projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" "gitlab.com/mbugroup/lti-api.git/internal/utils" - "strings" "github.com/go-playground/validator/v10" "github.com/gofiber/fiber/v2" @@ -35,9 +39,11 @@ type transferService struct { StockLogsRepository rStockLogs.StockLogRepository ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository SupplierRepo rSupplier.SupplierRepository + WarehouseRepo warehouseRepo.WarehouseRepository + ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository } -func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository) TransferService { +func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo warehouseRepo.WarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) TransferService { return &transferService{ Log: utils.Log, Validate: validate, @@ -48,6 +54,8 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr StockLogsRepository: stockLogsRepo, ProductWarehouseRepo: productWarehouseRepo, SupplierRepo: supplierRepo, + WarehouseRepo: warehouseRepo, + ProjectFlockKandangRepo: projectFlockKandangRepo, } } func (s transferService) withRelations(db *gorm.DB) *gorm.DB { @@ -301,10 +309,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } if err != nil && errors.Is(err, gorm.ErrRecordNotFound) { // Jika belum ada record untuk produk di gudang tujuan, buat baru + ctx := c.Context() + projectFlockKandangID, err := s.getActiveProjectFlockKandangID(ctx, uint(req.DestinationWarehouseID)) + if err != nil { + return err + } destPW = &entity.ProductWarehouse{ - ProductId: uint(product.ProductID), - WarehouseId: uint(req.DestinationWarehouseID), - Quantity: 0, + ProductId: uint(product.ProductID), + WarehouseId: uint(req.DestinationWarehouseID), + Quantity: 0, + ProjectFlockKandangId: &projectFlockKandangID, // CreatedBy: 1, // TODO: should Get from auth middleware } if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil { @@ -357,3 +371,29 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques } return result, nil } + +func (s *transferService) getActiveProjectFlockKandangID(ctx context.Context, warehouseID uint) (uint, error) { + warehouse, err := s.WarehouseRepo.GetByID(ctx, warehouseID, nil) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Gudang dengan ID %d tidak ditemukan", warehouseID)) + } + s.Log.Errorf("Failed to get warehouse %d: %+v", warehouseID, err) + return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil data gudang") + } + + if warehouse.KandangId == nil || *warehouse.KandangId == 0 { + return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Gudang %d belum terhubung ke kandang", warehouseID)) + } + + projectFlockKandang, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(ctx, uint(*warehouse.KandangId)) + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d belum memiliki project flock aktif", *warehouse.KandangId)) + } + s.Log.Errorf("Failed to get active project flock for kandang %d: %+v", *warehouse.KandangId, err) + return 0, fiber.NewError(fiber.StatusInternalServerError, "Gagal mengambil project flock kandang") + } + + return uint(projectFlockKandang.Id), nil +}