Merge branch 'feat/BE/Sprint-6' of https://gitlab.com/mbugroup/lti-api into feat/BE/US-279/closing-produksi

This commit is contained in:
ragilap
2025-12-10 17:03:40 +07:00
49 changed files with 1350 additions and 394 deletions
@@ -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"
@@ -39,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.ClosingListItemDTO]{
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,
})
}
@@ -123,3 +124,67 @@ func (u *ClosingController) GetPenjualan(c *fiber.Ctx) error {
Data: dto.ToPenjualanRealisasiResponseDTO(projectFlock.Category, uint(projectFlockID), result),
})
}
func (u *ClosingController) GetOverhead(c *fiber.Ctx) error {
param := c.Params("project_flock_id")
projectFlockID, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Project Flock Id")
}
result, err := u.ClosingService.GetOverhead(c, uint(projectFlockID))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get overhead successfully",
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
}
return c.Status(fiber.StatusOK).
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,
})
}
+60 -26
View File
@@ -27,20 +27,35 @@ 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 {
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 +67,38 @@ 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,
}
}
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,
}
}
@@ -4,14 +4,13 @@ import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
deliveryOrdersDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
customerDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/dto"
kandangDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/dto"
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
)
// === Response DTO ===
type SalesDTO struct {
Id uint `json:"id"`
RealizationDate time.Time `json:"realization_date"`
@@ -0,0 +1,175 @@
package dto
import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
)
// === DTO Structs ===
type OverheadDTO struct {
ItemName string `json:"item_name"`
UOMName string `json:"uom_name"`
BudgetQuantity float64 `json:"budget_quantity"`
BudgetUnitPrice float64 `json:"budget_unit_price"`
BudgetTotalAmount float64 `json:"budget_total_amount"`
ActualDate string `json:"actual_date"`
ActualQuantity float64 `json:"actual_quantity"`
ActualUnitPrice float64 `json:"actual_unit_price"`
ActualTotalAmount float64 `json:"actual_total_amount"`
CostPerBird float64 `json:"cost_per_bird"`
}
type TotalDTO struct {
BudgetQuantity float64 `json:"budget_quantity"`
BudgetTotalAmount float64 `json:"budget_total_amount"`
ActualQuantity float64 `json:"actual_quantity"`
ActualTotalAmount float64 `json:"actual_total_amount"`
CostPerBird float64 `json:"cost_per_bird"`
}
type OverheadListDTO struct {
Total TotalDTO `json:"total"`
Overheads []OverheadDTO `json:"overheads"`
}
// === Mapper Functions ===
func ToOverheadDTO(budget *entity.ProjectBudget, realization *entity.ExpenseRealization) OverheadDTO {
if budget == nil && realization == nil {
return OverheadDTO{}
}
var itemName, itemUOM string
if budget != nil {
itemName, itemUOM = getItemInfo(budget.Nonstock)
}
if itemName == "" && realization != nil && realization.ExpenseNonstock != nil {
itemName, itemUOM = getItemInfo(realization.ExpenseNonstock.Nonstock)
}
dto := OverheadDTO{
ItemName: itemName,
UOMName: itemUOM,
}
if budget != nil {
dto.BudgetQuantity = budget.Qty
dto.BudgetUnitPrice = budget.Price
dto.BudgetTotalAmount = calculateTotal(budget.Qty, budget.Price)
}
if realization != nil {
dto.ActualQuantity = realization.Qty
dto.ActualUnitPrice = realization.Price
dto.ActualTotalAmount = calculateTotal(realization.Qty, realization.Price)
dto.ActualDate = formatRealizationDate(realization)
}
return dto
}
func ToOverheadListDTOs(budgets []entity.ProjectBudget, realizations []entity.ExpenseRealization, totalChickinQty float64) OverheadListDTO {
overheadsByNonstockID := make(map[uint]*OverheadDTO)
latestDateByNonstockID := make(map[uint]string)
for i := range budgets {
nonstockID := budgets[i].NonstockId
if overheadsByNonstockID[nonstockID] == nil {
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
}
itemName, itemUOM := getItemInfo(budgets[i].Nonstock)
overheadsByNonstockID[nonstockID].ItemName = itemName
overheadsByNonstockID[nonstockID].UOMName = itemUOM
overheadsByNonstockID[nonstockID].BudgetQuantity = budgets[i].Qty
overheadsByNonstockID[nonstockID].BudgetUnitPrice = budgets[i].Price
overheadsByNonstockID[nonstockID].BudgetTotalAmount = calculateTotal(budgets[i].Qty, budgets[i].Price)
}
for i := range realizations {
if realizations[i].ExpenseNonstock == nil || realizations[i].ExpenseNonstock.NonstockId == nil {
continue
}
nonstockID := uint(*realizations[i].ExpenseNonstock.NonstockId)
if overheadsByNonstockID[nonstockID] == nil {
overheadsByNonstockID[nonstockID] = &OverheadDTO{}
}
overheadsByNonstockID[nonstockID].ActualQuantity += realizations[i].Qty
overheadsByNonstockID[nonstockID].ActualTotalAmount += calculateTotal(realizations[i].Qty, realizations[i].Price)
if overheadsByNonstockID[nonstockID].ItemName == "" {
itemName, itemUOM := getItemInfo(realizations[i].ExpenseNonstock.Nonstock)
overheadsByNonstockID[nonstockID].ItemName = itemName
overheadsByNonstockID[nonstockID].UOMName = itemUOM
}
realizationDateStr := formatRealizationDate(&realizations[i])
if realizationDateStr != "" {
if latestDateByNonstockID[nonstockID] == "" || realizationDateStr > latestDateByNonstockID[nonstockID] {
latestDateByNonstockID[nonstockID] = realizationDateStr
}
}
}
var totalBudgetQuantity, totalBudgetAmount, totalActualQuantity, totalActualAmount float64
overheadItems := make([]OverheadDTO, 0, len(overheadsByNonstockID))
for nonstockID, overhead := range overheadsByNonstockID {
overhead.ActualDate = latestDateByNonstockID[nonstockID]
overhead.CostPerBird = calculateCostPerBird(overhead.ActualTotalAmount, totalChickinQty)
if overhead.ActualQuantity > 0 {
overhead.ActualUnitPrice = overhead.ActualTotalAmount / overhead.ActualQuantity
}
totalBudgetQuantity += overhead.BudgetQuantity
totalBudgetAmount += overhead.BudgetTotalAmount
totalActualQuantity += overhead.ActualQuantity
totalActualAmount += overhead.ActualTotalAmount
overheadItems = append(overheadItems, *overhead)
}
return OverheadListDTO{
Total: TotalDTO{
BudgetQuantity: totalBudgetQuantity,
BudgetTotalAmount: totalBudgetAmount,
ActualQuantity: totalActualQuantity,
ActualTotalAmount: totalActualAmount,
CostPerBird: calculateCostPerBird(totalActualAmount, totalChickinQty),
},
Overheads: overheadItems,
}
}
// === Helper Functions ===
func getItemInfo(nonstock *entity.Nonstock) (string, string) {
if nonstock != nil && nonstock.Id != 0 {
return nonstock.Name, nonstock.Uom.Name
}
return "", ""
}
func calculateTotal(qty, price float64) float64 {
return qty * price
}
func calculateCostPerBird(totalPrice, totalChickinQty float64) float64 {
if totalChickinQty > 0 {
return totalPrice / totalChickinQty
}
return 0
}
func formatRealizationDate(realization *entity.ExpenseRealization) string {
if realization != nil && realization.ExpenseNonstock != nil && realization.ExpenseNonstock.Expense != nil {
if !realization.ExpenseNonstock.Expense.RealizationDate.IsZero() {
return realization.ExpenseNonstock.Expense.RealizationDate.Format("2006-01-02T15:04:05Z07:00")
}
}
return ""
}
@@ -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"`
}
+7 -2
View File
@@ -9,7 +9,9 @@ import (
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
rExpenseRealization "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
rMarketings "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/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"
@@ -22,12 +24,15 @@ func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
closingRepo := rClosing.NewClosingRepository(db)
userRepo := rUser.NewUserRepository(db)
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
projectBudgetRepo := rProjectFlock.NewProjectBudgetRepository(db)
marketingRepo := rMarketings.NewMarketingRepository(db)
marketingDeliveryProductRepo := rMarketings.NewMarketingDeliveryProductRepository(db)
expenseRealizationRepo := rExpenseRealization.NewExpenseRealizationRepository(db)
chickinRepo := rChickin.NewChickinRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, validate)
closingService := sClosing.NewClosingService(closingRepo, projectFlockRepo, marketingRepo, marketingDeliveryProductRepo, approvalService, expenseRealizationRepo, projectBudgetRepo, chickinRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ClosingRoutes(router, userService, closingService)
@@ -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,199 @@ 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,
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,
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,
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,
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,
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,
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,
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,
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 ?
`
)
+3 -1
View File
@@ -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)
@@ -22,5 +22,7 @@ func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService
route.Get("/", ctrl.GetAll)
route.Get("/:project_flock_id/penjualan", ctrl.GetPenjualan)
route.Get("/:project_flock_id/overhead", ctrl.GetOverhead)
route.Get("/:projectFlockId", ctrl.GetClosingSummary)
route.Get("/:projectFlockId/sapronak", ctrl.GetClosingSapronak)
}
@@ -3,14 +3,17 @@ 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"
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
expenseRealizationRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
marketingDeliveryProductRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
marketingRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
chickinRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
projectflockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
@@ -22,10 +25,12 @@ import (
)
type ClosingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, 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)
GetOverhead(ctx *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error)
GetClosingSapronak(ctx *fiber.Ctx, projectFlockID uint, params *validation.SapronakQuery) ([]dto.ClosingSapronakItemDTO, int64, error)
}
type closingService struct {
@@ -36,9 +41,12 @@ type closingService struct {
MarketingRepo marketingRepository.MarketingRepository
MarketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository
ApprovalSvc commonSvc.ApprovalService
ExpenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository
ProjectBudgetRepo projectflockRepository.ProjectBudgetRepository
ChickinRepo chickinRepository.ProjectChickinRepository
}
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) ClosingService {
func NewClosingService(repo repository.ClosingRepository, projectFlockRepo projectflockRepository.ProjectflockRepository, marketingRepo marketingRepository.MarketingRepository, marketingDeliveryProductRepo marketingDeliveryProductRepository.MarketingDeliveryProductRepository, approvalSvc commonSvc.ApprovalService, expenseRealizationRepo expenseRealizationRepository.ExpenseRealizationRepository, projectBudgetRepo projectflockRepository.ProjectBudgetRepository, chickinRepo chickinRepository.ProjectChickinRepository, validate *validator.Validate) ClosingService {
return &closingService{
Log: utils.Log,
Validate: validate,
@@ -47,6 +55,9 @@ func NewClosingService(repo repository.ClosingRepository, projectFlockRepo proje
MarketingRepo: marketingRepo,
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
ApprovalSvc: approvalSvc,
ExpenseRealizationRepo: expenseRealizationRepo,
ProjectBudgetRepo: projectBudgetRepo,
ChickinRepo: chickinRepo,
}
}
@@ -56,11 +67,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) ([]entity.ProjectFlock, 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
}
@@ -68,9 +80,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")
})
@@ -79,7 +91,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.ClosingListItemDTO, 0, len(closings))
for _, closing := range closings {
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.ToClosingListItemDTO(closing, statusProject))
}
return result, total, nil
}
func (s closingService) GetProjectFlockByID(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
@@ -144,6 +168,147 @@ 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.ClosingSapronakItemDTO, 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,
})
}
return items, 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
@@ -188,3 +353,29 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
return statusProject, statusClosing, nil
}
func (s closingService) GetOverhead(c *fiber.Ctx, projectFlockID uint) (*dto.OverheadListDTO, error) {
budgets, err := s.ProjectBudgetRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, err
}
realizations, err := s.ExpenseRealizationRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, err
}
chickins, err := s.ChickinRepo.GetByProjectFlockID(c.Context(), projectFlockID)
if err != nil {
return nil, err
}
var totalChickinQty float64
for _, chickin := range chickins {
totalChickinQty += chickin.UsageQty
}
result := dto.ToOverheadListDTOs(budgets, realizations, totalChickinQty)
return &result, nil
}
@@ -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"`
}
@@ -12,6 +12,7 @@ type ExpenseRealizationRepository interface {
repository.BaseRepository[entity.ExpenseRealization]
IdExists(ctx context.Context, id uint64) (bool, error)
GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error)
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error)
}
type ExpenseRealizationRepositoryImpl struct {
@@ -30,11 +31,22 @@ func (r *ExpenseRealizationRepositoryImpl) IdExists(ctx context.Context, id uint
func (r *ExpenseRealizationRepositoryImpl) GetByExpenseNonstockID(ctx context.Context, expenseNonstockID uint64) (*entity.ExpenseRealization, error) {
var realization entity.ExpenseRealization
err := r.DB().WithContext(ctx).
Where("expense_nonstock_id = ?", expenseNonstockID).
First(&realization).Error
if err != nil {
return nil, err
}
return &realization, nil
err := r.DB().WithContext(ctx).Where("expense_nonstock_id = ?", expenseNonstockID).First(&realization).Error
return &realization, err
}
func (r *ExpenseRealizationRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ExpenseRealization, error) {
var realizations []entity.ExpenseRealization
err := r.DB().WithContext(ctx).
Preload("ExpenseNonstock").
Preload("ExpenseNonstock.Nonstock").
Preload("ExpenseNonstock.Nonstock.Uom").
Preload("ExpenseNonstock.Expense").
Joins("JOIN expense_nonstocks ON expense_nonstocks.id = expense_realizations.expense_nonstock_id").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = expense_nonstocks.project_flock_kandang_id").
Joins("JOIN expenses ON expenses.id = expense_nonstocks.expense_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Where("expenses.category = ?", "BOP").
Find(&realizations).Error
return realizations, err
}
@@ -11,6 +11,7 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
expenseDto "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/dto"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/validations"
@@ -188,7 +189,11 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number")
}
createdBy := uint64(1) //todo get from auth
actorID, err := middleware.ActorIDFromContext(c)
if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
}
createdBy := uint64(actorID)
expense = &entity.Expense{
ReferenceNumber: referenceNumber,
PoNumber: req.PoNumber,
@@ -502,7 +507,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
}
}
actorID := uint(1) // TODO: replace with authenticated user id
actorID, err := middleware.ActorIDFromContext(c)
if err != nil {
return fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
}
if *latestApproval.Action != entity.ApprovalActionUpdated {
approvalAction := entity.ApprovalActionUpdated
@@ -689,7 +697,10 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (
return nil, err
}
actorID := uint(1) // TODO: replace with authenticated user id
actorID, err := middleware.ActorIDFromContext(c)
if err != nil {
return nil, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
}
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, id, nil)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
@@ -1006,11 +1017,14 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
return nil, fiber.NewError(fiber.StatusBadRequest, "No expense IDs provided")
}
actorID := uint(1) // TODO: replace with authenticated user id
actorID, err := middleware.ActorIDFromContext(c)
if err != nil {
return nil, fiber.NewError(fiber.StatusUnauthorized, "Failed to get actor ID from context")
}
var results []expenseDto.ExpenseDetailDTO
err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
expenseRepoTx := repository.NewExpenseRepository(tx)
@@ -21,10 +21,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)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, projectFlockKandangRepo, validate)
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate, projectFlockKandangRepo)
userService := sUser.NewUserService(userRepo, validate)
AdjustmentRoutes(router, userService, adjustmentService)
@@ -1,7 +1,7 @@
package adjustments
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/controllers"
adjustment "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -13,7 +13,7 @@ func AdjustmentRoutes(v1 fiber.Router, u user.UserService, s adjustment.Adjustme
ctrl := controller.NewAdjustmentController(s)
route := v1.Group("/adjustments")
route.Use(m.Auth(u))
// Standard CRUD routes following master data pattern
route.Get("/", ctrl.AdjustmentHistory) // Get all with pagination and filters
route.Post("/", ctrl.Adjustment) // Create adjustment
@@ -1,7 +1,9 @@
package service
import (
"context"
"errors"
"fmt"
"strings"
"github.com/go-playground/validator/v10"
@@ -36,8 +38,7 @@ type adjustmentService struct {
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
}
func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
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,
@@ -108,11 +109,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
}
@@ -190,6 +195,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
@@ -9,11 +9,11 @@ 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"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
)
type TransferModule struct{}
@@ -26,9 +26,10 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
stockLogsRepo := rStockLogs.NewStockLogRepository(db)
supplierRepo := rSupplier.NewSupplierRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo)
userService := sUser.NewUserService(userRepo, validate)
@@ -1,6 +1,7 @@
package service
import (
"context"
"errors"
"fmt"
"strings"
@@ -15,7 +16,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"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/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"
@@ -39,12 +40,11 @@ type transferService struct {
StockLogsRepository rStockLogs.StockLogRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
SupplierRepo rSupplier.SupplierRepository
WarehouseRepo rWarehouse.WarehouseRepository
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
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, warehouseRepo rWarehouse.WarehouseRepository,
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) 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,
@@ -56,7 +56,7 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
ProductWarehouseRepo: productWarehouseRepo,
SupplierRepo: supplierRepo,
WarehouseRepo: warehouseRepo,
projectFlockKandangRepo: projectFlockKandangRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
}
}
func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
@@ -321,10 +321,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 {
@@ -377,3 +383,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
}
@@ -4,9 +4,9 @@ import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
@@ -23,7 +23,7 @@ func NewDeliveryOrdersController(deliveryOrdersService service.DeliveryOrdersSer
}
func (u *DeliveryOrdersController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
query := &validation.DeliveryOrderQuery{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
MarketingId: uint(c.QueryInt("marketing_id", 0)),
@@ -76,7 +76,7 @@ func (u *DeliveryOrdersController) GetOne(c *fiber.Ctx) error {
}
func (u *DeliveryOrdersController) CreateOne(c *fiber.Ctx) error {
req := new(validation.Create)
req := new(validation.DeliveryOrderCreate)
if err := c.BodyParser(req); err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
@@ -97,7 +97,7 @@ func (u *DeliveryOrdersController) CreateOne(c *fiber.Ctx) error {
}
func (u *DeliveryOrdersController) UpdateOne(c *fiber.Ctx) error {
req := new(validation.Update)
req := new(validation.DeliveryOrderUpdate)
param := c.Params("id")
id, err := strconv.Atoi(param)
@@ -3,9 +3,9 @@ package controller
import (
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
@@ -1,38 +0,0 @@
package delivery_orderss
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
sDeliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
rMarketing "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
type DeliveryOrdersModule struct{}
func (DeliveryOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
marketingRepo := rMarketing.NewMarketingRepository(db)
marketingProductRepo := rMarketing.NewMarketingProductRepository(db)
marketingDeliveryProductRepo := rMarketing.NewMarketingDeliveryProductRepository(db)
userRepo := rUser.NewUserRepository(db)
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
// Register workflow steps for MARKETINGS approval
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
}
deliveryOrdersService := sDeliveryOrders.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
userService := sUser.NewUserService(userRepo, validate)
DeliveryOrdersRoutes(router, userService, deliveryOrdersService)
}
@@ -1,31 +0,0 @@
package delivery_orderss
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/controllers"
deliveryOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func DeliveryOrdersRoutes(v1 fiber.Router, u user.UserService, s deliveryOrders.DeliveryOrdersService) {
ctrl := controller.NewDeliveryOrdersController(s)
v1.Get("/", ctrl.GetAll)
v1.Get("/:id", ctrl.GetOne)
// Sisanya di group /delivery-orders
route := v1.Group("/delivery-orders")
route.Use(m.Auth(u))
// 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.Post("/", ctrl.CreateOne)
route.Patch("/:id", ctrl.UpdateOne)
}
@@ -24,7 +24,7 @@ type MarketingListDTO struct {
Customer customerDTO.CustomerRelationDTO `json:"customer"`
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
SoDocs string `json:"so_docs"`
SalesOrder []MarketingProductDTO `json:"sales_order"`
SalesOrder []DeliveryMarketingProductDTO `json:"sales_order"`
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
@@ -36,13 +36,14 @@ type MarketingDetailDTO struct {
Customer customerDTO.CustomerRelationDTO `json:"customer"`
SalesPerson userDTO.UserRelationDTO `json:"sales_person"`
SoDocs string `json:"so_docs"`
SalesOrder []MarketingProductDTO `json:"sales_order"`
SalesOrder []DeliveryMarketingProductDTO `json:"sales_order"`
DeliveryOrder []DeliveryGroupDTO `json:"delivery_order"`
CreatedUser userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LatestApproval approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
}
type MarketingDeliveryProductDTO struct {
Id uint `json:"id"`
MarketingProductId uint `json:"marketing_product_id"`
@@ -73,7 +74,7 @@ type DeliveryGroupDTO struct {
Deliveries []DeliveryItemDTO `json:"deliveries"`
}
type MarketingProductDTO struct {
type DeliveryMarketingProductDTO struct {
Id uint `json:"id"`
MarketingId uint `json:"marketing_id"`
ProductWarehouseId uint `json:"product_warehouse_id"`
@@ -95,14 +96,14 @@ func ToMarketingRelationDTO(marketing *entity.Marketing) MarketingRelationDTO {
}
}
func ToMarketingProductDTO(e entity.MarketingProduct) MarketingProductDTO {
func ToDeliveryMarketingProductDTO(e entity.MarketingProduct) DeliveryMarketingProductDTO {
var productWarehouse *productwarehouseDTO.ProductWarehousNestedDTO
if e.ProductWarehouse.Id != 0 {
mapped := productwarehouseDTO.ToProductWarehouseNestedDTO(e.ProductWarehouse)
productWarehouse = &mapped
}
return MarketingProductDTO{
return DeliveryMarketingProductDTO{
Id: e.Id,
MarketingId: e.MarketingId,
ProductWarehouseId: e.ProductWarehouseId,
@@ -155,11 +156,11 @@ func ToMarketingListDTO(marketing *entity.Marketing, deliveryProducts []entity.M
latestApproval = mapped
}
var salesOrderProducts []MarketingProductDTO
var salesOrderProducts []DeliveryMarketingProductDTO
if len(marketing.Products) > 0 {
salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products))
salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products))
for i, product := range marketing.Products {
salesOrderProducts[i] = ToMarketingProductDTO(product)
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product)
}
}
@@ -195,11 +196,11 @@ func ToMarketingDetailDTO(marketing *entity.Marketing, deliveryProducts []entity
salesPerson = mapped
}
var salesOrderProducts []MarketingProductDTO
var salesOrderProducts []DeliveryMarketingProductDTO
if len(marketing.Products) > 0 {
salesOrderProducts = make([]MarketingProductDTO, len(marketing.Products))
salesOrderProducts = make([]DeliveryMarketingProductDTO, len(marketing.Products))
for i, product := range marketing.Products {
salesOrderProducts[i] = ToMarketingProductDTO(product)
salesOrderProducts[i] = ToDeliveryMarketingProductDTO(product)
}
}
+40 -1
View File
@@ -1,13 +1,52 @@
package marketing
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
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"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
type MarketingModule struct{}
func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
RegisterRoutes(router, db, validate)
// Initialize repositories
marketingRepo := repository.NewMarketingRepository(db)
marketingProductRepo := repository.NewMarketingProductRepository(db)
marketingDeliveryProductRepo := repository.NewMarketingDeliveryProductRepository(db)
userRepo := rUser.NewUserRepository(db)
customerRepo := rCustomer.NewCustomerRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
// Initialize approval service
approvalRepo := commonRepo.NewApprovalRepository(db)
approvalSvc := commonSvc.NewApprovalService(approvalRepo)
// Register workflow steps for marketing approval
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
}
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
// Initialize services
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc,warehouseRepo,projectFlockKandangRepo, validate)
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
userService := sUser.NewUserService(userRepo, validate)
// Register routes
RegisterRoutes(router, userService, salesOrdersService, deliveryOrdersService)
}
@@ -0,0 +1,57 @@
package repository
import (
"context"
"fmt"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type MarketingRepository interface {
repository.BaseRepository[entity.Marketing]
IdExists(ctx context.Context, id uint) (bool, error)
GetNextSequence(ctx context.Context) (uint, error)
NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error)
}
type MarketingRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Marketing]
}
func NewMarketingRepository(db *gorm.DB) MarketingRepository {
return &MarketingRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Marketing](db),
}
}
func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Marketing](ctx, r.DB(), id)
}
func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, error) {
var maxID uint
if err := r.DB().WithContext(ctx).Model(&entity.Marketing{}).Select("COALESCE(MAX(id), 0)").Scan(&maxID).Error; err != nil {
return 0, err
}
return maxID + 1, nil
}
func (r *MarketingRepositoryImpl) NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
db := tx
if db == nil {
db = r.DB()
}
var soNumber string
err := db.WithContext(ctx).
Raw("SELECT generate_so_number()").
Scan(&soNumber).Error
if err != nil {
return "", fmt.Errorf("failed to generate SO number: %w", err)
}
return soNumber, nil
}
+21 -17
View File
@@ -1,27 +1,31 @@
package marketing
import (
"gitlab.com/mbugroup/lti-api.git/internal/modules"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/controllers"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
salesOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders"
deliveryOrderss "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss"
// MODULE IMPORTS
)
func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
group := router.Group("/marketing")
func RegisterRoutes(router fiber.Router, userService user.UserService, salesOrdersService service.SalesOrdersService, deliveryOrdersService service.DeliveryOrdersService) {
salesOrdersCtrl := controller.NewSalesOrdersController(salesOrdersService)
deliveryOrdersCtrl := controller.NewDeliveryOrdersController(deliveryOrdersService)
allModules := []modules.Module{
salesOrderss.SalesOrdersModule{},
deliveryOrderss.DeliveryOrdersModule{},
// MODULE REGISTRY
}
route := router.Group("/marketing")
route.Use(m.Auth(userService))
for _, m := range allModules {
m.RegisterRoutes(group, db, validate)
}
route.Get("/", deliveryOrdersCtrl.GetAll)
route.Get("/:id", deliveryOrdersCtrl.GetOne)
route.Delete("/:id", salesOrdersCtrl.DeleteOne)
route.Post("/sales-orders", salesOrdersCtrl.CreateOne)
route.Patch("/sales-orders/:id", salesOrdersCtrl.UpdateOne)
route.Post("/sales-orders/approvals", salesOrdersCtrl.Approval)
route.Get("/delivery-orders", deliveryOrdersCtrl.GetAll)
route.Get("/delivery-orders/:id", deliveryOrdersCtrl.GetOne)
route.Post("/delivery-orders", deliveryOrdersCtrl.CreateOne)
route.Patch("/delivery-orders/:id", deliveryOrdersCtrl.UpdateOne)
}
@@ -1,42 +0,0 @@
package sales_orders
import (
"fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/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"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
)
type SalesOrdersModule struct{}
func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
marketingRepo := rSalesOrders.NewMarketingRepository(db)
userRepo := rUser.NewUserRepository(db)
customerRepo := rCustomer.NewCustomerRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db))
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
}
salesOrdersService := sSalesOrders.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, warehouseRepo, projectFlockKandangRepo,validate)
userService := sUser.NewUserService(userRepo, validate)
SalesOrdersRoutes(router, userService, salesOrdersService)
}
@@ -1,122 +0,0 @@
package repository
import (
"context"
"fmt"
"strconv"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type MarketingRepository interface {
repository.BaseRepository[entity.Marketing]
IdExists(ctx context.Context, id uint) (bool, error)
GetNextSequence(ctx context.Context) (uint, error)
NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error)
}
type MarketingRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.Marketing]
}
func NewMarketingRepository(db *gorm.DB) MarketingRepository {
return &MarketingRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.Marketing](db),
}
}
func (r *MarketingRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) {
return repository.Exists[entity.Marketing](ctx, r.DB(), id)
}
func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, error) {
var maxID uint
if err := r.DB().WithContext(ctx).Model(&entity.Marketing{}).Select("COALESCE(MAX(id), 0)").Scan(&maxID).Error; err != nil {
return 0, err
}
return maxID + 1, nil
}
func (r *MarketingRepositoryImpl) NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
return r.generateSequentialNumber(ctx, tx, "so_number", utils.MarketingSoNumberPrefix, utils.MarketingNumberPadding)
}
func parseNumericSuffix(value, prefix string) (int, bool) {
if !strings.HasPrefix(value, prefix) {
return 0, false
}
suffix := strings.TrimPrefix(value, prefix)
if suffix == "" {
return 0, false
}
trimmed := strings.TrimLeft(suffix, "0")
if trimmed == "" {
trimmed = "0"
}
number, err := strconv.Atoi(trimmed)
if err != nil {
return 0, false
}
return number, true
}
func (r *MarketingRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, column, value string) (bool, error) {
var count int64
if err := db.WithContext(ctx).
Model(&entity.Marketing{}).
Where(fmt.Sprintf("%s = ?", column), value).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *MarketingRepositoryImpl) generateSequentialNumber(ctx context.Context, tx *gorm.DB, column, prefix string, padding int) (string, error) {
db := tx
if db == nil {
db = r.DB()
}
var values []string
err := db.WithContext(ctx).
Model(&entity.Marketing{}).
Where(fmt.Sprintf("%s LIKE ?", column), prefix+"%").
Select(column).
Order(fmt.Sprintf("%s DESC", column)).
Limit(20).
Clauses(clause.Locking{Strength: "UPDATE"}).
Pluck(column, &values).Error
if err != nil {
return "", err
}
next := 1
for _, value := range values {
if number, ok := parseNumericSuffix(value, prefix); ok {
next = number + 1
break
}
}
const maxAttempts = 20
for attempt := 0; attempt < maxAttempts; attempt++ {
candidate := fmt.Sprintf("%s%0*d", prefix, padding, next)
exists, err := r.numberExists(ctx, db, column, candidate)
if err != nil {
return "", err
}
if !exists {
return candidate, nil
}
next++
}
return "", fmt.Errorf("unable to generate unique %s", column)
}
@@ -1,27 +0,0 @@
package sales_orders
import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/controllers"
salesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func SalesOrdersRoutes(v1 fiber.Router, u user.UserService, s salesOrders.SalesOrdersService) {
ctrl := controller.NewSalesOrdersController(s)
v1.Delete("/:id", ctrl.DeleteOne)
route := v1.Group("/sales-orders")
route.Use(m.Auth(u))
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Post("/", ctrl.CreateOne)
route.Patch("/:id", ctrl.UpdateOne)
route.Post("/approvals", ctrl.Approval)
}
@@ -11,9 +11,9 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/dto"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/delivery-orderss/validations"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/dto"
marketingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
@@ -23,10 +23,10 @@ import (
)
type DeliveryOrdersService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error)
GetAll(ctx *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error)
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error)
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error)
CreateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error)
UpdateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error)
}
type deliveryOrdersService struct {
@@ -85,7 +85,7 @@ func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketin
return &responseDTO, nil
}
func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.MarketingListDTO, int64, error) {
func (s deliveryOrdersService) GetAll(c *fiber.Ctx, params *validation.DeliveryOrderQuery) ([]dto.MarketingListDTO, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
@@ -164,7 +164,7 @@ func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDeta
return &responseDTO, nil
}
func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*dto.MarketingDetailDTO, error) {
func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
@@ -293,7 +293,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
return s.getMarketingWithDeliveries(c, req.MarketingId)
}
func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*dto.MarketingDetailDTO, error) {
func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error) {
if err := s.Validate.Struct(req); err != nil {
return nil, err
}
@@ -11,8 +11,8 @@ import (
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/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"
@@ -11,22 +11,22 @@ type DeliveryProduct struct {
VehicleNumber string `json:"vehicle_number" validate:"omitempty,max=50"`
}
type Create struct {
type DeliveryOrderCreate struct {
MarketingId uint `json:"marketing_id" validate:"required,gt=0"`
DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"required,min=1,dive"`
}
type Update struct {
type DeliveryOrderUpdate struct {
DeliveryProducts []DeliveryProduct `json:"delivery_products" validate:"omitempty,min=1,dive"`
}
type Query struct {
type DeliveryOrderQuery struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
MarketingId uint `query:"marketing_id" validate:"omitempty,gt=0"`
}
type Approve struct {
type DeliveryOrderApprove struct {
Action string `json:"action" validate:"required_strict"`
ApprovableIds []uint `json:"approvable_ids" validate:"required_strict,min=1,dive,gt=0"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
@@ -11,6 +11,7 @@ import (
type ProjectChickinRepository interface {
repository.BaseRepository[entity.ProjectChickin]
GetFirstByProjectFlockID(ctx context.Context, projectFlockID uint) (*entity.ProjectChickin, error)
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectChickin, error)
GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
GetPendingByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error)
GetTotalPendingUsageQtyByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) (float64, error)
@@ -40,6 +41,16 @@ func (r *ChickinRepositoryImpl) GetFirstByProjectFlockID(ctx context.Context, pr
return &chickin, nil
}
func (r *ChickinRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectChickin, error) {
var chickins []entity.ProjectChickin
err := r.db.WithContext(ctx).
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = project_chickins.project_flock_kandang_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
Order("project_chickins.created_at DESC").
Find(&chickins).Error
return chickins, err
}
func (r *ChickinRepositoryImpl) GetByProjectFlockKandangID(ctx context.Context, projectFlockKandangID uint) ([]entity.ProjectChickin, error) {
var chickins []entity.ProjectChickin
err := r.db.WithContext(ctx).
@@ -197,7 +197,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
if category == string(utils.ProjectFlockCategoryLaying) {
for _, chickin := range newChikins {
updates := map[string]any{"quantity": gorm.Expr("quantity - ?", chickin.PendingUsageQty)}
updates := map[string]any{"qty": gorm.Expr("qty - ?", chickin.PendingUsageQty)}
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -498,7 +498,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
for _, chickin := range chickins {
if categoryForRejection == string(utils.ProjectFlockCategoryGrowing) {
updates := map[string]any{"quantity": gorm.Expr("quantity + ?", chickin.PendingUsageQty)}
updates := map[string]any{"qty": gorm.Expr("qty + ?", chickin.PendingUsageQty)}
if err := productWarehouseTx.PatchOne(c.Context(), chickin.ProductWarehouseId, updates, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -600,7 +600,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
if chickin.ProductWarehouseId != targetPW.Id {
if err := productWarehouseTx.PatchOne(ctx.Context(), chickin.ProductWarehouseId, map[string]any{
"quantity": gorm.Expr("quantity - ?", quantityToConvert),
"qty": gorm.Expr("qty - ?", quantityToConvert),
}, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Source product warehouse %d not found", chickin.ProductWarehouseId))
@@ -610,7 +610,7 @@ func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []enti
}
if err := productWarehouseTx.PatchOne(ctx.Context(), targetPW.Id, map[string]any{
"quantity": gorm.Expr("quantity + ?", quantityToConvert),
"qty": gorm.Expr("qty + ?", quantityToConvert),
}, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Target product warehouse %d not found", targetPW.Id))
@@ -1,6 +1,8 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
@@ -8,6 +10,7 @@ import (
type ProjectBudgetRepository interface {
repository.BaseRepository[entity.ProjectBudget]
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectBudget, error)
}
type ProjectBudgetRepositoryImpl struct {
@@ -21,3 +24,13 @@ func NewProjectBudgetRepository(db *gorm.DB) ProjectBudgetRepository {
db: db,
}
}
func (r *ProjectBudgetRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectBudget, error) {
var budgets []entity.ProjectBudget
err := r.db.WithContext(ctx).
Where("project_flock_id = ?", projectFlockID).
Preload("Nonstock").
Preload("Nonstock.Uom").
Find(&budgets).Error
return budgets, err
}
@@ -0,0 +1,98 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type RepportController struct {
RepportService service.RepportService
}
func NewRepportController(repportService service.RepportService) *RepportController {
return &RepportController{
RepportService: repportService,
}
}
func (c *RepportController) GetAll(ctx *fiber.Ctx) error {
query := &validation.Query{
Page: ctx.QueryInt("page", 1),
Limit: ctx.QueryInt("limit", 10),
Search: ctx.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := c.RepportService.GetAll(ctx, query)
if err != nil {
return err
}
return ctx.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.RepportListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all reports successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: result,
})
}
func (c *RepportController) GetOne(ctx *fiber.Ctx) error {
param := ctx.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := c.RepportService.GetOne(ctx, uint(id))
if err != nil {
return err
}
return ctx.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get report successfully",
Data: result,
})
}
func (c *RepportController) GetExpense(ctx *fiber.Ctx) error {
param := ctx.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := c.RepportService.GetOne(ctx, uint(id))
if err != nil {
return err
}
return ctx.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get report successfully",
Data: result,
})
}
@@ -0,0 +1,16 @@
package dto
import "time"
// === DTO Structs ===
type RepportListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type RepportDetailDTO struct {
RepportListDTO
}
+23
View File
@@ -0,0 +1,23 @@
package repports
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
sRepport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
)
type RepportModule struct{}
func (RepportModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
// Initialize expense realization repository
expRealizationRepo := expenseRepo.NewExpenseRealizationRepository(db)
// Initialize report service with expense realization repo
repportService := sRepport.NewRepportService(validate, expRealizationRepo)
RepportRoutes(router, repportService)
}
+20
View File
@@ -0,0 +1,20 @@
package repports
import (
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/controllers"
repport "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/services"
"github.com/gofiber/fiber/v2"
)
func RepportRoutes(v1 fiber.Router, s repport.RepportService) {
ctrl := controller.NewRepportController(s)
route := v1.Group("/repports")
route.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne)
route.Get("expense", ctrl.GetExpense)
}
@@ -0,0 +1,106 @@
package service
import (
"strings"
"time"
"gitlab.com/mbugroup/lti-api.git/internal/modules/repports/dto"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/repports/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
)
type RepportService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]dto.RepportListDTO, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*dto.RepportListDTO, error)
GetExpense(ctx *fiber.Ctx, id uint) (*dto.RepportListDTO, error)
}
type repportService struct {
Log *logrus.Logger
Validate *validator.Validate
dummyData map[uint]dto.RepportListDTO
ExpenseRealizationRepo expenseRepo.ExpenseRealizationRepository
}
func NewRepportService(validate *validator.Validate, expenseRealizationRepo expenseRepo.ExpenseRealizationRepository) RepportService {
// Initialize with dummy data
now := time.Now()
dummyData := map[uint]dto.RepportListDTO{
1: {
Id: 1,
Name: "Sales Report",
CreatedAt: now,
UpdatedAt: now,
},
2: {
Id: 2,
Name: "Inventory Report",
CreatedAt: now,
UpdatedAt: now,
},
3: {
Id: 3,
Name: "Production Report",
CreatedAt: now,
UpdatedAt: now,
},
}
return &repportService{
Log: utils.Log,
Validate: validate,
dummyData: dummyData,
ExpenseRealizationRepo: expenseRealizationRepo,
}
}
func (s *repportService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.RepportListDTO, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
// Convert map to slice
var results []dto.RepportListDTO
for _, v := range s.dummyData {
// Apply search filter if provided
if params.Search != "" && !strings.Contains(strings.ToLower(v.Name), strings.ToLower(params.Search)) {
continue
}
results = append(results, v)
}
// Apply pagination
total := int64(len(results))
offset := (params.Page - 1) * params.Limit
if offset >= int(total) {
return []dto.RepportListDTO{}, total, nil
}
end := offset + params.Limit
if end > int(total) {
end = int(total)
}
return results[offset:end], total, nil
}
func (s *repportService) GetOne(c *fiber.Ctx, id uint) (*dto.RepportListDTO, error) {
if data, ok := s.dummyData[id]; ok {
return &data, nil
}
return nil, fiber.NewError(fiber.StatusNotFound, "Report not found")
}
func (s *repportService) GetExpense(c *fiber.Ctx, id uint) (*dto.RepportListDTO, error) {
if data, ok := s.dummyData[id]; ok {
return &data, nil
}
return nil, fiber.NewError(fiber.StatusNotFound, "Report not found")
}
@@ -0,0 +1,7 @@
package validation
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}