mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
codex/fix: purchase receivement error and recording doesn't show depletion/egg
This commit is contained in:
+19
-4
@@ -53,6 +53,24 @@ func (s productWarehouseService) withRelations(db *gorm.DB) *gorm.DB {
|
||||
Preload("ProjectFlockKandang.Chickins")
|
||||
}
|
||||
|
||||
func applyWarehouseSelectionFilter(db *gorm.DB, kandangID, locationID uint) *gorm.DB {
|
||||
switch {
|
||||
case kandangID != 0 && locationID != 0:
|
||||
return db.Where(
|
||||
"w_scope.location_id = ? AND (w_scope.type = ? OR w_scope.kandang_id = ?)",
|
||||
locationID,
|
||||
"LOKASI",
|
||||
kandangID,
|
||||
)
|
||||
case kandangID != 0:
|
||||
return db.Where("w_scope.kandang_id = ?", kandangID)
|
||||
case locationID != 0:
|
||||
return db.Where("w_scope.location_id = ?", locationID)
|
||||
default:
|
||||
return db
|
||||
}
|
||||
}
|
||||
|
||||
func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProductWarehouse, int64, error) {
|
||||
if err := s.Validate.Struct(params); err != nil {
|
||||
return nil, 0, err
|
||||
@@ -133,10 +151,7 @@ func (s productWarehouseService) GetAll(c *fiber.Ctx, params *validation.Query)
|
||||
db = db.Where("product_id = ?", params.ProductId)
|
||||
}
|
||||
|
||||
if params.KandangId != 0 {
|
||||
db = db.Joins("JOIN warehouses ON product_warehouses.warehouse_id = warehouses.id").
|
||||
Where("warehouses.kandang_id = ?", params.KandangId)
|
||||
}
|
||||
db = applyWarehouseSelectionFilter(db, params.KandangId, params.LocationId)
|
||||
|
||||
if params.WarehouseId != 0 {
|
||||
db = db.Where("warehouse_id = ?", params.WarehouseId)
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestApplyWarehouseSelectionFilterIncludesFarmAndSelectedKandangInLocation(t *testing.T) {
|
||||
db := setupProductWarehouseServiceTestDB(t)
|
||||
|
||||
var ids []uint
|
||||
err := applyWarehouseSelectionFilter(baseProductWarehouseSelectionQuery(db), 11, 101).
|
||||
Order("product_warehouses.id").
|
||||
Pluck("product_warehouses.id", &ids).Error
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
assertUintIDs(t, ids, []uint{1, 2})
|
||||
}
|
||||
|
||||
func TestApplyWarehouseSelectionFilterPreservesKandangOnlyBehavior(t *testing.T) {
|
||||
db := setupProductWarehouseServiceTestDB(t)
|
||||
|
||||
var ids []uint
|
||||
err := applyWarehouseSelectionFilter(baseProductWarehouseSelectionQuery(db), 11, 0).
|
||||
Order("product_warehouses.id").
|
||||
Pluck("product_warehouses.id", &ids).Error
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
assertUintIDs(t, ids, []uint{1})
|
||||
}
|
||||
|
||||
func TestApplyWarehouseSelectionFilterSupportsLocationOnlyQuery(t *testing.T) {
|
||||
db := setupProductWarehouseServiceTestDB(t)
|
||||
|
||||
var ids []uint
|
||||
err := applyWarehouseSelectionFilter(baseProductWarehouseSelectionQuery(db), 0, 101).
|
||||
Order("product_warehouses.id").
|
||||
Pluck("product_warehouses.id", &ids).Error
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
assertUintIDs(t, ids, []uint{1, 2, 3})
|
||||
}
|
||||
|
||||
func setupProductWarehouseServiceTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed opening sqlite db: %v", err)
|
||||
}
|
||||
|
||||
statements := []string{
|
||||
`CREATE TABLE warehouses (
|
||||
id INTEGER PRIMARY KEY,
|
||||
type TEXT NOT NULL,
|
||||
location_id INTEGER NULL,
|
||||
kandang_id INTEGER NULL,
|
||||
deleted_at TIMESTAMP NULL
|
||||
)`,
|
||||
`CREATE TABLE product_warehouses (
|
||||
id INTEGER PRIMARY KEY,
|
||||
warehouse_id INTEGER NOT NULL
|
||||
)`,
|
||||
`INSERT INTO warehouses (id, type, location_id, kandang_id, deleted_at) VALUES
|
||||
(1, 'KANDANG', 101, 11, NULL),
|
||||
(2, 'LOKASI', 101, NULL, NULL),
|
||||
(3, 'KANDANG', 101, 12, NULL),
|
||||
(4, 'LOKASI', 102, NULL, NULL)`,
|
||||
`INSERT INTO product_warehouses (id, warehouse_id) VALUES
|
||||
(1, 1),
|
||||
(2, 2),
|
||||
(3, 3),
|
||||
(4, 4)`,
|
||||
}
|
||||
|
||||
for _, stmt := range statements {
|
||||
if err := db.Exec(stmt).Error; err != nil {
|
||||
t.Fatalf("failed preparing schema: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func baseProductWarehouseSelectionQuery(db *gorm.DB) *gorm.DB {
|
||||
return db.Table("product_warehouses").
|
||||
Joins("JOIN warehouses w_scope ON product_warehouses.warehouse_id = w_scope.id").
|
||||
Where("w_scope.deleted_at IS NULL")
|
||||
}
|
||||
|
||||
func assertUintIDs(t *testing.T, got []uint, want []uint) {
|
||||
t.Helper()
|
||||
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("expected ids %v, got %v", want, got)
|
||||
}
|
||||
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Fatalf("expected ids %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
@@ -17,6 +17,7 @@ type Query struct {
|
||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||
ProductId uint `query:"product_id" validate:"omitempty,number,min=1"`
|
||||
WarehouseId uint `query:"warehouse_id" validate:"omitempty,number,min=1"`
|
||||
LocationId uint `query:"location_id" validate:"omitempty,number,min=1"`
|
||||
Flags string `query:"flags" validate:"omitempty"`
|
||||
KandangId uint `query:"kandang_id" validate:"omitempty,number,min=1"`
|
||||
TransferContext string `query:"transfer_context" validate:"omitempty,oneof=inventory_transfer"`
|
||||
|
||||
@@ -143,6 +143,17 @@ func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uin
|
||||
return r.DB().WithContext(ctx).Create(&items).Error
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) purchaseItemExists(ctx context.Context, purchaseID uint, itemID uint) (bool, error) {
|
||||
var count int64
|
||||
if err := r.DB().WithContext(ctx).
|
||||
Model(&entity.PurchaseItem{}).
|
||||
Where("purchase_id = ? AND id = ?", purchaseID, itemID).
|
||||
Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
type PurchasePricingUpdate struct {
|
||||
ItemID uint
|
||||
ProductID *uint
|
||||
@@ -197,7 +208,13 @@ func (r *PurchaseRepositoryImpl) UpdatePricing(
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
exists, err := r.purchaseItemExists(ctx, purchaseID, upd.ItemID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -251,7 +268,13 @@ func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
exists, err := r.purchaseItemExists(ctx, purchaseID, upd.ItemID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func TestUpdateReceivingDetailsAllowsNoOpUpdatesOnExistingItem(t *testing.T) {
|
||||
db := setupPurchaseRepositoryTestDB(t)
|
||||
repo := NewPurchaseRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
receivedAt := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
|
||||
travelNumber := "SJ-001"
|
||||
vehicleNumber := "B 1234 CD"
|
||||
|
||||
if err := db.WithContext(ctx).Create(&entity.PurchaseItem{
|
||||
Id: 10,
|
||||
PurchaseId: 1,
|
||||
ProductId: 2,
|
||||
WarehouseId: 3,
|
||||
SubQty: 10,
|
||||
TotalQty: 10,
|
||||
Price: 15000,
|
||||
TotalPrice: 150000,
|
||||
ReceivedDate: &receivedAt,
|
||||
TravelNumber: &travelNumber,
|
||||
VehicleNumber: &vehicleNumber,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("failed seeding purchase item: %v", err)
|
||||
}
|
||||
|
||||
pwID := uint(99)
|
||||
if err := repo.UpdateReceivingDetails(ctx, 1, []PurchaseReceivingUpdate{
|
||||
{
|
||||
ItemID: 10,
|
||||
ReceivedDate: &receivedAt,
|
||||
TravelNumber: &travelNumber,
|
||||
VehicleNumber: &vehicleNumber,
|
||||
ProductWarehouseID: &pwID,
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("expected no-op receive update to succeed, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateReceivingDetailsReturnsNotFoundForMissingItem(t *testing.T) {
|
||||
db := setupPurchaseRepositoryTestDB(t)
|
||||
repo := NewPurchaseRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
receivedAt := time.Date(2026, 4, 1, 0, 0, 0, 0, time.UTC)
|
||||
|
||||
err := repo.UpdateReceivingDetails(ctx, 1, []PurchaseReceivingUpdate{
|
||||
{
|
||||
ItemID: 999,
|
||||
ReceivedDate: &receivedAt,
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
t.Fatalf("expected gorm.ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePricingAllowsNoOpUpdatesOnExistingItem(t *testing.T) {
|
||||
db := setupPurchaseRepositoryTestDB(t)
|
||||
repo := NewPurchaseRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
if err := db.WithContext(ctx).Create(&entity.PurchaseItem{
|
||||
Id: 20,
|
||||
PurchaseId: 2,
|
||||
ProductId: 5,
|
||||
WarehouseId: 6,
|
||||
SubQty: 5,
|
||||
TotalQty: 5,
|
||||
Price: 10000,
|
||||
TotalPrice: 50000,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("failed seeding purchase item: %v", err)
|
||||
}
|
||||
|
||||
if err := repo.UpdatePricing(ctx, 2, []PurchasePricingUpdate{
|
||||
{
|
||||
ItemID: 20,
|
||||
Price: 10000,
|
||||
TotalPrice: 50000,
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("expected no-op pricing update to succeed, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePricingReturnsNotFoundForMissingItem(t *testing.T) {
|
||||
db := setupPurchaseRepositoryTestDB(t)
|
||||
repo := NewPurchaseRepository(db)
|
||||
ctx := context.Background()
|
||||
|
||||
err := repo.UpdatePricing(ctx, 2, []PurchasePricingUpdate{
|
||||
{
|
||||
ItemID: 777,
|
||||
Price: 10000,
|
||||
TotalPrice: 50000,
|
||||
},
|
||||
})
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
t.Fatalf("expected gorm.ErrRecordNotFound, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func setupPurchaseRepositoryTestDB(t *testing.T) *gorm.DB {
|
||||
t.Helper()
|
||||
|
||||
db, err := gorm.Open(sqlite.Open("file:"+t.Name()+"?mode=memory&cache=private"), &gorm.Config{})
|
||||
if err != nil {
|
||||
t.Fatalf("failed opening sqlite db: %v", err)
|
||||
}
|
||||
|
||||
statements := []string{
|
||||
`CREATE TABLE purchase_items (
|
||||
id INTEGER PRIMARY KEY,
|
||||
purchase_id INTEGER NOT NULL,
|
||||
product_id INTEGER NOT NULL,
|
||||
warehouse_id INTEGER NOT NULL,
|
||||
product_warehouse_id INTEGER NULL,
|
||||
project_flock_kandang_id INTEGER NULL,
|
||||
received_date TIMESTAMP NULL,
|
||||
travel_number TEXT NULL,
|
||||
travel_number_docs TEXT NULL,
|
||||
vehicle_number TEXT NULL,
|
||||
sub_qty NUMERIC(15,3) NOT NULL DEFAULT 0,
|
||||
total_qty NUMERIC(15,3) NOT NULL DEFAULT 0,
|
||||
total_used NUMERIC(15,3) NOT NULL DEFAULT 0,
|
||||
price NUMERIC(15,3) NOT NULL DEFAULT 0,
|
||||
total_price NUMERIC(15,3) NOT NULL DEFAULT 0,
|
||||
expense_nonstock_id INTEGER NULL
|
||||
)`,
|
||||
}
|
||||
for _, stmt := range statements {
|
||||
if err := db.Exec(stmt).Error; err != nil {
|
||||
t.Fatalf("failed preparing schema: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
Reference in New Issue
Block a user