mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
1429 lines
51 KiB
Go
1429 lines
51 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"strings"
|
|
"time"
|
|
|
|
"gitlab.com/mbugroup/lti-api.git/internal/common/exportprogress"
|
|
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
|
fifoV2 "gitlab.com/mbugroup/lti-api.git/internal/common/service/fifo_stock_v2"
|
|
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"
|
|
"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"
|
|
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
|
rShared "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
|
|
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
|
|
|
"github.com/go-playground/validator/v10"
|
|
"github.com/gofiber/fiber/v2"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type DeliveryOrdersService interface {
|
|
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.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error)
|
|
UpdateOne(ctx *fiber.Ctx, req *validation.DeliveryOrderUpdate, id uint) (*dto.MarketingDetailDTO, error)
|
|
BulkApproveToStatus(ctx *fiber.Ctx, req *validation.BulkApprovalRequest, target approvalutils.ApprovalStep) ([]dto.MarketingDetailDTO, error)
|
|
GetProgressRows(ctx *fiber.Ctx, query *exportprogress.Query) ([]exportprogress.Row, error)
|
|
}
|
|
|
|
type deliveryOrdersService struct {
|
|
Validate *validator.Validate
|
|
MarketingRepo marketingRepo.MarketingRepository
|
|
MarketingProductRepo marketingRepo.MarketingProductRepository
|
|
MarketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository
|
|
StockLogRepo rShared.StockLogRepository
|
|
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
|
ProjectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
|
|
ApprovalSvc commonSvc.ApprovalService
|
|
FifoStockV2Svc commonSvc.FifoStockV2Service
|
|
}
|
|
|
|
func NewDeliveryOrdersService(
|
|
marketingRepo marketingRepo.MarketingRepository,
|
|
marketingProductRepo marketingRepo.MarketingProductRepository,
|
|
marketingDeliveryProductRepo marketingRepo.MarketingDeliveryProductRepository,
|
|
stockLogRepo rShared.StockLogRepository,
|
|
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
|
|
projectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository,
|
|
approvalSvc commonSvc.ApprovalService,
|
|
fifoStockV2Svc commonSvc.FifoStockV2Service,
|
|
validate *validator.Validate,
|
|
) DeliveryOrdersService {
|
|
return &deliveryOrdersService{
|
|
Validate: validate,
|
|
MarketingRepo: marketingRepo,
|
|
MarketingProductRepo: marketingProductRepo,
|
|
MarketingDeliveryProductRepo: marketingDeliveryProductRepo,
|
|
StockLogRepo: stockLogRepo,
|
|
ProductWarehouseRepo: productWarehouseRepo,
|
|
ProjectFlockPopulationRepo: projectFlockPopulationRepo,
|
|
ApprovalSvc: approvalSvc,
|
|
FifoStockV2Svc: fifoStockV2Svc,
|
|
}
|
|
}
|
|
|
|
func (s deliveryOrdersService) withRelations(db *gorm.DB) *gorm.DB {
|
|
return db.
|
|
Preload("CreatedUser").
|
|
Preload("Customer").
|
|
Preload("SalesPerson").
|
|
Preload("Products.ProductWarehouse.Product").
|
|
Preload("Products.ProductWarehouse.Product.Uom").
|
|
Preload("Products.ProductWarehouse.Warehouse").
|
|
Preload("Products.DeliveryProduct")
|
|
}
|
|
|
|
func (s deliveryOrdersService) marketingOwnerRelationQuery(ctx context.Context) *gorm.DB {
|
|
return s.MarketingRepo.DB().
|
|
WithContext(ctx).
|
|
Table("marketing_products mp").
|
|
Select("1").
|
|
Joins("JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id").
|
|
Joins("LEFT JOIN project_flock_kandangs pfk ON pfk.id = pw.project_flock_kandang_id").
|
|
Joins("LEFT JOIN project_flocks pf ON pf.id = pfk.project_flock_id").
|
|
Joins("LEFT JOIN warehouses w ON w.id = pw.warehouse_id").
|
|
Joins("LEFT JOIN kandangs k ON k.id = COALESCE(pfk.kandang_id, w.kandang_id)").
|
|
Where("mp.marketing_id = marketings.id")
|
|
}
|
|
|
|
func (s deliveryOrdersService) marketingAttributionRelationQuery(ctx context.Context) *gorm.DB {
|
|
baseDB := s.MarketingRepo.DB().WithContext(ctx)
|
|
return baseDB.
|
|
Table("marketing_delivery_products mdp").
|
|
Select("1").
|
|
Joins("JOIN marketing_products mp ON mp.id = mdp.marketing_product_id").
|
|
Joins("JOIN (?) AS mda ON mda.marketing_delivery_product_id = mdp.id", commonRepo.MarketingDeliveryAttributionRowsQuery(baseDB)).
|
|
Joins("JOIN project_flock_kandangs pfk_attr ON pfk_attr.id = mda.project_flock_kandang_id").
|
|
Joins("JOIN project_flocks pf_attr ON pf_attr.id = pfk_attr.project_flock_id").
|
|
Joins("JOIN kandangs k_attr ON k_attr.id = pfk_attr.kandang_id").
|
|
Where("mp.marketing_id = marketings.id")
|
|
}
|
|
|
|
func (s deliveryOrdersService) applyMarketingProjectFlockFilter(ctx context.Context, db *gorm.DB, projectFlockID, projectFlockKandangID uint) *gorm.DB {
|
|
if projectFlockID > 0 {
|
|
db = db.Where(
|
|
"(EXISTS (?) OR EXISTS (?))",
|
|
s.marketingOwnerRelationQuery(ctx).Where("pfk.project_flock_id = ?", projectFlockID),
|
|
s.marketingAttributionRelationQuery(ctx).Where("mda.project_flock_id = ?", projectFlockID),
|
|
)
|
|
}
|
|
|
|
if projectFlockKandangID > 0 {
|
|
db = db.Where(
|
|
"(EXISTS (?) OR EXISTS (?))",
|
|
s.marketingOwnerRelationQuery(ctx).Where("pw.project_flock_kandang_id = ?", projectFlockKandangID),
|
|
s.marketingAttributionRelationQuery(ctx).Where("mda.project_flock_kandang_id = ?", projectFlockKandangID),
|
|
)
|
|
}
|
|
|
|
return db
|
|
}
|
|
|
|
func (s deliveryOrdersService) applyMarketingSearchFilter(ctx context.Context, db *gorm.DB, rawSearch string) *gorm.DB {
|
|
searchPattern := "%" + strings.TrimSpace(rawSearch) + "%"
|
|
if searchPattern == "%%" {
|
|
return db
|
|
}
|
|
|
|
return db.Where(
|
|
`(
|
|
marketings.so_number ILIKE ? OR
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM customers c
|
|
WHERE c.id = marketings.customer_id
|
|
AND c.name ILIKE ?
|
|
) OR
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM users su
|
|
WHERE su.id = marketings.sales_person_id
|
|
AND su.name ILIKE ?
|
|
) OR
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM marketing_products mp
|
|
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
|
JOIN products p ON p.id = pw.product_id
|
|
WHERE mp.marketing_id = marketings.id
|
|
AND p.name ILIKE ?
|
|
) OR
|
|
EXISTS (
|
|
SELECT 1
|
|
FROM marketing_products mp
|
|
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
|
JOIN warehouses w ON w.id = pw.warehouse_id
|
|
WHERE mp.marketing_id = marketings.id
|
|
AND w.name ILIKE ?
|
|
) OR
|
|
EXISTS (?) OR
|
|
EXISTS (?)
|
|
)`,
|
|
searchPattern,
|
|
searchPattern,
|
|
searchPattern,
|
|
searchPattern,
|
|
searchPattern,
|
|
s.marketingOwnerRelationQuery(ctx).Where("pf.flock_name ILIKE ? OR k.name ILIKE ?", searchPattern, searchPattern),
|
|
s.marketingAttributionRelationQuery(ctx).Where("pf_attr.flock_name ILIKE ? OR k_attr.name ILIKE ?", searchPattern, searchPattern),
|
|
)
|
|
}
|
|
|
|
func (s deliveryOrdersService) getMarketingWithDeliveries(c *fiber.Ctx, marketingId uint) (*dto.MarketingDetailDTO, error) {
|
|
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), marketingId); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
marketing, err := s.MarketingRepo.GetByID(c.Context(), marketingId, s.withRelations)
|
|
if err != nil {
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing")
|
|
}
|
|
|
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketingId, nil)
|
|
if err != nil {
|
|
}
|
|
marketing.LatestApproval = latestApproval
|
|
|
|
allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), marketingId)
|
|
if err != nil {
|
|
allDeliveryProducts = []entity.MarketingDeliveryProduct{}
|
|
}
|
|
|
|
responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts)
|
|
return &responseDTO, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
scope, err := m.ResolveLocationScope(c, s.MarketingRepo.DB())
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
offset := (params.Page - 1) * params.Limit
|
|
|
|
marketings, total, err := s.MarketingRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
|
db = db.
|
|
Preload("CreatedUser").
|
|
Preload("Customer").
|
|
Preload("SalesPerson").
|
|
Preload("Products.ProductWarehouse.Product").
|
|
Preload("Products.ProductWarehouse.Product.Uom").
|
|
Preload("Products.ProductWarehouse.Warehouse").
|
|
Preload("Products.DeliveryProduct")
|
|
|
|
if params.Status != "" {
|
|
status := strings.TrimSpace(params.Status)
|
|
latestApprovalSubQuery := s.MarketingRepo.DB().
|
|
WithContext(c.Context()).
|
|
Table("approvals").
|
|
Select("DISTINCT ON (approvable_id) approvable_id, step_name, action").
|
|
Where("approvable_type = ?", utils.ApprovalWorkflowMarketing.String()).
|
|
Order("approvable_id, id DESC")
|
|
|
|
if strings.EqualFold(status, "DITOLAK") {
|
|
db = db.Where(`EXISTS (
|
|
SELECT 1
|
|
FROM (?) AS latest_approval
|
|
WHERE latest_approval.approvable_id = marketings.id
|
|
AND latest_approval.action = ?
|
|
)`, latestApprovalSubQuery, string(entity.ApprovalActionRejected))
|
|
} else {
|
|
db = db.Where(`EXISTS (
|
|
SELECT 1
|
|
FROM (?) AS latest_approval
|
|
WHERE latest_approval.approvable_id = marketings.id
|
|
AND LOWER(latest_approval.step_name) = LOWER(?)
|
|
AND (latest_approval.action IS NULL OR latest_approval.action <> ?)
|
|
)`, latestApprovalSubQuery, status, string(entity.ApprovalActionRejected))
|
|
}
|
|
}
|
|
|
|
if len(params.ProductIDs) > 0 {
|
|
db = db.Where(`EXISTS (
|
|
SELECT 1
|
|
FROM marketing_products mp
|
|
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
|
JOIN products p ON p.id = pw.product_id
|
|
WHERE mp.marketing_id = marketings.id
|
|
AND p.id IN ?
|
|
)`, params.ProductIDs)
|
|
}
|
|
|
|
if params.CustomerId != 0 {
|
|
db = db.Where("marketings.customer_id = ?", params.CustomerId)
|
|
}
|
|
|
|
db = s.applyMarketingProjectFlockFilter(c.Context(), db, params.ProjectFlockID, params.ProjectFlockKandangID)
|
|
db = s.applyMarketingSearchFilter(c.Context(), db, params.Search)
|
|
|
|
if scope.Restrict {
|
|
if len(scope.IDs) == 0 {
|
|
return db.Where("1 = 0")
|
|
}
|
|
db = db.Where(
|
|
`EXISTS (
|
|
SELECT 1
|
|
FROM marketing_products mp
|
|
JOIN product_warehouses pw ON pw.id = mp.product_warehouse_id
|
|
JOIN warehouses w ON w.id = pw.warehouse_id
|
|
WHERE mp.marketing_id = marketings.id
|
|
AND w.location_id IN ?
|
|
)`,
|
|
scope.IDs,
|
|
)
|
|
}
|
|
|
|
if params.MarketingId != 0 {
|
|
return db.Where("id = ?", params.MarketingId)
|
|
}
|
|
|
|
orderDir := "DESC"
|
|
if params.SortOrder != "" {
|
|
orderDir = strings.ToUpper(params.SortOrder)
|
|
}
|
|
|
|
switch strings.TrimSpace(params.SortBy) {
|
|
case "so_number":
|
|
return db.Order("marketings.so_number " + orderDir)
|
|
case "so_date":
|
|
return db.Order("marketings.so_date " + orderDir)
|
|
case "status":
|
|
statusSQL := "(SELECT step_name FROM approvals WHERE approvable_type = '" + utils.ApprovalWorkflowMarketing.String() + "' AND approvable_id = marketings.id ORDER BY action_at DESC, id DESC LIMIT 1) " + orderDir
|
|
return db.Order(statusSQL)
|
|
case "customer":
|
|
return db.Joins("LEFT JOIN customers ON customers.id = marketings.customer_id").Order("COALESCE(customers.name, '') " + orderDir)
|
|
case "grand_total":
|
|
return db.Order("(SELECT COALESCE(SUM(mp.total_price), 0) FROM marketing_products mp WHERE mp.marketing_id = marketings.id) " + orderDir)
|
|
default:
|
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
|
}
|
|
})
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
for i := range marketings {
|
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketings[i].Id, func(db *gorm.DB) *gorm.DB {
|
|
return db.Preload("ActionUser")
|
|
})
|
|
if err != nil {
|
|
continue
|
|
}
|
|
marketings[i].LatestApproval = latestApproval
|
|
}
|
|
|
|
result := make([]dto.MarketingListDTO, len(marketings))
|
|
for i, marketing := range marketings {
|
|
result[i] = dto.ToMarketingListDTO(&marketing, []entity.MarketingDeliveryProduct{})
|
|
}
|
|
|
|
return result, total, nil
|
|
}
|
|
|
|
func (s deliveryOrdersService) GetProgressRows(c *fiber.Ctx, query *exportprogress.Query) ([]exportprogress.Row, error) {
|
|
scope, err := m.ResolveLocationScope(c, s.MarketingRepo.DB())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return s.MarketingRepo.GetProgressRows(c.Context(), query.StartDate, query.EndDate, scope.IDs, scope.Restrict)
|
|
}
|
|
|
|
func (s deliveryOrdersService) GetOne(c *fiber.Ctx, id uint) (*dto.MarketingDetailDTO, error) {
|
|
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
marketing, err := s.MarketingRepo.GetByID(c.Context(), id, s.withRelations)
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, fiber.NewError(fiber.StatusNotFound, "Marketing not found")
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
allDeliveryProducts, err := s.MarketingDeliveryProductRepo.GetByMarketingId(c.Context(), id)
|
|
if err != nil {
|
|
allDeliveryProducts = []entity.MarketingDeliveryProduct{}
|
|
}
|
|
|
|
if s.ApprovalSvc != nil {
|
|
approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), utils.ApprovalWorkflowMarketing, marketing.Id, func(db *gorm.DB) *gorm.DB {
|
|
return db.Preload("ActionUser")
|
|
})
|
|
if err != nil {
|
|
} else if len(approvals) > 0 {
|
|
if marketing.LatestApproval == nil {
|
|
latest := approvals[len(approvals)-1]
|
|
marketing.LatestApproval = &latest
|
|
}
|
|
} else {
|
|
marketing.LatestApproval = nil
|
|
}
|
|
}
|
|
|
|
responseDTO := dto.ToMarketingDetailDTO(marketing, allDeliveryProducts)
|
|
return &responseDTO, nil
|
|
}
|
|
|
|
func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.DeliveryOrderCreate) (*dto.MarketingDetailDTO, error) {
|
|
if err := s.Validate.Struct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), req.MarketingId); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := commonSvc.EnsureRelations(c.Context(),
|
|
commonSvc.RelationCheck{Name: "Marketing", ID: &req.MarketingId, Exists: s.MarketingRepo.IdExists},
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
actorID, err := m.ActorIDFromContext(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB()))
|
|
|
|
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil)
|
|
if err != nil {
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
|
}
|
|
if latestApproval == nil {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing has not been submitted for approval")
|
|
}
|
|
if latestApproval.StepNumber < uint16(utils.MarketingStepSalesOrder) {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Marketing must be approved to Sales Order step before creating delivery order")
|
|
}
|
|
if latestApproval.StepNumber >= uint16(utils.MarketingDeliveryOrder) {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery order already exists for this marketing")
|
|
}
|
|
|
|
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
|
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
|
|
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
|
marketingRepoTx := marketingRepo.NewMarketingRepository(dbTransaction)
|
|
|
|
marketing, err := marketingRepoTx.GetByID(c.Context(), req.MarketingId, nil)
|
|
if err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing")
|
|
}
|
|
|
|
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), req.MarketingId)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("No marketing products found for marketing %d", req.MarketingId))
|
|
}
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products")
|
|
}
|
|
|
|
for _, requestedProduct := range req.DeliveryProducts {
|
|
var foundMarketingProduct *entity.MarketingProduct
|
|
for i := range allMarketingProducts {
|
|
if allMarketingProducts[i].Id == requestedProduct.MarketingProductId {
|
|
foundMarketingProduct = &allMarketingProducts[i]
|
|
break
|
|
}
|
|
}
|
|
if foundMarketingProduct == nil {
|
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
|
}
|
|
|
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
|
c.Context(),
|
|
dbTransaction,
|
|
[]uint{foundMarketingProduct.ProductWarehouseId},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", requestedProduct.MarketingProductId))
|
|
}
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product")
|
|
}
|
|
|
|
var itemDeliveryDate *time.Time
|
|
if requestedProduct.DeliveryDate != "" {
|
|
parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate)
|
|
if err != nil {
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err))
|
|
}
|
|
itemDeliveryDate = &parsedDate
|
|
}
|
|
|
|
totalWeight, totalPrice := s.resolveDeliveryTotals(marketing.MarketingType, requestedProduct, foundMarketingProduct)
|
|
|
|
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
|
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
|
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
|
|
deliveryProduct.WeightPerConvertion = requestedProduct.WeightPerConvertion
|
|
deliveryProduct.TotalWeight = totalWeight
|
|
deliveryProduct.TotalPrice = totalPrice
|
|
deliveryProduct.DeliveryDate = itemDeliveryDate
|
|
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
|
|
|
|
if requestedProduct.Qty > 0 {
|
|
if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty, actorID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
|
|
}
|
|
|
|
}
|
|
|
|
approvalAction := entity.ApprovalActionApproved
|
|
if _, err := approvalSvcTx.CreateApproval(
|
|
c.Context(),
|
|
utils.ApprovalWorkflowMarketing,
|
|
req.MarketingId,
|
|
utils.MarketingDeliveryOrder,
|
|
&approvalAction,
|
|
actorID,
|
|
nil); err != nil {
|
|
if !errors.Is(err, gorm.ErrDuplicatedKey) {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order approval")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
|
return nil, fiberErr
|
|
}
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create delivery order")
|
|
}
|
|
|
|
return s.getMarketingWithDeliveries(c, req.MarketingId)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := commonSvc.EnsureRelations(c.Context(),
|
|
commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists},
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
actorID, err := m.ActorIDFromContext(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
|
marketingProductRepositoryTx := marketingRepo.NewMarketingProductRepository(dbTransaction)
|
|
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(dbTransaction)
|
|
marketingRepoTx := marketingRepo.NewMarketingRepository(dbTransaction)
|
|
|
|
marketing, err := marketingRepoTx.GetByID(c.Context(), id, nil)
|
|
if err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing")
|
|
}
|
|
|
|
allMarketingProducts, err := marketingProductRepositoryTx.GetByMarketingID(c.Context(), id)
|
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing products")
|
|
}
|
|
|
|
if len(req.DeliveryProducts) > 0 {
|
|
for _, requestedProduct := range req.DeliveryProducts {
|
|
|
|
var foundMarketingProduct *entity.MarketingProduct
|
|
for i := range allMarketingProducts {
|
|
if allMarketingProducts[i].Id == requestedProduct.MarketingProductId {
|
|
foundMarketingProduct = &allMarketingProducts[i]
|
|
break
|
|
}
|
|
}
|
|
if foundMarketingProduct == nil {
|
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
|
}
|
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
|
c.Context(),
|
|
dbTransaction,
|
|
[]uint{foundMarketingProduct.ProductWarehouseId},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", requestedProduct.MarketingProductId))
|
|
}
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product")
|
|
}
|
|
|
|
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
|
|
var itemDeliveryDate *time.Time
|
|
if requestedProduct.DeliveryDate != "" {
|
|
parsedDate, err := utils.ParseDateString(requestedProduct.DeliveryDate)
|
|
if err != nil {
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid delivery date format for product %d: %v", requestedProduct.MarketingProductId, err))
|
|
}
|
|
itemDeliveryDate = &parsedDate
|
|
} else if deliveryProduct.DeliveryDate != nil {
|
|
itemDeliveryDate = deliveryProduct.DeliveryDate
|
|
}
|
|
|
|
totalWeight, totalPrice := s.resolveDeliveryTotals(marketing.MarketingType, requestedProduct, foundMarketingProduct)
|
|
|
|
deliveryProduct.ProductWarehouseId = foundMarketingProduct.ProductWarehouseId
|
|
deliveryProduct.UnitPrice = requestedProduct.UnitPrice
|
|
deliveryProduct.AvgWeight = requestedProduct.AvgWeight
|
|
deliveryProduct.WeightPerConvertion = requestedProduct.WeightPerConvertion
|
|
deliveryProduct.TotalWeight = totalWeight
|
|
deliveryProduct.TotalPrice = totalPrice
|
|
deliveryProduct.DeliveryDate = itemDeliveryDate
|
|
deliveryProduct.VehicleNumber = requestedProduct.VehicleNumber
|
|
|
|
if requestedProduct.Qty != oldRequestedQty {
|
|
|
|
if oldRequestedQty > 0 {
|
|
if err := s.releaseDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, actorID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if requestedProduct.Qty > 0 {
|
|
if err := s.consumeDeliveryStock(c.Context(), dbTransaction, deliveryProduct, foundMarketingProduct, requestedProduct.Qty, actorID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := marketingDeliveryProductRepositoryTx.UpdateOne(c.Context(), deliveryProduct.Id, deliveryProduct, nil); err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
|
return nil, fiberErr
|
|
}
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery order")
|
|
}
|
|
|
|
return s.getMarketingWithDeliveries(c, id)
|
|
}
|
|
|
|
func (s deliveryOrdersService) BulkApproveToStatus(c *fiber.Ctx, req *validation.BulkApprovalRequest, target approvalutils.ApprovalStep) ([]dto.MarketingDetailDTO, error) {
|
|
if err := s.Validate.Struct(req); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
approvableIDs := utils.UniqueUintSlice(req.ApprovableIds)
|
|
if len(approvableIDs) == 0 {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
|
|
}
|
|
|
|
for _, id := range approvableIDs {
|
|
if err := m.EnsureMarketingAccess(c, s.MarketingRepo.DB(), id); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
actorID, err := m.ActorIDFromContext(c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var deliveryDate time.Time
|
|
if req.RequiresDate(target) {
|
|
deliveryDate, err = utils.ParseDateString(req.Date)
|
|
if err != nil {
|
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format")
|
|
}
|
|
}
|
|
|
|
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
|
|
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
|
marketingRepoTx := marketingRepo.NewMarketingRepository(tx)
|
|
|
|
for _, id := range approvableIDs {
|
|
if err := commonSvc.EnsureRelations(c.Context(),
|
|
commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: marketingRepoTx.IdExists},
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
marketing, err := marketingRepoTx.GetByID(c.Context(), id, s.withRelations)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing %d not found", id))
|
|
}
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch marketing")
|
|
}
|
|
|
|
latestApproval, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, id, nil)
|
|
if err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
|
}
|
|
if latestApproval == nil {
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for Marketing %d", id))
|
|
}
|
|
if latestApproval.Action != nil && *latestApproval.Action == entity.ApprovalActionRejected {
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d is rejected and cannot be bulk approved", id))
|
|
}
|
|
|
|
currentStep := approvalutils.ApprovalStep(latestApproval.StepNumber)
|
|
if currentStep >= target {
|
|
currentStepName := utils.MarketingApprovalSteps[currentStep]
|
|
targetStepName := utils.MarketingApprovalSteps[target]
|
|
if currentStep == target {
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d is already at %s step", id, targetStepName))
|
|
}
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing %d is already beyond %s step (current step: %s)", id, targetStepName, currentStepName))
|
|
}
|
|
|
|
if len(marketing.Products) > 0 {
|
|
pwIDs := make([]uint, 0, len(marketing.Products))
|
|
for _, product := range marketing.Products {
|
|
if product.ProductWarehouseId != 0 {
|
|
pwIDs = append(pwIDs, product.ProductWarehouseId)
|
|
}
|
|
}
|
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), tx, pwIDs); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
for step := currentStep + 1; step <= target; step++ {
|
|
if step == utils.MarketingDeliveryOrder {
|
|
if err := s.createDeliveryFromMarketingProducts(c.Context(), tx, marketing, deliveryDate, actorID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
approvalAction := entity.ApprovalActionApproved
|
|
if _, err := approvalSvcTx.CreateApproval(
|
|
c.Context(),
|
|
utils.ApprovalWorkflowMarketing,
|
|
id,
|
|
step,
|
|
&approvalAction,
|
|
actorID,
|
|
req.Notes,
|
|
); err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
|
return nil, fiberErr
|
|
}
|
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to bulk approve marketings")
|
|
}
|
|
|
|
results := make([]dto.MarketingDetailDTO, 0, len(approvableIDs))
|
|
for _, id := range approvableIDs {
|
|
result, err := s.getMarketingWithDeliveries(c, id)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
results = append(results, *result)
|
|
}
|
|
|
|
return results, nil
|
|
}
|
|
|
|
func (s deliveryOrdersService) createDeliveryFromMarketingProducts(
|
|
ctx context.Context,
|
|
tx *gorm.DB,
|
|
marketing *entity.Marketing,
|
|
deliveryDate time.Time,
|
|
actorID uint,
|
|
) error {
|
|
if marketing == nil {
|
|
return fiber.NewError(fiber.StatusBadRequest, "Marketing not found")
|
|
}
|
|
if tx == nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Transaction is required")
|
|
}
|
|
|
|
marketingDeliveryProductRepositoryTx := marketingRepo.NewMarketingDeliveryProductRepository(tx)
|
|
|
|
for _, marketingProduct := range marketing.Products {
|
|
deliveryProduct := marketingProduct.DeliveryProduct
|
|
if deliveryProduct == nil {
|
|
record, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(ctx, marketingProduct.Id)
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Delivery product for marketing product %d not found", marketingProduct.Id))
|
|
}
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch delivery product")
|
|
}
|
|
deliveryProduct = record
|
|
}
|
|
|
|
oldRequestedQty := deliveryProduct.UsageQty + deliveryProduct.PendingQty
|
|
deliveryDateCopy := deliveryDate
|
|
|
|
deliveryProduct.ProductWarehouseId = marketingProduct.ProductWarehouseId
|
|
deliveryProduct.UnitPrice = marketingProduct.UnitPrice
|
|
deliveryProduct.AvgWeight = marketingProduct.AvgWeight
|
|
deliveryProduct.WeightPerConvertion = marketingProduct.WeightPerConvertion
|
|
deliveryProduct.TotalWeight = marketingProduct.TotalWeight
|
|
deliveryProduct.TotalPrice = marketingProduct.TotalPrice
|
|
deliveryProduct.DeliveryDate = &deliveryDateCopy
|
|
|
|
requestedQty := marketingProduct.Qty
|
|
if requestedQty != oldRequestedQty {
|
|
if oldRequestedQty > 0 {
|
|
if err := s.releaseDeliveryStock(ctx, tx, deliveryProduct, &marketingProduct, actorID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if requestedQty > 0 {
|
|
if err := s.consumeDeliveryStock(ctx, tx, deliveryProduct, &marketingProduct, requestedQty, actorID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if err := marketingDeliveryProductRepositoryTx.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s *deliveryOrdersService) calculatePriceByMarketingType(marketingType string, qty, avgWeight, unitPrice float64, week *int, convertionUnit *string, _ *float64) (totalWeight, totalPrice float64) {
|
|
if marketingType == string(utils.MarketingTypeTrading) {
|
|
totalWeight = 0
|
|
totalPrice = math.Round(qty*unitPrice*100) / 100
|
|
} else if marketingType == string(utils.MarketingTypeAyamPullet) && week != nil && *week > 0 {
|
|
totalWeight = math.Round(qty*avgWeight*100) / 100
|
|
totalPrice = math.Round(unitPrice*float64(*week)*qty*100) / 100
|
|
} else {
|
|
totalWeight = math.Round(qty*avgWeight*100) / 100
|
|
|
|
if marketingType == string(utils.MarketingTypeTelur) && convertionUnit != nil {
|
|
switch *convertionUnit {
|
|
case string(utils.ConvertionUnitQty):
|
|
totalPrice = math.Round(qty*unitPrice*100) / 100
|
|
return totalWeight, totalPrice
|
|
case string(utils.ConvertionUnitPeti):
|
|
totalPrice = math.Round(totalWeight*unitPrice*100) / 100
|
|
return totalWeight, totalPrice
|
|
}
|
|
}
|
|
|
|
totalPrice = math.Round(totalWeight*unitPrice*100) / 100
|
|
}
|
|
return totalWeight, totalPrice
|
|
}
|
|
|
|
func (s *deliveryOrdersService) resolveDeliveryTotals(marketingType string, requestedProduct validation.DeliveryProduct, marketingProduct *entity.MarketingProduct) (totalWeight, totalPrice float64) {
|
|
totalWeight, totalPrice = s.calculatePriceByMarketingType(
|
|
marketingType,
|
|
requestedProduct.Qty,
|
|
requestedProduct.AvgWeight,
|
|
requestedProduct.UnitPrice,
|
|
marketingProduct.Week,
|
|
marketingProduct.ConvertionUnit,
|
|
marketingProduct.WeightPerConvertion,
|
|
)
|
|
|
|
if requestedProduct.TotalWeight != nil {
|
|
totalWeight = *requestedProduct.TotalWeight
|
|
}
|
|
if requestedProduct.TotalPrice != nil {
|
|
totalPrice = *requestedProduct.TotalPrice
|
|
}
|
|
|
|
return totalWeight, totalPrice
|
|
}
|
|
|
|
func (s deliveryOrdersService) consumeDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, requestedQty float64, actorID uint) error {
|
|
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
|
|
}
|
|
|
|
if deliveryProduct == nil || deliveryProduct.Id == 0 {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Delivery product not found")
|
|
}
|
|
if deliveryProduct.ProductWarehouseId == 0 {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Delivery product warehouse not found")
|
|
}
|
|
if deliveryProduct.ProductWarehouseId != marketingProduct.ProductWarehouseId {
|
|
return fiber.NewError(fiber.StatusBadRequest, "Delivery product warehouse mismatch with marketing product")
|
|
}
|
|
|
|
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
|
|
previousUsage := deliveryProduct.UsageQty
|
|
deliveryProduct.UsageQty = requestedQty
|
|
deliveryProduct.PendingQty = 0
|
|
|
|
if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update delivery product")
|
|
}
|
|
if err := reflowMarketingScope(
|
|
ctx,
|
|
s.FifoStockV2Svc,
|
|
tx,
|
|
marketingProduct.ProductWarehouseId,
|
|
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
|
|
); err != nil {
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Insufficient stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
|
|
}
|
|
|
|
refreshed, err := deliveryProductRepo.GetByID(ctx, deliveryProduct.Id, nil)
|
|
if err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh delivery product")
|
|
}
|
|
deliveryProduct.UsageQty = refreshed.UsageQty
|
|
deliveryProduct.PendingQty = refreshed.PendingQty
|
|
deliveryProduct.CreatedAt = refreshed.CreatedAt
|
|
|
|
if err := s.allocatePopulationForMarketingDelivery(ctx, tx, deliveryProduct, marketingProduct.ProductWarehouseId); err != nil {
|
|
return err
|
|
}
|
|
|
|
allocatedDelta := deliveryProduct.UsageQty - previousUsage
|
|
if actorID > 0 && allocatedDelta > 0 {
|
|
decreaseLog := &entity.StockLog{
|
|
Decrease: allocatedDelta,
|
|
LoggableType: string(utils.StockLogTypeMarketing),
|
|
LoggableId: deliveryProduct.Id,
|
|
ProductWarehouseId: deliveryProduct.ProductWarehouseId,
|
|
CreatedBy: actorID,
|
|
Notes: fmt.Sprintf("FIFO v2 reflow consume (%.2f)", allocatedDelta),
|
|
}
|
|
|
|
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, deliveryProduct.ProductWarehouseId, 1)
|
|
if err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
|
}
|
|
if len(stockLogs) > 0 {
|
|
latestStockLog := stockLogs[0]
|
|
decreaseLog.Stock = latestStockLog.Stock - decreaseLog.Decrease
|
|
} else {
|
|
decreaseLog.Stock -= decreaseLog.Decrease
|
|
}
|
|
s.StockLogRepo.WithTx(tx).CreateOne(ctx, decreaseLog, nil)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s deliveryOrdersService) releaseDeliveryStock(ctx context.Context, tx *gorm.DB, deliveryProduct *entity.MarketingDeliveryProduct, marketingProduct *entity.MarketingProduct, actorID uint) error {
|
|
if deliveryProduct == nil || deliveryProduct.Id == 0 {
|
|
return nil
|
|
}
|
|
|
|
if marketingProduct == nil || marketingProduct.ProductWarehouseId == 0 {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Product warehouse not found")
|
|
}
|
|
|
|
deliveryProductRepo := marketingRepo.NewMarketingDeliveryProductRepository(tx)
|
|
currentUsage := deliveryProduct.UsageQty
|
|
currentPending := deliveryProduct.PendingQty
|
|
if currentUsage <= 0 && currentPending <= 0 {
|
|
return nil
|
|
}
|
|
|
|
affectedKandangIDs, err := s.marketingPopulationKandangIDsFromActiveAllocations(ctx, tx, deliveryProduct.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
deliveryProduct.UsageQty = 0
|
|
deliveryProduct.PendingQty = 0
|
|
if err := deliveryProductRepo.UpdateOne(ctx, deliveryProduct.Id, deliveryProduct, nil); err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to reset delivery product")
|
|
}
|
|
|
|
if err := reflowMarketingScope(
|
|
ctx,
|
|
s.FifoStockV2Svc,
|
|
tx,
|
|
marketingProduct.ProductWarehouseId,
|
|
resolveMarketingAsOf(deliveryProduct.DeliveryDate, deliveryProduct.CreatedAt),
|
|
); err != nil {
|
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to reflow stock for product warehouse %d: %v", marketingProduct.ProductWarehouseId, err))
|
|
}
|
|
|
|
refreshed, err := deliveryProductRepo.GetByID(ctx, deliveryProduct.Id, nil)
|
|
if err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to refresh delivery product")
|
|
}
|
|
deliveryProduct.UsageQty = refreshed.UsageQty
|
|
deliveryProduct.PendingQty = refreshed.PendingQty
|
|
deliveryProduct.CreatedAt = refreshed.CreatedAt
|
|
|
|
if err := fifoV2.ReleasePopulationConsumptionByUsable(ctx, tx, fifo.UsableKeyMarketingDelivery.String(), deliveryProduct.Id); err != nil {
|
|
return err
|
|
}
|
|
if err := s.resyncPopulationUsageByKandangIDs(ctx, tx, affectedKandangIDs); err != nil {
|
|
return err
|
|
}
|
|
|
|
releasedUsage := currentUsage - deliveryProduct.UsageQty
|
|
if actorID > 0 && releasedUsage > 0 {
|
|
increaseLog := &entity.StockLog{
|
|
Increase: releasedUsage,
|
|
LoggableType: string(utils.StockLogTypeMarketing),
|
|
LoggableId: deliveryProduct.Id,
|
|
ProductWarehouseId: marketingProduct.ProductWarehouseId,
|
|
CreatedBy: actorID,
|
|
Notes: fmt.Sprintf("FIFO v2 reflow release (%.2f)", releasedUsage),
|
|
}
|
|
stockLogs, err := s.StockLogRepo.GetByProductWarehouse(ctx, marketingProduct.ProductWarehouseId, 1)
|
|
if err != nil {
|
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get stock logs")
|
|
}
|
|
if len(stockLogs) > 0 {
|
|
latestStockLog := stockLogs[0]
|
|
increaseLog.Stock = latestStockLog.Stock + increaseLog.Increase
|
|
} else {
|
|
increaseLog.Stock += increaseLog.Increase
|
|
}
|
|
|
|
s.StockLogRepo.WithTx(tx).CreateOne(ctx, increaseLog, nil)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (s deliveryOrdersService) allocatePopulationForMarketingDelivery(
|
|
ctx context.Context,
|
|
tx *gorm.DB,
|
|
deliveryProduct *entity.MarketingDeliveryProduct,
|
|
productWarehouseID uint,
|
|
) error {
|
|
if deliveryProduct == nil || deliveryProduct.Id == 0 {
|
|
return fiber.NewError(fiber.StatusBadRequest, "Delivery product tidak valid")
|
|
}
|
|
if tx == nil {
|
|
return errors.New("transaction is required")
|
|
}
|
|
if deliveryProduct.UsageQty <= 0 {
|
|
return nil
|
|
}
|
|
if productWarehouseID == 0 {
|
|
return fiber.NewError(fiber.StatusBadRequest, "Product warehouse tidak ditemukan")
|
|
}
|
|
|
|
flagGroupCode, err := resolveMarketingFlagGroupByProductWarehouse(ctx, tx, productWarehouseID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !strings.EqualFold(flagGroupCode, "AYAM") {
|
|
return nil
|
|
}
|
|
|
|
exactAllocations, err := s.findDirectPopulationAllocationsForMarketing(ctx, tx, deliveryProduct.Id)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(exactAllocations) > 0 {
|
|
if err := fifoV2.ReleasePopulationConsumptionByUsable(ctx, tx, fifo.UsableKeyMarketingDelivery.String(), deliveryProduct.Id); err != nil {
|
|
return err
|
|
}
|
|
if err := s.applyDirectPopulationAllocationsForMarketing(ctx, tx, productWarehouseID, deliveryProduct.Id, exactAllocations); err != nil {
|
|
return err
|
|
}
|
|
return s.resyncPopulationUsageByKandangIDs(ctx, tx, marketingAllocationKandangIDs(exactAllocations))
|
|
}
|
|
|
|
sourceGroups, err := s.findPopulationSourceGroupsForMarketing(ctx, tx, deliveryProduct.Id, productWarehouseID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(sourceGroups) == 0 {
|
|
return nil
|
|
}
|
|
if err := fifoV2.ReleasePopulationConsumptionByUsable(ctx, tx, fifo.UsableKeyMarketingDelivery.String(), deliveryProduct.Id); err != nil {
|
|
return err
|
|
}
|
|
for _, group := range sourceGroups {
|
|
populations, err := s.ProjectFlockPopulationRepo.WithTx(tx).GetByProjectFlockKandangIDAndProductWarehouseID(
|
|
ctx,
|
|
group.ProjectFlockKandangID,
|
|
group.ProductWarehouseID,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(populations) == 0 {
|
|
return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak ditemukan untuk delivery")
|
|
}
|
|
if err := s.allocatePopulationConsumptionWithoutRelease(
|
|
ctx,
|
|
tx,
|
|
populations,
|
|
productWarehouseID,
|
|
deliveryProduct.Id,
|
|
group.Qty,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return s.resyncPopulationUsageByKandangIDs(ctx, tx, marketingSourceGroupKandangIDs(sourceGroups))
|
|
}
|
|
|
|
type marketingPopulationAllocation struct {
|
|
ProjectFlockPopulationID uint `gorm:"column:project_flock_population_id"`
|
|
ProjectFlockKandangID uint `gorm:"column:project_flock_kandang_id"`
|
|
Qty float64 `gorm:"column:qty"`
|
|
}
|
|
|
|
type marketingPopulationSourceGroup struct {
|
|
ProjectFlockKandangID uint `gorm:"column:project_flock_kandang_id"`
|
|
ProductWarehouseID uint `gorm:"column:product_warehouse_id"`
|
|
Qty float64 `gorm:"column:qty"`
|
|
}
|
|
|
|
func (s deliveryOrdersService) findDirectPopulationAllocationsForMarketing(
|
|
ctx context.Context,
|
|
tx *gorm.DB,
|
|
deliveryProductID uint,
|
|
) ([]marketingPopulationAllocation, error) {
|
|
var rows []marketingPopulationAllocation
|
|
err := tx.WithContext(ctx).
|
|
Table("stock_allocations sa").
|
|
Select(`
|
|
pfp.id AS project_flock_population_id,
|
|
pc.project_flock_kandang_id AS project_flock_kandang_id,
|
|
SUM(sa.qty) AS qty
|
|
`).
|
|
Joins("JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
|
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
|
Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
|
|
fifo.UsableKeyMarketingDelivery.String(),
|
|
deliveryProductID,
|
|
entity.StockAllocationStatusActive,
|
|
entity.StockAllocationPurposeConsume,
|
|
).
|
|
Group("pfp.id, pc.project_flock_kandang_id").
|
|
Order("pfp.id ASC").
|
|
Scan(&rows).Error
|
|
return rows, err
|
|
}
|
|
|
|
func (s deliveryOrdersService) findPopulationSourceGroupsForMarketing(
|
|
ctx context.Context,
|
|
tx *gorm.DB,
|
|
deliveryProductID uint,
|
|
productWarehouseID uint,
|
|
) ([]marketingPopulationSourceGroup, error) {
|
|
groups := make(map[string]marketingPopulationSourceGroup)
|
|
|
|
appendGroup := func(projectFlockKandangID uint, sourceProductWarehouseID uint, qty float64) {
|
|
if projectFlockKandangID == 0 || sourceProductWarehouseID == 0 || qty <= 0 {
|
|
return
|
|
}
|
|
key := fmt.Sprintf("%d:%d", projectFlockKandangID, sourceProductWarehouseID)
|
|
current := groups[key]
|
|
current.ProjectFlockKandangID = projectFlockKandangID
|
|
current.ProductWarehouseID = sourceProductWarehouseID
|
|
current.Qty += qty
|
|
groups[key] = current
|
|
}
|
|
|
|
var transferRows []marketingPopulationSourceGroup
|
|
if err := tx.WithContext(ctx).
|
|
Table("stock_allocations sa").
|
|
Select(`
|
|
source_pw.project_flock_kandang_id AS project_flock_kandang_id,
|
|
std.source_product_warehouse_id AS product_warehouse_id,
|
|
SUM(sa.qty) AS qty
|
|
`).
|
|
Joins("JOIN stock_transfer_details std ON std.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyStockTransferIn.String()).
|
|
Joins("JOIN product_warehouses source_pw ON source_pw.id = std.source_product_warehouse_id").
|
|
Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
|
|
fifo.UsableKeyMarketingDelivery.String(),
|
|
deliveryProductID,
|
|
entity.StockAllocationStatusActive,
|
|
entity.StockAllocationPurposeConsume,
|
|
).
|
|
Where("source_pw.project_flock_kandang_id IS NOT NULL").
|
|
Group("source_pw.project_flock_kandang_id, std.source_product_warehouse_id").
|
|
Scan(&transferRows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
for _, row := range transferRows {
|
|
appendGroup(row.ProjectFlockKandangID, row.ProductWarehouseID, row.Qty)
|
|
}
|
|
|
|
var purchaseRows []marketingPopulationSourceGroup
|
|
if err := tx.WithContext(ctx).
|
|
Table("stock_allocations sa").
|
|
Select(`
|
|
pi.project_flock_kandang_id AS project_flock_kandang_id,
|
|
pi.product_warehouse_id AS product_warehouse_id,
|
|
SUM(sa.qty) AS qty
|
|
`).
|
|
Joins("JOIN purchase_items pi ON pi.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
|
|
Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
|
|
fifo.UsableKeyMarketingDelivery.String(),
|
|
deliveryProductID,
|
|
entity.StockAllocationStatusActive,
|
|
entity.StockAllocationPurposeConsume,
|
|
).
|
|
Where("pi.project_flock_kandang_id IS NOT NULL").
|
|
Where("pi.product_warehouse_id IS NOT NULL").
|
|
Group("pi.project_flock_kandang_id, pi.product_warehouse_id").
|
|
Scan(&purchaseRows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
for _, row := range purchaseRows {
|
|
appendGroup(row.ProjectFlockKandangID, row.ProductWarehouseID, row.Qty)
|
|
}
|
|
|
|
var layingRows []marketingPopulationSourceGroup
|
|
if err := tx.WithContext(ctx).
|
|
Table("stock_allocations sa").
|
|
Select(`
|
|
ltt.target_project_flock_kandang_id AS project_flock_kandang_id,
|
|
ltt.product_warehouse_id AS product_warehouse_id,
|
|
SUM(sa.qty) AS qty
|
|
`).
|
|
Joins("JOIN laying_transfer_targets ltt ON ltt.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyTransferToLayingIn.String()).
|
|
Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
|
|
fifo.UsableKeyMarketingDelivery.String(),
|
|
deliveryProductID,
|
|
entity.StockAllocationStatusActive,
|
|
entity.StockAllocationPurposeConsume,
|
|
).
|
|
Where("ltt.product_warehouse_id IS NOT NULL").
|
|
Group("ltt.target_project_flock_kandang_id, ltt.product_warehouse_id").
|
|
Scan(&layingRows).Error; err != nil {
|
|
return nil, err
|
|
}
|
|
for _, row := range layingRows {
|
|
appendGroup(row.ProjectFlockKandangID, row.ProductWarehouseID, row.Qty)
|
|
}
|
|
|
|
if len(groups) == 0 {
|
|
pw, err := s.ProductWarehouseRepo.WithTx(tx).GetByID(ctx, productWarehouseID, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if pw.ProjectFlockKandangId != nil && *pw.ProjectFlockKandangId != 0 {
|
|
appendGroup(*pw.ProjectFlockKandangId, productWarehouseID, 0)
|
|
}
|
|
}
|
|
|
|
result := make([]marketingPopulationSourceGroup, 0, len(groups))
|
|
for _, group := range groups {
|
|
if group.Qty == 0 {
|
|
group.Qty = s.resolveMarketingRequestedUsageQty(ctx, tx, deliveryProductID)
|
|
}
|
|
if group.Qty > 0 {
|
|
result = append(result, group)
|
|
}
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (s deliveryOrdersService) applyDirectPopulationAllocationsForMarketing(
|
|
ctx context.Context,
|
|
tx *gorm.DB,
|
|
productWarehouseID uint,
|
|
deliveryProductID uint,
|
|
allocations []marketingPopulationAllocation,
|
|
) error {
|
|
stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx)
|
|
for _, allocation := range allocations {
|
|
if allocation.ProjectFlockPopulationID == 0 || allocation.Qty <= 0 {
|
|
continue
|
|
}
|
|
record := &entity.StockAllocation{
|
|
ProductWarehouseId: productWarehouseID,
|
|
StockableType: fifo.StockableKeyProjectFlockPopulation.String(),
|
|
StockableId: allocation.ProjectFlockPopulationID,
|
|
UsableType: fifo.UsableKeyMarketingDelivery.String(),
|
|
UsableId: deliveryProductID,
|
|
Qty: allocation.Qty,
|
|
Status: entity.StockAllocationStatusActive,
|
|
AllocationPurpose: entity.StockAllocationPurposeConsume,
|
|
}
|
|
if err := stockAllocationRepo.CreateOne(ctx, record, nil); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.WithContext(ctx).
|
|
Model(&entity.ProjectFlockPopulation{}).
|
|
Where("id = ?", allocation.ProjectFlockPopulationID).
|
|
Update("total_used_qty", gorm.Expr("total_used_qty + ?", allocation.Qty)).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s deliveryOrdersService) allocatePopulationConsumptionWithoutRelease(
|
|
ctx context.Context,
|
|
tx *gorm.DB,
|
|
populations []entity.ProjectFlockPopulation,
|
|
productWarehouseID uint,
|
|
deliveryProductID uint,
|
|
consumeQty float64,
|
|
) error {
|
|
if consumeQty <= 0 {
|
|
return nil
|
|
}
|
|
remaining := consumeQty
|
|
stockAllocationRepo := commonRepo.NewStockAllocationRepository(tx)
|
|
for _, population := range populations {
|
|
available := population.TotalQty - population.TotalUsedQty
|
|
if available <= 0 {
|
|
continue
|
|
}
|
|
portion := available
|
|
if remaining < portion {
|
|
portion = remaining
|
|
}
|
|
if portion <= 0 {
|
|
continue
|
|
}
|
|
|
|
record := &entity.StockAllocation{
|
|
ProductWarehouseId: productWarehouseID,
|
|
StockableType: fifo.StockableKeyProjectFlockPopulation.String(),
|
|
StockableId: population.Id,
|
|
UsableType: fifo.UsableKeyMarketingDelivery.String(),
|
|
UsableId: deliveryProductID,
|
|
Qty: portion,
|
|
Status: entity.StockAllocationStatusActive,
|
|
AllocationPurpose: entity.StockAllocationPurposeConsume,
|
|
}
|
|
if err := stockAllocationRepo.CreateOne(ctx, record, nil); err != nil {
|
|
return err
|
|
}
|
|
if err := tx.WithContext(ctx).
|
|
Model(&entity.ProjectFlockPopulation{}).
|
|
Where("id = ?", population.Id).
|
|
Update("total_used_qty", gorm.Expr("total_used_qty + ?", portion)).Error; err != nil {
|
|
return err
|
|
}
|
|
|
|
remaining -= portion
|
|
if remaining <= 0.000001 {
|
|
break
|
|
}
|
|
}
|
|
|
|
if remaining > 0.000001 {
|
|
return fiber.NewError(fiber.StatusBadRequest, "Populasi tidak mencukupi")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s deliveryOrdersService) marketingPopulationKandangIDsFromActiveAllocations(
|
|
ctx context.Context,
|
|
tx *gorm.DB,
|
|
deliveryProductID uint,
|
|
) ([]uint, error) {
|
|
var ids []uint
|
|
err := tx.WithContext(ctx).
|
|
Table("stock_allocations sa").
|
|
Distinct("pc.project_flock_kandang_id").
|
|
Joins("JOIN project_flock_populations pfp ON pfp.id = sa.stockable_id AND sa.stockable_type = ?", fifo.StockableKeyProjectFlockPopulation.String()).
|
|
Joins("JOIN project_chickins pc ON pc.id = pfp.project_chickin_id").
|
|
Where("sa.usable_type = ? AND sa.usable_id = ? AND sa.status = ? AND sa.allocation_purpose = ?",
|
|
fifo.UsableKeyMarketingDelivery.String(),
|
|
deliveryProductID,
|
|
entity.StockAllocationStatusActive,
|
|
entity.StockAllocationPurposeConsume,
|
|
).
|
|
Pluck("pc.project_flock_kandang_id", &ids).Error
|
|
return ids, err
|
|
}
|
|
|
|
func (s deliveryOrdersService) resyncPopulationUsageByKandangIDs(ctx context.Context, tx *gorm.DB, kandangIDs []uint) error {
|
|
for _, kandangID := range uniqueUintIDs(kandangIDs) {
|
|
if kandangID == 0 {
|
|
continue
|
|
}
|
|
if err := s.ProjectFlockPopulationRepo.WithTx(tx).ResyncUsageByProjectFlockKandangID(ctx, tx, kandangID); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s deliveryOrdersService) resolveMarketingRequestedUsageQty(ctx context.Context, tx *gorm.DB, deliveryProductID uint) float64 {
|
|
var usageQty float64
|
|
if err := tx.WithContext(ctx).
|
|
Table("marketing_delivery_products").
|
|
Select("usage_qty + pending_qty").
|
|
Where("id = ?", deliveryProductID).
|
|
Scan(&usageQty).Error; err != nil {
|
|
return 0
|
|
}
|
|
return usageQty
|
|
}
|
|
|
|
func marketingAllocationKandangIDs(rows []marketingPopulationAllocation) []uint {
|
|
ids := make([]uint, 0, len(rows))
|
|
for _, row := range rows {
|
|
ids = append(ids, row.ProjectFlockKandangID)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func marketingSourceGroupKandangIDs(rows []marketingPopulationSourceGroup) []uint {
|
|
ids := make([]uint, 0, len(rows))
|
|
for _, row := range rows {
|
|
ids = append(ids, row.ProjectFlockKandangID)
|
|
}
|
|
return ids
|
|
}
|
|
|
|
func uniqueUintIDs(ids []uint) []uint {
|
|
seen := make(map[uint]struct{}, len(ids))
|
|
result := make([]uint, 0, len(ids))
|
|
for _, id := range ids {
|
|
if id == 0 {
|
|
continue
|
|
}
|
|
if _, ok := seen[id]; ok {
|
|
continue
|
|
}
|
|
seen[id] = struct{}{}
|
|
result = append(result, id)
|
|
}
|
|
return result
|
|
}
|