mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
add restrict create/edit/delete depletion
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"math"
|
||||
"mime/multipart"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -29,6 +30,7 @@ import (
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type PurchaseService interface {
|
||||
@@ -67,6 +69,13 @@ type staffAdjustmentPayload struct {
|
||||
NewItems []*entity.PurchaseItem
|
||||
}
|
||||
|
||||
type purchaseDownstreamDependency struct {
|
||||
UsableType string `gorm:"column:usable_type"`
|
||||
UsableID uint64 `gorm:"column:usable_id"`
|
||||
FunctionCode string `gorm:"column:function_code"`
|
||||
FlagGroupCode string `gorm:"column:flag_group_code"`
|
||||
}
|
||||
|
||||
func NewPurchaseService(
|
||||
validate *validator.Validate,
|
||||
purchaseRepo rPurchase.PurchaseRepository,
|
||||
@@ -884,8 +893,28 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
||||
if receivedQty > item.SubQty {
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Received quantity for item %d cannot exceed ordered quantity (%.3f)", payload.PurchaseItemID, item.SubQty))
|
||||
}
|
||||
if receivedQty < item.TotalUsed && isReceivingBelowUsedBlocked(item, lockedIDs) {
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Received quantity for item %d cannot be lower than used amount (%.3f)", payload.PurchaseItemID, item.TotalUsed))
|
||||
if receivedQty < item.TotalUsed {
|
||||
deps, allowPending, err := s.resolvePurchaseDependenciesAndPendingPolicy(
|
||||
ctx,
|
||||
s.PurchaseRepo.DB(),
|
||||
[]uint{item.Id},
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(deps) > 0 && !allowPending {
|
||||
return nil, utils.BadRequest(
|
||||
fmt.Sprintf(
|
||||
"Received quantity for item %d cannot be lower than used amount (%.3f). Dependensi aktif: %s. Alasan block: pending disabled by config.",
|
||||
payload.PurchaseItemID,
|
||||
item.TotalUsed,
|
||||
formatPurchaseDependencySummary(deps),
|
||||
),
|
||||
)
|
||||
}
|
||||
if len(deps) == 0 && isReceivingBelowUsedBlocked(item, lockedIDs) {
|
||||
return nil, utils.BadRequest(fmt.Sprintf("Received quantity for item %d cannot be lower than used amount (%.3f)", payload.PurchaseItemID, item.TotalUsed))
|
||||
}
|
||||
}
|
||||
|
||||
if _, dup := visitedItems[payload.PurchaseItemID]; dup {
|
||||
@@ -1317,6 +1346,30 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
||||
|
||||
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
repoTx := rPurchase.NewPurchaseRepository(tx)
|
||||
var lockedPurchase entity.Purchase
|
||||
if err := tx.WithContext(ctx).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id = ?", purchase.Id).
|
||||
Take(&lockedPurchase).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allowPending, deps, err := s.ensurePurchaseDeletePolicy(ctx, tx, toDelete)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(deps) > 0 && !allowPending {
|
||||
return utils.BadRequest(
|
||||
fmt.Sprintf(
|
||||
"Purchase item tidak dapat dihapus karena sudah dipakai transaksi turunan. Dependensi aktif: %s. Alasan block: pending disabled by config.",
|
||||
formatPurchaseDependencySummary(deps),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if err := s.rollbackPurchaseStock(ctx, tx, itemsToDelete, fmt.Sprintf("Purchase-Item-Delete#%d", purchase.Id), purchase.CreatedBy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := repoTx.DeleteItems(ctx, purchase.Id, toDelete); err != nil {
|
||||
return err
|
||||
@@ -1385,12 +1438,25 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
||||
}
|
||||
|
||||
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
lockedIDs, err := s.resolveChickinLockedItemIDs(ctx, tx, itemsToDelete)
|
||||
var lockedPurchase entity.Purchase
|
||||
if err := tx.WithContext(ctx).
|
||||
Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("id = ?", purchase.Id).
|
||||
Take(&lockedPurchase).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allowPending, deps, err := s.ensurePurchaseDeletePolicy(ctx, tx, collectPurchaseItemIDs(itemsToDelete))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(lockedIDs) > 0 {
|
||||
return utils.BadRequest("Purchase already chickin, failed to delete purchase")
|
||||
if len(deps) > 0 && !allowPending {
|
||||
return utils.BadRequest(
|
||||
fmt.Sprintf(
|
||||
"Purchase tidak dapat dihapus karena sudah dipakai transaksi turunan. Dependensi aktif: %s. Alasan block: pending disabled by config.",
|
||||
formatPurchaseDependencySummary(deps),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
if err := s.rollbackPurchaseStock(ctx, tx, itemsToDelete, note, actorID); err != nil {
|
||||
@@ -1795,6 +1861,125 @@ func purchaseItemHasFlag(item *entity.PurchaseItem, flag utils.FlagType) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *purchaseService) ensurePurchaseDeletePolicy(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
itemIDs []uint,
|
||||
) (bool, []purchaseDownstreamDependency, error) {
|
||||
deps, allowPending, err := s.resolvePurchaseDependenciesAndPendingPolicy(ctx, tx, itemIDs)
|
||||
if err != nil {
|
||||
return false, nil, err
|
||||
}
|
||||
return allowPending, deps, nil
|
||||
}
|
||||
|
||||
func (s *purchaseService) resolvePurchaseDependenciesAndPendingPolicy(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
itemIDs []uint,
|
||||
) ([]purchaseDownstreamDependency, bool, error) {
|
||||
deps, err := s.loadPurchaseDownstreamDependencies(ctx, tx, itemIDs)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to load downstream dependencies for purchase items: %+v", err)
|
||||
return nil, false, utils.Internal("Failed to validate downstream purchase dependencies")
|
||||
}
|
||||
if len(deps) == 0 {
|
||||
return nil, true, nil
|
||||
}
|
||||
|
||||
allowPending := true
|
||||
for _, dep := range deps {
|
||||
policy, policyErr := commonSvc.ResolveFifoPendingPolicy(ctx, tx, commonSvc.FifoPendingPolicyInput{
|
||||
Lane: "USABLE",
|
||||
FlagGroupCode: dep.FlagGroupCode,
|
||||
FunctionCode: dep.FunctionCode,
|
||||
LegacyTypeKey: dep.UsableType,
|
||||
})
|
||||
if policyErr != nil {
|
||||
s.Log.Errorf("Failed to resolve FIFO pending policy for purchase dependency: %+v", policyErr)
|
||||
return nil, false, utils.Internal("Failed to read FIFO v2 configuration")
|
||||
}
|
||||
if !policy.Found || !policy.AllowPending {
|
||||
allowPending = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return deps, allowPending, nil
|
||||
}
|
||||
|
||||
func (s *purchaseService) loadPurchaseDownstreamDependencies(
|
||||
ctx context.Context,
|
||||
tx *gorm.DB,
|
||||
itemIDs []uint,
|
||||
) ([]purchaseDownstreamDependency, error) {
|
||||
if len(itemIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
db := s.PurchaseRepo.DB().WithContext(ctx)
|
||||
if tx != nil {
|
||||
db = tx.WithContext(ctx)
|
||||
}
|
||||
|
||||
var rows []purchaseDownstreamDependency
|
||||
err := db.Table("stock_allocations").
|
||||
Select("usable_type, usable_id, COALESCE(function_code,'') AS function_code, COALESCE(flag_group_code,'') AS flag_group_code").
|
||||
Where("stockable_type = ?", fifo.StockableKeyPurchaseItems.String()).
|
||||
Where("stockable_id IN ?", itemIDs).
|
||||
Where("status = ?", entity.StockAllocationStatusActive).
|
||||
Where("allocation_purpose = ?", entity.StockAllocationPurposeConsume).
|
||||
Where("deleted_at IS NULL").
|
||||
Group("usable_type, usable_id, function_code, flag_group_code").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return rows, nil
|
||||
}
|
||||
|
||||
func formatPurchaseDependencySummary(rows []purchaseDownstreamDependency) string {
|
||||
if len(rows) == 0 {
|
||||
return "-"
|
||||
}
|
||||
|
||||
dependencyMap := make(map[string]map[uint64]struct{})
|
||||
for _, row := range rows {
|
||||
label := strings.ToUpper(strings.TrimSpace(row.UsableType))
|
||||
if label == "" {
|
||||
label = "UNKNOWN"
|
||||
}
|
||||
if _, ok := dependencyMap[label]; !ok {
|
||||
dependencyMap[label] = make(map[uint64]struct{})
|
||||
}
|
||||
dependencyMap[label][row.UsableID] = struct{}{}
|
||||
}
|
||||
|
||||
labels := make([]string, 0, len(dependencyMap))
|
||||
for label := range dependencyMap {
|
||||
labels = append(labels, label)
|
||||
}
|
||||
sort.Strings(labels)
|
||||
|
||||
parts := make([]string, 0, len(labels))
|
||||
for _, label := range labels {
|
||||
ids := make([]uint64, 0, len(dependencyMap[label]))
|
||||
for id := range dependencyMap[label] {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
|
||||
|
||||
idParts := make([]string, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
idParts = append(idParts, fmt.Sprintf("%d", id))
|
||||
}
|
||||
parts = append(parts, fmt.Sprintf("%s=%s", label, strings.Join(idParts, "|")))
|
||||
}
|
||||
|
||||
return strings.Join(parts, ", ")
|
||||
}
|
||||
|
||||
func isReceivingBelowUsedBlocked(item *entity.PurchaseItem, lockedIDs map[uint]struct{}) bool {
|
||||
if item == nil {
|
||||
return false
|
||||
|
||||
Reference in New Issue
Block a user