mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 05:21:57 +00:00
fix(BE): sorting by date approval get all
This commit is contained in:
@@ -15,7 +15,7 @@ type ApprovalService interface {
|
||||
WorkflowSteps(workflow approvalutils.ApprovalWorkflowKey) map[approvalutils.ApprovalStep]string
|
||||
WorkflowStepName(workflow approvalutils.ApprovalWorkflowKey, step approvalutils.ApprovalStep) (string, bool)
|
||||
CreateApproval(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, step approvalutils.ApprovalStep, action *entity.ApprovalAction, actorID uint, note *string) (*entity.Approval, error)
|
||||
List(ctx context.Context, module string, approvableID *uint, page, limit int, search string) ([]entity.Approval, int64, error)
|
||||
List(ctx context.Context, module string, approvableID *uint, page, limit int, search string, sortByDate string) ([]entity.Approval, int64, error)
|
||||
ListByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.Approval, error)
|
||||
LatestByTarget(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableID uint, modifier func(*gorm.DB) *gorm.DB) (*entity.Approval, error)
|
||||
LatestByTargets(ctx context.Context, workflow approvalutils.ApprovalWorkflowKey, approvableIDs []uint, modifier func(*gorm.DB) *gorm.DB) (map[uint]*entity.Approval, error)
|
||||
@@ -70,9 +70,14 @@ func (s *approvalService) List(
|
||||
approvableID *uint,
|
||||
page, limit int,
|
||||
search string,
|
||||
sortByDate string,
|
||||
) ([]entity.Approval, int64, error) {
|
||||
module = strings.TrimSpace(strings.ToUpper(module))
|
||||
search = strings.TrimSpace(search)
|
||||
sortByDate = strings.TrimSpace(strings.ToUpper(sortByDate))
|
||||
if sortByDate != "ASC" && sortByDate != "DESC" {
|
||||
sortByDate = "DESC"
|
||||
}
|
||||
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
@@ -90,7 +95,7 @@ func (s *approvalService) List(
|
||||
func(db *gorm.DB) *gorm.DB {
|
||||
query := db.
|
||||
Where("approvable_type = ?", module).
|
||||
Order("action_at DESC").
|
||||
Order("action_at " + sortByDate).
|
||||
Preload("ActionUser")
|
||||
|
||||
if approvableID != nil {
|
||||
|
||||
@@ -44,6 +44,15 @@ func (u *ApprovalController) GetAll(c *fiber.Ctx) error {
|
||||
page := c.QueryInt("page", 1)
|
||||
limit := c.QueryInt("limit", 10)
|
||||
search := strings.TrimSpace(c.Query("search", ""))
|
||||
sortByDate := strings.TrimSpace(c.Query("sort_by_date", ""))
|
||||
if sortByDate == "" {
|
||||
sortByDate = "DESC"
|
||||
} else {
|
||||
sortByDate = strings.ToUpper(sortByDate)
|
||||
if sortByDate != "ASC" && sortByDate != "DESC" {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "sort_by_date must be either ASC or DESC")
|
||||
}
|
||||
}
|
||||
|
||||
query := &validation.Query{
|
||||
ModuleName: moduleName,
|
||||
@@ -52,6 +61,7 @@ func (u *ApprovalController) GetAll(c *fiber.Ctx) error {
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
Search: search,
|
||||
SortByDate: sortByDate,
|
||||
}
|
||||
|
||||
records, totalResults, err := u.ApprovalService.List(
|
||||
@@ -61,6 +71,7 @@ func (u *ApprovalController) GetAll(c *fiber.Ctx) error {
|
||||
query.Page,
|
||||
query.Limit,
|
||||
query.Search,
|
||||
query.SortByDate,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -7,4 +7,5 @@ type Query struct {
|
||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
|
||||
Search string `query:"search" validate:"omitempty,max=50"`
|
||||
SortByDate string `query:"sort_by_date" validate:"omitempty,oneof=ASC DESC"`
|
||||
}
|
||||
|
||||
@@ -264,7 +264,7 @@ func (s closingService) getClosingSummaryByKandang(ctx context.Context, projectF
|
||||
statusProject := "Belum Selesai"
|
||||
var approvalDate string
|
||||
if s.ApprovalSvc != nil {
|
||||
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlockKandang.String(), &kandang.Id, 1, 1000, "")
|
||||
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlockKandang.String(), &kandang.Id, 1, 1000, "", "")
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to fetch approvals for project flock kandang %d: %+v", kandang.Id, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch approval data")
|
||||
@@ -542,7 +542,7 @@ func (s closingService) getApprovalStatuses(ctx context.Context, projectFlockID
|
||||
return "", "Belum Selesai", nil
|
||||
}
|
||||
|
||||
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlock.String(), &projectFlockID, 1, 1000, "")
|
||||
records, _, err := s.ApprovalSvc.List(ctx, utils.ApprovalWorkflowProjectFlock.String(), &projectFlockID, 1, 1000, "", "")
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
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"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
)
|
||||
|
||||
// Test Transfer FIFO with Purchase as initial stockable
|
||||
func TestTransferFIFO_PurchaseToTransfer(t *testing.T) {
|
||||
db, fifoSvc := setupTransferFIFOTest(t)
|
||||
ctx := context.Background()
|
||||
|
||||
// Setup warehouses
|
||||
sourcePW := createProductWarehouseRow(t, db, 100) // 100 qty from purchase
|
||||
destPW := createProductWarehouseRow(t, db, 0) // 0 qty initially
|
||||
|
||||
// Step 1: Simulate Purchase - Replenish stock to source warehouse
|
||||
purchaseStockableKey := fifo.StockableKey("PURCHASE_ITEMS")
|
||||
if _, err := fifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||
StockableKey: purchaseStockableKey,
|
||||
StockableID: 1, // PurchaseItem ID
|
||||
ProductWarehouseID: sourcePW.Id,
|
||||
Quantity: 100,
|
||||
}); err != nil {
|
||||
t.Fatalf("Failed to replenish from purchase: %v", err)
|
||||
}
|
||||
|
||||
// Verify source warehouse has stock
|
||||
assertWarehouseQuantity(t, db, sourcePW.Id, 100)
|
||||
assertAllocationCount(t, db, 1) // 1 allocation from purchase
|
||||
|
||||
// Step 2: Create Transfer - will consume from source (usable) and replenish to dest (stockable)
|
||||
|
||||
// Register Transfer as Usable (source warehouse - STOCK_TRANSFER_OUT)
|
||||
transferUsableKey := fifo.UsableKey("STOCK_TRANSFER_OUT")
|
||||
if err := fifoSvc.RegisterUsable(fifo.UsableConfig{
|
||||
Key: transferUsableKey,
|
||||
Table: "stock_transfer_details",
|
||||
Columns: fifo.UsableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "source_product_warehouse_id",
|
||||
UsageQuantity: "usage_qty",
|
||||
PendingQuantity: "pending_qty",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
}); err != nil && !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
t.Fatalf("Failed to register STOCK_TRANSFER_OUT as Usable: %v", err)
|
||||
}
|
||||
|
||||
// Register Transfer as Stockable (destination warehouse - STOCK_TRANSFER_IN)
|
||||
transferStockableKey := fifo.StockableKey("STOCK_TRANSFER_IN")
|
||||
if err := fifoSvc.RegisterStockable(fifo.StockableConfig{
|
||||
Key: transferStockableKey,
|
||||
Table: "stock_transfer_details",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "dest_product_warehouse_id",
|
||||
TotalQuantity: "total_qty",
|
||||
TotalUsedQuantity: "total_used",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
}); err != nil && !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
t.Fatalf("Failed to register STOCK_TRANSFER_IN as Stockable: %v", err)
|
||||
}
|
||||
|
||||
// Create transfer detail record
|
||||
transferDetail := entity.StockTransferDetail{
|
||||
Id: 1,
|
||||
StockTransferId: 1,
|
||||
ProductId: 1,
|
||||
SourceProductWarehouseID: uint64Ptr(uint64(sourcePW.Id)),
|
||||
DestProductWarehouseID: uint64Ptr(uint64(destPW.Id)),
|
||||
UsageQty: 0,
|
||||
PendingQty: 0,
|
||||
TotalQty: 0,
|
||||
TotalUsed: 0,
|
||||
}
|
||||
transferDetailID := uint(transferDetail.Id)
|
||||
if err := db.Create(&transferDetail).Error; err != nil {
|
||||
t.Fatalf("Failed to create transfer detail: %v", err)
|
||||
}
|
||||
|
||||
transferQty := 50.0
|
||||
|
||||
// Consume from source warehouse (STOCK_TRANSFER_OUT)
|
||||
consumeResult, err := fifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
||||
UsableKey: "STOCK_TRANSFER_OUT",
|
||||
UsableID: transferDetailID,
|
||||
ProductWarehouseID: sourcePW.Id,
|
||||
Quantity: transferQty,
|
||||
AllowPending: false, // Don't allow pending
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to consume from source warehouse: %v", err)
|
||||
}
|
||||
|
||||
// Verify consumption
|
||||
if mathAbs(consumeResult.UsageQuantity-transferQty) > 1e-6 {
|
||||
t.Fatalf("Expected usage quantity %.2f, got %.2f", transferQty, consumeResult.UsageQuantity)
|
||||
}
|
||||
if mathAbs(consumeResult.PendingQuantity) > 1e-6 {
|
||||
t.Fatalf("Expected pending quantity 0, got %.2f", consumeResult.PendingQuantity)
|
||||
}
|
||||
|
||||
// Update transfer detail usable fields
|
||||
if err := db.Model(&entity.StockTransferDetail{}).
|
||||
Where("id = ?", transferDetail.Id).
|
||||
Updates(map[string]interface{}{
|
||||
"usage_qty": consumeResult.UsageQuantity,
|
||||
"pending_qty": consumeResult.PendingQuantity,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("Failed to update transfer detail usable fields: %v", err)
|
||||
}
|
||||
|
||||
// Verify source warehouse decreased
|
||||
assertWarehouseQuantity(t, db, sourcePW.Id, 50) // 100 - 50 = 50
|
||||
|
||||
// Verify allocation updated - should have 50 allocated to transfer
|
||||
allocations := fetchAllocationsByUsable(t, db, "STOCK_TRANSFER_OUT", transferDetailID)
|
||||
if len(allocations) != 1 {
|
||||
t.Fatalf("Expected 1 allocation, got %d", len(allocations))
|
||||
}
|
||||
if mathAbs(allocations[0].Qty-transferQty) > 1e-6 {
|
||||
t.Fatalf("Expected allocation qty %.2f, got %.2f", transferQty, allocations[0].Qty)
|
||||
}
|
||||
|
||||
// Replenish to destination warehouse (STOCK_TRANSFER_IN)
|
||||
note := "Transfer #1"
|
||||
replenishResult, err := fifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||
StockableKey: "STOCK_TRANSFER_IN",
|
||||
StockableID: transferDetailID,
|
||||
ProductWarehouseID: destPW.Id,
|
||||
Quantity: transferQty,
|
||||
Note: ¬e,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to replenish to destination warehouse: %v", err)
|
||||
}
|
||||
|
||||
// Verify replenishment
|
||||
if mathAbs(replenishResult.AddedQuantity-transferQty) > 1e-6 {
|
||||
t.Fatalf("Expected added quantity %.2f, got %.2f", transferQty, replenishResult.AddedQuantity)
|
||||
}
|
||||
|
||||
// Update transfer detail stockable fields
|
||||
if err := db.Model(&entity.StockTransferDetail{}).
|
||||
Where("id = ?", transferDetail.Id).
|
||||
Updates(map[string]interface{}{
|
||||
"total_qty": replenishResult.AddedQuantity,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("Failed to update transfer detail stockable fields: %v", err)
|
||||
}
|
||||
|
||||
// Verify destination warehouse increased
|
||||
assertWarehouseQuantity(t, db, destPW.Id, transferQty)
|
||||
|
||||
// Verify new stockable allocation created
|
||||
stockableAllocations := fetchAllocationsByStockable(t, db, "STOCK_TRANSFER_IN", transferDetailID)
|
||||
if len(stockableAllocations) != 1 {
|
||||
t.Fatalf("Expected 1 stockable allocation, got %d", len(stockableAllocations))
|
||||
}
|
||||
if mathAbs(stockableAllocations[0].Qty-transferQty) > 1e-6 {
|
||||
t.Fatalf("Expected stockable allocation qty %.2f, got %.2f", transferQty, stockableAllocations[0].Qty)
|
||||
}
|
||||
|
||||
t.Logf("✅ Transfer FIFO test passed:")
|
||||
t.Logf(" - Source warehouse: 100 → 50 (consumed %d)", int(transferQty))
|
||||
t.Logf(" - Destination warehouse: 0 → %d (replenished)", int(transferQty))
|
||||
t.Logf(" - Usable allocation: %.2f allocated to transfer", allocations[0].Qty)
|
||||
t.Logf(" - Stockable allocation: %.2f available at destination", stockableAllocations[0].Qty)
|
||||
}
|
||||
|
||||
// Setup function for transfer FIFO test
|
||||
func setupTransferFIFOTest(t *testing.T) (*gorm.DB, commonSvc.FifoService) {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("open db: %v", err)
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(
|
||||
&entity.ProductWarehouse{},
|
||||
&entity.StockAllocation{},
|
||||
&entity.StockTransferDetail{},
|
||||
); err != nil {
|
||||
t.Fatalf("auto migrate entities: %v", err)
|
||||
}
|
||||
|
||||
stockAllocRepo := commonRepo.NewStockAllocationRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
fifoSvc := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||
|
||||
// Register Purchase as Stockable
|
||||
purchaseStockableKey := fifo.StockableKey("PURCHASE_ITEMS")
|
||||
if err := fifoSvc.RegisterStockable(fifo.StockableConfig{
|
||||
Key: purchaseStockableKey,
|
||||
Table: "purchase_items",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
TotalQuantity: "total_qty",
|
||||
TotalUsedQuantity: "total_used",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
}); err != nil && !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
t.Fatalf("register purchase stockable: %v", err)
|
||||
}
|
||||
|
||||
return db, fifoSvc
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
|
||||
func createProductWarehouseRow(t *testing.T, db *gorm.DB, qty float64) entity.ProductWarehouse {
|
||||
t.Helper()
|
||||
pw := entity.ProductWarehouse{
|
||||
ProductId: 1,
|
||||
WarehouseId: 1,
|
||||
Quantity: qty,
|
||||
}
|
||||
if err := db.Create(&pw).Error; err != nil {
|
||||
t.Fatalf("create product warehouse: %v", err)
|
||||
}
|
||||
return pw
|
||||
}
|
||||
|
||||
func assertWarehouseQuantity(t *testing.T, db *gorm.DB, pwID uint, expected float64) {
|
||||
t.Helper()
|
||||
var pw entity.ProductWarehouse
|
||||
if err := db.First(&pw, pwID).Error; err != nil {
|
||||
t.Fatalf("fetch product warehouse %d: %v", pwID, err)
|
||||
}
|
||||
if mathAbs(pw.Quantity-expected) > 1e-6 {
|
||||
t.Fatalf("expected warehouse quantity %.2f, got %.2f", expected, pw.Quantity)
|
||||
}
|
||||
}
|
||||
|
||||
func assertAllocationCount(t *testing.T, db *gorm.DB, expected int) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
if err := db.Model(&entity.StockAllocation{}).Count(&count).Error; err != nil {
|
||||
t.Fatalf("count allocations: %v", err)
|
||||
}
|
||||
if int(count) != expected {
|
||||
t.Fatalf("expected %d allocations, got %d", expected, count)
|
||||
}
|
||||
}
|
||||
|
||||
func fetchAllocationsByUsable(t *testing.T, db *gorm.DB, usableType string, usableID uint) []entity.StockAllocation {
|
||||
t.Helper()
|
||||
var allocations []entity.StockAllocation
|
||||
if err := db.Where("usable_type = ? AND usable_id = ?", usableType, usableID).
|
||||
Find(&allocations).Error; err != nil {
|
||||
t.Fatalf("fetch allocations by usable: %v", err)
|
||||
}
|
||||
return allocations
|
||||
}
|
||||
|
||||
func fetchAllocationsByStockable(t *testing.T, db *gorm.DB, stockableType string, stockableID uint) []entity.StockAllocation {
|
||||
t.Helper()
|
||||
var allocations []entity.StockAllocation
|
||||
if err := db.Where("stockable_type = ? AND stockable_id = ?", stockableType, stockableID).
|
||||
Find(&allocations).Error; err != nil {
|
||||
t.Fatalf("fetch allocations by stockable: %v", err)
|
||||
}
|
||||
return allocations
|
||||
}
|
||||
|
||||
func floatPtr(f float64) *float64 {
|
||||
return &f
|
||||
}
|
||||
|
||||
func uint64Ptr(u uint64) *uint64 {
|
||||
return &u
|
||||
}
|
||||
|
||||
func mathAbs(f float64) float64 {
|
||||
return math.Abs(f)
|
||||
}
|
||||
|
||||
func sanitizeKey(name string) string {
|
||||
return strings.Map(func(r rune) rune {
|
||||
if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
||||
return r
|
||||
}
|
||||
return '_'
|
||||
}, name)
|
||||
}
|
||||
@@ -1,446 +0,0 @@
|
||||
package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
|
||||
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"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
|
||||
servicePkg "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
|
||||
)
|
||||
|
||||
func TestRecordingFIFO_CreatePendingWithoutStock(t *testing.T) {
|
||||
db, svc, _, _ := setupRecordingFIFOTableTest(t)
|
||||
ctx := context.Background()
|
||||
|
||||
recordingID := uint(1)
|
||||
productWarehouse := createProductWarehouseRow(t, db, 0)
|
||||
stock := createRecordingStockRow(t, db, recordingID, productWarehouse.Id, 10)
|
||||
|
||||
if err := svc.ConsumeRecordingStocks(ctx, db, []entity.RecordingStock{stock}); err != nil {
|
||||
t.Fatalf("consumeRecordingStocks (pending) failed: %v", err)
|
||||
}
|
||||
|
||||
updated := fetchRecordingStock(t, db, stock.Id)
|
||||
assertFloatEqual(t, 0, updated.UsageQty, "usage_qty should remain zero when no stock is available")
|
||||
assertFloatEqual(t, 10, updated.PendingQty, "pending_qty should capture the entire request")
|
||||
assertWarehouseQuantity(t, db, productWarehouse.Id, 0)
|
||||
assertAllocationCount(t, db, 0)
|
||||
|
||||
assertAllocationCount(t, db, 0)
|
||||
}
|
||||
|
||||
func TestRecordingFIFO_EditReallocatesUsage(t *testing.T) {
|
||||
db, svc, fifoSvc, stockableKey := setupRecordingFIFOTableTest(t)
|
||||
ctx := context.Background()
|
||||
|
||||
recordingID := uint(1)
|
||||
productWarehouse := createProductWarehouseRow(t, db, 0)
|
||||
stock := createRecordingStockRow(t, db, recordingID, productWarehouse.Id, 10)
|
||||
lot := createStockLot(t, db, productWarehouse.Id)
|
||||
|
||||
if _, err := fifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||
StockableKey: stockableKey,
|
||||
StockableID: lot.Id,
|
||||
ProductWarehouseID: productWarehouse.Id,
|
||||
Quantity: 12,
|
||||
}); err != nil {
|
||||
t.Fatalf("replenish failed: %v", err)
|
||||
}
|
||||
|
||||
if err := svc.ConsumeRecordingStocks(ctx, db, []entity.RecordingStock{stock}); err != nil {
|
||||
t.Fatalf("consumeRecordingStocks (initial) failed: %v", err)
|
||||
}
|
||||
|
||||
assertWarehouseQuantity(t, db, productWarehouse.Id, 2)
|
||||
|
||||
desired := 4.0
|
||||
stock.UsageQty = &desired
|
||||
|
||||
if err := svc.ConsumeRecordingStocks(ctx, db, []entity.RecordingStock{stock}); err != nil {
|
||||
t.Fatalf("consumeRecordingStocks (edit) failed: %v", err)
|
||||
}
|
||||
|
||||
updated := fetchRecordingStock(t, db, stock.Id)
|
||||
assertFloatEqual(t, 4, updated.UsageQty, "usage_qty should reflect edited request")
|
||||
assertFloatEqual(t, 0, updated.PendingQty, "pending_qty should remain zero after downsize")
|
||||
assertWarehouseQuantity(t, db, productWarehouse.Id, 8)
|
||||
|
||||
alloc := fetchSingleAllocation(t, db, stock.Id)
|
||||
if alloc.Status != entity.StockAllocationStatusActive {
|
||||
t.Fatalf("expected ACTIVE allocation, got %s", alloc.Status)
|
||||
}
|
||||
if mathAbs(alloc.Qty-4) > 1e-6 {
|
||||
t.Fatalf("expected allocation qty 4, got %.3f", alloc.Qty)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecordingFIFO_DeleteReleasesStock(t *testing.T) {
|
||||
db, svc, fifoSvc, stockableKey := setupRecordingFIFOTableTest(t)
|
||||
ctx := context.Background()
|
||||
|
||||
recordingID := uint(1)
|
||||
productWarehouse := createProductWarehouseRow(t, db, 0)
|
||||
stock := createRecordingStockRow(t, db, recordingID, productWarehouse.Id, 10)
|
||||
lot := createStockLot(t, db, productWarehouse.Id)
|
||||
|
||||
if _, err := fifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||
StockableKey: stockableKey,
|
||||
StockableID: lot.Id,
|
||||
ProductWarehouseID: productWarehouse.Id,
|
||||
Quantity: 10,
|
||||
}); err != nil {
|
||||
t.Fatalf("replenish failed: %v", err)
|
||||
}
|
||||
if err := svc.ConsumeRecordingStocks(ctx, db, []entity.RecordingStock{stock}); err != nil {
|
||||
t.Fatalf("consumeRecordingStocks failed: %v", err)
|
||||
}
|
||||
|
||||
if err := svc.ReleaseRecordingStocks(ctx, db, []entity.RecordingStock{stock}); err != nil {
|
||||
t.Fatalf("releaseRecordingStocks failed: %v", err)
|
||||
}
|
||||
|
||||
updated := fetchRecordingStock(t, db, stock.Id)
|
||||
assertFloatEqual(t, 0, updated.UsageQty, "usage_qty should be cleared after delete")
|
||||
assertFloatEqual(t, 0, updated.PendingQty, "pending_qty should be cleared after delete")
|
||||
assertWarehouseQuantity(t, db, productWarehouse.Id, 10)
|
||||
|
||||
alloc := fetchSingleAllocation(t, db, stock.Id)
|
||||
if alloc.Status != entity.StockAllocationStatusReleased {
|
||||
t.Fatalf("expected allocation to be released, got %s", alloc.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// --- helpers ----------------------------------------------------------------
|
||||
|
||||
type recordingStockTable struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
RecordingId uint `gorm:"column:recording_id;not null"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
UsageQty *float64 `gorm:"column:usage_qty"`
|
||||
PendingQty *float64 `gorm:"column:pending_qty"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (recordingStockTable) TableName() string { return "recording_stocks" }
|
||||
|
||||
type productWarehouseTable struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
ProductId uint `gorm:"column:product_id"`
|
||||
WarehouseId uint `gorm:"column:warehouse_id"`
|
||||
Quantity float64 `gorm:"column:quantity"`
|
||||
CreatedBy uint `gorm:"column:created_by"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
}
|
||||
|
||||
func (productWarehouseTable) TableName() string { return "product_warehouses" }
|
||||
|
||||
type stockAllocationTable struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
ProductWarehouseId uint `gorm:"not null"`
|
||||
StockableType string `gorm:"size:100"`
|
||||
StockableId uint
|
||||
UsableType string `gorm:"size:100"`
|
||||
UsableId uint
|
||||
Qty float64 `gorm:"column:qty"`
|
||||
Status string `gorm:"size:20"`
|
||||
Note *string `gorm:"type:text"`
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ReleasedAt *time.Time
|
||||
DeletedAt gorm.DeletedAt `gorm:"index"`
|
||||
}
|
||||
|
||||
func (stockAllocationTable) TableName() string { return "stock_allocations" }
|
||||
|
||||
type testStockSource struct {
|
||||
Id uint `gorm:"primaryKey"`
|
||||
ProductWarehouseId uint `gorm:"column:product_warehouse_id;not null"`
|
||||
TotalQty float64 `gorm:"column:total_qty"`
|
||||
TotalUsedQty float64 `gorm:"column:total_used_qty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
func (testStockSource) TableName() string { return "test_fifo_stockables" }
|
||||
|
||||
func setupRecordingFIFOTableTest(t *testing.T) (*gorm.DB, servicePkg.RecordingFIFOIntegrationService, commonSvc.FifoService, fifo.StockableKey) {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{
|
||||
Logger: logger.Default.LogMode(logger.Silent),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(
|
||||
&recordingStockTable{},
|
||||
&productWarehouseTable{},
|
||||
&stockAllocationTable{},
|
||||
&testStockSource{},
|
||||
); err != nil {
|
||||
t.Fatalf("auto migrate: %v", err)
|
||||
}
|
||||
|
||||
if err := db.AutoMigrate(
|
||||
&entity.ProductWarehouse{},
|
||||
&entity.StockAllocation{},
|
||||
&entity.RecordingStock{},
|
||||
); err != nil {
|
||||
t.Fatalf("auto migrate entities: %v", err)
|
||||
}
|
||||
|
||||
stockAllocRepo := newFifoTestStockAllocationRepo(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
fifoSvc := commonSvc.NewFifoService(db, stockAllocRepo, productWarehouseRepo, utils.Log)
|
||||
|
||||
registerRecordingUsable(t, fifoSvc)
|
||||
|
||||
key := fifo.StockableKey(fmt.Sprintf("TEST_STOCKABLE_%s_%d", sanitizeKey(t.Name()), time.Now().UnixNano()))
|
||||
if err := fifoSvc.RegisterStockable(fifo.StockableConfig{
|
||||
Key: key,
|
||||
Table: "test_fifo_stockables",
|
||||
Columns: fifo.StockableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
TotalQuantity: "total_qty",
|
||||
TotalUsedQuantity: "total_used_qty",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("register stockable: %v", err)
|
||||
}
|
||||
|
||||
svc := servicePkg.NewRecordingFIFOIntegrationService(
|
||||
recordingRepo.NewRecordingRepository(db),
|
||||
productWarehouseRepo,
|
||||
fifoSvc,
|
||||
)
|
||||
|
||||
return db, svc, fifoSvc, key
|
||||
}
|
||||
|
||||
func registerRecordingUsable(t *testing.T, fifoSvc commonSvc.FifoService) {
|
||||
t.Helper()
|
||||
err := fifoSvc.RegisterUsable(fifo.UsableConfig{
|
||||
Key: fifo.UsableKeyRecordingStock,
|
||||
Table: "recording_stocks",
|
||||
Columns: fifo.UsableColumns{
|
||||
ID: "id",
|
||||
ProductWarehouseID: "product_warehouse_id",
|
||||
UsageQuantity: "usage_qty",
|
||||
PendingQuantity: "pending_qty",
|
||||
CreatedAt: "created_at",
|
||||
},
|
||||
})
|
||||
if err != nil && !strings.Contains(strings.ToLower(err.Error()), "already registered") {
|
||||
t.Fatalf("register usable: %v", err)
|
||||
}
|
||||
if _, ok := fifo.Usable(fifo.UsableKeyRecordingStock); !ok {
|
||||
t.Fatal("recording stock usable key not registered")
|
||||
}
|
||||
}
|
||||
|
||||
func createProductWarehouseRow(t *testing.T, db *gorm.DB, qty float64) entity.ProductWarehouse {
|
||||
t.Helper()
|
||||
pw := entity.ProductWarehouse{
|
||||
ProductId: 1,
|
||||
WarehouseId: 1,
|
||||
Quantity: qty,
|
||||
// CreatedBy: 1,
|
||||
}
|
||||
if err := db.Create(&pw).Error; err != nil {
|
||||
t.Fatalf("create product warehouse: %v", err)
|
||||
}
|
||||
return pw
|
||||
}
|
||||
|
||||
func createRecordingStockRow(t *testing.T, db *gorm.DB, recordingID, productWarehouseID uint, desired float64) entity.RecordingStock {
|
||||
t.Helper()
|
||||
stock := entity.RecordingStock{
|
||||
RecordingId: recordingID,
|
||||
ProductWarehouseId: productWarehouseID,
|
||||
UsageQty: floatPtr(0),
|
||||
PendingQty: floatPtr(0),
|
||||
}
|
||||
if err := db.Create(&stock).Error; err != nil {
|
||||
t.Fatalf("create recording stock: %v", err)
|
||||
}
|
||||
stock.UsageQty = floatPtr(desired)
|
||||
return stock
|
||||
}
|
||||
|
||||
func createStockLot(t *testing.T, db *gorm.DB, productWarehouseID uint) testStockSource {
|
||||
t.Helper()
|
||||
lot := testStockSource{
|
||||
ProductWarehouseId: productWarehouseID,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
if err := db.Create(&lot).Error; err != nil {
|
||||
t.Fatalf("create stock lot: %v", err)
|
||||
}
|
||||
return lot
|
||||
}
|
||||
|
||||
func fetchRecordingStock(t *testing.T, db *gorm.DB, id uint) entity.RecordingStock {
|
||||
t.Helper()
|
||||
var stock entity.RecordingStock
|
||||
if err := db.First(&stock, id).Error; err != nil {
|
||||
t.Fatalf("fetch recording stock: %v", err)
|
||||
}
|
||||
return stock
|
||||
}
|
||||
|
||||
func fetchSingleAllocation(t *testing.T, db *gorm.DB, usableID uint) entity.StockAllocation {
|
||||
t.Helper()
|
||||
var alloc entity.StockAllocation
|
||||
if err := db.Where("usable_id = ?", usableID).Order("created_at ASC").First(&alloc).Error; err != nil {
|
||||
t.Fatalf("fetch allocation: %v", err)
|
||||
}
|
||||
return alloc
|
||||
}
|
||||
|
||||
func assertAllocationCount(t *testing.T, db *gorm.DB, expected int64) {
|
||||
t.Helper()
|
||||
var count int64
|
||||
if err := db.Model(&entity.StockAllocation{}).Count(&count).Error; err != nil {
|
||||
t.Fatalf("count allocations: %v", err)
|
||||
}
|
||||
if count != expected {
|
||||
t.Fatalf("expected %d allocations, got %d", expected, count)
|
||||
}
|
||||
}
|
||||
|
||||
func assertWarehouseQuantity(t *testing.T, db *gorm.DB, id uint, expected float64) {
|
||||
t.Helper()
|
||||
var pw entity.ProductWarehouse
|
||||
if err := db.First(&pw, id).Error; err != nil {
|
||||
t.Fatalf("fetch product warehouse: %v", err)
|
||||
}
|
||||
if mathAbs(pw.Quantity-expected) > 1e-6 {
|
||||
t.Fatalf("expected warehouse quantity %.3f, got %.3f", expected, pw.Quantity)
|
||||
}
|
||||
}
|
||||
|
||||
func assertFloatEqual(t *testing.T, expected float64, value *float64, msg string) {
|
||||
t.Helper()
|
||||
if value == nil {
|
||||
t.Fatalf("expected %s %.3f, got nil", msg, expected)
|
||||
}
|
||||
if mathAbs(*value-expected) > 1e-6 {
|
||||
t.Fatalf("%s: expected %.3f, got %.3f", msg, expected, *value)
|
||||
}
|
||||
}
|
||||
|
||||
func floatPtr(v float64) *float64 {
|
||||
p := new(float64)
|
||||
*p = v
|
||||
return p
|
||||
}
|
||||
|
||||
func mathAbs(v float64) float64 {
|
||||
if v < 0 {
|
||||
return -v
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func sanitizeKey(name string) string {
|
||||
if name == "" {
|
||||
return "CASE"
|
||||
}
|
||||
clean := strings.Map(func(r rune) rune {
|
||||
if (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') {
|
||||
return r
|
||||
}
|
||||
if r >= 'a' && r <= 'z' {
|
||||
return r - 32
|
||||
}
|
||||
return '_'
|
||||
}, name)
|
||||
return clean
|
||||
}
|
||||
|
||||
type fifoTestStockAllocationRepo struct {
|
||||
commonRepo.StockAllocationRepository
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func newFifoTestStockAllocationRepo(db *gorm.DB) commonRepo.StockAllocationRepository {
|
||||
return &fifoTestStockAllocationRepo{
|
||||
StockAllocationRepository: commonRepo.NewStockAllocationRepository(db),
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *fifoTestStockAllocationRepo) PatchOne(
|
||||
ctx context.Context,
|
||||
id uint,
|
||||
updates map[string]any,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
base := r.db
|
||||
|
||||
setClauses := make([]string, 0, len(updates))
|
||||
args := make([]any, 0, len(updates)+1)
|
||||
for column, value := range updates {
|
||||
colName := column
|
||||
if strings.EqualFold(column, "quantity") {
|
||||
colName = "qty"
|
||||
}
|
||||
setClauses = append(setClauses, fmt.Sprintf("%s = ?", colName))
|
||||
args = append(args, value)
|
||||
}
|
||||
args = append(args, id)
|
||||
sql := fmt.Sprintf("UPDATE stock_allocations SET %s WHERE id = ?", strings.Join(setClauses, ", "))
|
||||
|
||||
result := base.Exec(sql, args...)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *fifoTestStockAllocationRepo) ReleaseByUsable(
|
||||
ctx context.Context,
|
||||
usableType string,
|
||||
usableID uint,
|
||||
note *string,
|
||||
modifier func(*gorm.DB) *gorm.DB,
|
||||
) error {
|
||||
base := r.db
|
||||
|
||||
setClause := "status = ?, released_at = ?"
|
||||
args := []any{entity.StockAllocationStatusReleased, time.Now()}
|
||||
if note != nil {
|
||||
setClause += ", note = ?"
|
||||
args = append(args, *note)
|
||||
}
|
||||
args = append(args, usableType, usableID, entity.StockAllocationStatusActive)
|
||||
sql := fmt.Sprintf(
|
||||
"UPDATE stock_allocations SET %s WHERE usable_type = ? AND usable_id = ? AND status = ?",
|
||||
setClause,
|
||||
)
|
||||
|
||||
result := base.Exec(sql, args...)
|
||||
return result.Error
|
||||
}
|
||||
Reference in New Issue
Block a user