Merge branch 'fix/BE-Document_s3' into 'development'

feat(BE): fix fifo system recording and uniformity dto

See merge request mbugroup/lti-api!129
This commit is contained in:
Hafizh A. Y.
2026-01-03 18:04:24 +00:00
21 changed files with 1009 additions and 104 deletions
@@ -187,10 +187,11 @@ func (r *BaseRepositoryImpl[T]) PatchOne(
updates map[string]any,
modifier func(*gorm.DB) *gorm.DB,
) error {
q := r.db.WithContext(ctx).Model(new(T)).Where("id = ?", id)
q := r.db.WithContext(ctx)
if modifier != nil {
q = modifier(q)
}
q = q.Model(new(T)).Where("id = ?", id)
result := q.Updates(updates)
if result.Error != nil {
@@ -6,6 +6,7 @@ import (
"fmt"
"mime"
"mime/multipart"
"net/url"
"path/filepath"
"strings"
"time"
@@ -305,6 +306,56 @@ func (s *documentService) PresignURL(ctx context.Context, document entity.Docume
return s.storage.PresignURL(ctx, document.Path, expires)
}
// ResolveDocumentURL normalizes a stored path or URL into a presigned URL.
func ResolveDocumentURL(
ctx context.Context,
svc DocumentService,
rawPath string,
expires time.Duration,
) (string, error) {
if svc == nil {
return "", nil
}
rawPath = strings.TrimSpace(rawPath)
if rawPath == "" {
return "", nil
}
key := rawPath
lower := strings.ToLower(rawPath)
if strings.HasPrefix(lower, "http://") || strings.HasPrefix(lower, "https://") {
key = extractS3KeyFromURL(rawPath)
if key == "" {
return "", nil
}
}
return svc.PresignURL(ctx, entity.Document{Path: key}, expires)
}
func extractS3KeyFromURL(raw string) string {
parsed, err := url.Parse(strings.TrimSpace(raw))
if err != nil {
return ""
}
path := strings.TrimPrefix(parsed.Path, "/")
if path == "" {
return ""
}
host := strings.ToLower(strings.TrimSpace(parsed.Host))
if strings.HasPrefix(host, "s3.") || strings.HasPrefix(host, "s3-") {
parts := strings.SplitN(path, "/", 2)
if len(parts) == 2 {
return parts[1]
}
return ""
}
return path
}
func (s *documentService) generateObjectKey(ext string) (string, error) {
normalizedExt := strings.TrimSpace(ext)
if normalizedExt != "" && !strings.HasPrefix(normalizedExt, ".") {
@@ -192,7 +192,6 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
if req.Quantity < 0 {
return nil, errors.New("quantity must be zero or greater")
}
cfg, ok := fifo.Usable(req.UsableKey)
if !ok {
return nil, fmt.Errorf("usable %q is not registered", req.UsableKey)
@@ -220,7 +219,6 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
currentPending := ctxRow.PendingQty
currentTotal := currentUsage + currentPending
delta := req.Quantity - currentTotal
var (
usageDelta float64
pendingDelta float64
@@ -285,7 +283,6 @@ func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*St
result.ReleasedQuantity = releasedAmount
result.UsageQuantity = currentUsage + usageDelta
result.PendingQuantity = currentPending + pendingDelta
return nil
})
if err != nil {
@@ -299,7 +296,6 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
return errors.New("usable key and id are required")
}
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
cfg, ok := fifo.Usable(req.UsableKey)
if !ok {
@@ -310,7 +306,6 @@ func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest)
if err != nil {
return err
}
var usageDelta, pendingDelta float64
if ctxRow.UsageQty > 0 {
if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty); err != nil {
@@ -715,7 +710,7 @@ func (s *fifoService) releaseUsagePortion(
}
} else {
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
"quantity": allocation.Qty - releaseAmt,
"qty": allocation.Qty - releaseAmt,
}, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
@@ -0,0 +1,126 @@
CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$
DECLARE
fk record;
child_column text;
parent_column text;
parent_value text;
child_has_deleted_at boolean;
ref_exists boolean;
sql text;
BEGIN
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
FOR fk IN
SELECT conrelid::regclass AS child_table,
conkey AS child_cols,
confkey AS parent_cols,
confdeltype
FROM pg_constraint
WHERE contype = 'f'
AND confrelid = TG_RELID
LOOP
IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1
OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN
RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table;
CONTINUE;
END IF;
SELECT attname INTO child_column
FROM pg_attribute
WHERE attrelid = fk.child_table
AND attnum = fk.child_cols[1]
AND NOT attisdropped;
SELECT attname INTO parent_column
FROM pg_attribute
WHERE attrelid = TG_RELID
AND attnum = fk.parent_cols[1]
AND NOT attisdropped;
EXECUTE format('SELECT ($1).%I', parent_column)
INTO parent_value
USING OLD;
SELECT EXISTS (
SELECT 1
FROM pg_attribute
WHERE attrelid = fk.child_table
AND attname = 'deleted_at'
AND NOT attisdropped
) INTO child_has_deleted_at;
IF fk.confdeltype IN ('r', 'a') THEN
sql := format(
'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1 %s)',
fk.child_table,
child_column,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql INTO ref_exists USING parent_value;
IF ref_exists THEN
RAISE EXCEPTION 'Cannot soft delete %, still referenced by %',
TG_TABLE_NAME, fk.child_table;
END IF;
ELSIF fk.confdeltype = 'n' THEN
sql := format(
'UPDATE %s SET %I = NULL WHERE %I = $1 %s',
fk.child_table,
child_column,
child_column,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql USING parent_value;
ELSIF fk.confdeltype = 'c' THEN
IF child_has_deleted_at THEN
sql := format(
'UPDATE %s SET deleted_at = NOW() WHERE %I = $1 AND deleted_at IS NULL',
fk.child_table,
child_column
);
EXECUTE sql USING parent_value;
ELSE
sql := format(
'DELETE FROM %s WHERE %I = $1',
fk.child_table,
child_column
);
EXECUTE sql USING parent_value;
END IF;
ELSIF fk.confdeltype = 'd' THEN
sql := format(
'UPDATE %s SET %I = DEFAULT WHERE %I = $1 %s',
fk.child_table,
child_column,
child_column,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql USING parent_value;
END IF;
END LOOP;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DO $$
DECLARE
r record;
trigger_name text;
BEGIN
FOR r IN
SELECT table_schema, table_name
FROM information_schema.columns
WHERE column_name = 'deleted_at'
AND table_schema = 'public'
GROUP BY table_schema, table_name
LOOP
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
EXECUTE format(
'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()',
trigger_name,
r.table_schema,
r.table_name
);
END LOOP;
END $$;
@@ -0,0 +1,142 @@
CREATE OR REPLACE FUNCTION soft_delete_handle_fk() RETURNS TRIGGER AS $$
DECLARE
fk record;
child_column text;
parent_column text;
parent_value text;
child_has_deleted_at boolean;
ref_exists boolean;
sql text;
child_type text;
BEGIN
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL THEN
FOR fk IN
SELECT conrelid::regclass AS child_table,
conkey AS child_cols,
confkey AS parent_cols,
confdeltype
FROM pg_constraint
WHERE contype = 'f'
AND confrelid = TG_RELID
LOOP
IF array_length(fk.child_cols, 1) IS DISTINCT FROM 1
OR array_length(fk.parent_cols, 1) IS DISTINCT FROM 1 THEN
RAISE NOTICE 'soft_delete_handle_fk skipped composite fk on %', fk.child_table;
CONTINUE;
END IF;
SELECT attname INTO child_column
FROM pg_attribute
WHERE attrelid = fk.child_table
AND attnum = fk.child_cols[1]
AND NOT attisdropped;
SELECT attname INTO parent_column
FROM pg_attribute
WHERE attrelid = TG_RELID
AND attnum = fk.parent_cols[1]
AND NOT attisdropped;
SELECT format_type(atttypid, atttypmod) INTO child_type
FROM pg_attribute
WHERE attrelid = fk.child_table
AND attname = child_column
AND NOT attisdropped;
IF child_type IS NULL THEN
child_type := 'text';
END IF;
EXECUTE format('SELECT ($1).%I', parent_column)
INTO parent_value
USING OLD;
SELECT EXISTS (
SELECT 1
FROM pg_attribute
WHERE attrelid = fk.child_table
AND attname = 'deleted_at'
AND NOT attisdropped
) INTO child_has_deleted_at;
IF fk.confdeltype IN ('r', 'a') THEN
sql := format(
'SELECT EXISTS (SELECT 1 FROM %s WHERE %I = $1::%s %s)',
fk.child_table,
child_column,
child_type,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql INTO ref_exists USING parent_value;
IF ref_exists THEN
RAISE EXCEPTION 'Cannot soft delete %, still referenced by %',
TG_TABLE_NAME, fk.child_table;
END IF;
ELSIF fk.confdeltype = 'n' THEN
sql := format(
'UPDATE %s SET %I = NULL WHERE %I = $1::%s %s',
fk.child_table,
child_column,
child_column,
child_type,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql USING parent_value;
ELSIF fk.confdeltype = 'c' THEN
IF child_has_deleted_at THEN
sql := format(
'UPDATE %s SET deleted_at = NOW() WHERE %I = $1::%s AND deleted_at IS NULL',
fk.child_table,
child_column,
child_type
);
EXECUTE sql USING parent_value;
ELSE
sql := format(
'DELETE FROM %s WHERE %I = $1::%s',
fk.child_table,
child_column,
child_type
);
EXECUTE sql USING parent_value;
END IF;
ELSIF fk.confdeltype = 'd' THEN
sql := format(
'UPDATE %s SET %I = DEFAULT WHERE %I = $1::%s %s',
fk.child_table,
child_column,
child_column,
child_type,
CASE WHEN child_has_deleted_at THEN 'AND deleted_at IS NULL' ELSE '' END
);
EXECUTE sql USING parent_value;
END IF;
END LOOP;
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
DO $$
DECLARE
r record;
trigger_name text;
BEGIN
FOR r IN
SELECT table_schema, table_name
FROM information_schema.columns
WHERE column_name = 'deleted_at'
AND table_schema = 'public'
GROUP BY table_schema, table_name
LOOP
trigger_name := format('trg_soft_delete_fk_%s', r.table_name);
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %I.%I', trigger_name, r.table_schema, r.table_name);
EXECUTE format(
'CREATE TRIGGER %I BEFORE UPDATE OF deleted_at ON %I.%I FOR EACH ROW EXECUTE FUNCTION soft_delete_handle_fk()',
trigger_name,
r.table_schema,
r.table_name
);
END LOOP;
END $$;
@@ -0,0 +1,86 @@
BEGIN;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_project_flock_kandang_uniformity_project_flock_kandang'
) THEN
ALTER TABLE project_flock_kandang_uniformity
DROP CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang;
END IF;
END $$;
ALTER TABLE project_flock_kandang_uniformity
ADD CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs (id)
ON DELETE RESTRICT ON UPDATE CASCADE;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_tables
WHERE tablename = 'project_budgets'
) THEN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_project_budgets_project_flock_id'
) THEN
ALTER TABLE project_budgets
DROP CONSTRAINT fk_project_budgets_project_flock_id;
END IF;
ALTER TABLE project_budgets
ADD CONSTRAINT fk_project_budgets_project_flock_id
FOREIGN KEY (project_flock_id)
REFERENCES project_flocks(id);
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_tables
WHERE tablename = 'project_flock_kandang_uniformity'
) THEN
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'project_flock_kandang_uniformity'
AND column_name = 'created_at'
) THEN
ALTER TABLE project_flock_kandang_uniformity
ADD COLUMN created_at TIMESTAMPTZ DEFAULT NOW();
END IF;
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'project_flock_kandang_uniformity'
AND column_name = 'updated_at'
) THEN
ALTER TABLE project_flock_kandang_uniformity
ADD COLUMN updated_at TIMESTAMPTZ DEFAULT NOW();
END IF;
IF NOT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'project_flock_kandang_uniformity'
AND column_name = 'deleted_at'
) THEN
ALTER TABLE project_flock_kandang_uniformity
ADD COLUMN deleted_at TIMESTAMPTZ;
END IF;
END IF;
END $$;
COMMIT;
@@ -0,0 +1,90 @@
BEGIN;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_project_flock_kandang_uniformity_project_flock_kandang'
) THEN
ALTER TABLE project_flock_kandang_uniformity
DROP CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang;
END IF;
END $$;
ALTER TABLE project_flock_kandang_uniformity
ADD CONSTRAINT fk_project_flock_kandang_uniformity_project_flock_kandang
FOREIGN KEY (project_flock_kandang_id)
REFERENCES project_flock_kandangs (id)
ON DELETE CASCADE ON UPDATE CASCADE;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_tables
WHERE tablename = 'project_budgets'
) THEN
IF EXISTS (
SELECT 1
FROM pg_constraint
WHERE conname = 'fk_project_budgets_project_flock_id'
) THEN
ALTER TABLE project_budgets
DROP CONSTRAINT fk_project_budgets_project_flock_id;
END IF;
ALTER TABLE project_budgets
ADD CONSTRAINT fk_project_budgets_project_flock_id
FOREIGN KEY (project_flock_id)
REFERENCES project_flocks(id)
ON DELETE CASCADE ON UPDATE CASCADE;
END IF;
END $$;
DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM pg_trigger
WHERE tgname = 'trg_soft_delete_fk_project_flock_kandang_uniformity'
) THEN
DROP TRIGGER trg_soft_delete_fk_project_flock_kandang_uniformity
ON project_flock_kandang_uniformity;
END IF;
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'project_flock_kandang_uniformity'
AND column_name = 'created_at'
) THEN
ALTER TABLE project_flock_kandang_uniformity
DROP COLUMN created_at;
END IF;
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'project_flock_kandang_uniformity'
AND column_name = 'updated_at'
) THEN
ALTER TABLE project_flock_kandang_uniformity
DROP COLUMN updated_at;
END IF;
IF EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_schema = 'public'
AND table_name = 'project_flock_kandang_uniformity'
AND column_name = 'deleted_at'
) THEN
ALTER TABLE project_flock_kandang_uniformity
DROP COLUMN deleted_at;
END IF;
END $$;
COMMIT;
@@ -1,10 +1,6 @@
package entities
import (
"time"
"gorm.io/gorm"
)
import "time"
type ProjectFlockKandangUniformity struct {
Id uint `gorm:"primaryKey"`
@@ -18,9 +14,6 @@ type ProjectFlockKandangUniformity struct {
UniformQty float64 `gorm:"type:numeric(15,3)"`
NotUniformQty float64 `gorm:"type:numeric(15,3)"`
UniformDate *time.Time `gorm:"type:timestamptz"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
CreatedBy uint `gorm:"not null"`
ProjectFlockKandang ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
@@ -194,13 +194,17 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
StockLogId: newLog.Id,
ProductWarehouseId: productWarehouse.Id,
}
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
s.Log.Errorf("Failed to create adjustment stock: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
}
if transactionType == string(utils.StockLogTransactionTypeIncrease) {
// Adjustment INCREASE → Replenish stock (Stockable)
note := fmt.Sprintf("Stock Adjustment IN #%d", newLog.Id)
replenishResult, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
_, err := s.FifoSvc.Replenish(ctx, common.StockReplenishRequest{
StockableKey: "ADJUSTMENT_IN",
StockableID: newLog.Id,
StockableID: adjustmentStock.Id,
ProductWarehouseID: uint(productWarehouse.Id),
Quantity: req.Quantity,
Note: &note,
@@ -210,15 +214,11 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock via FIFO: %v", err))
}
// Update stockable tracking fields
adjustmentStock.TotalQty = replenishResult.AddedQuantity
adjustmentStock.TotalUsed = 0
} else {
// Adjustment DECREASE → Consume stock (Usable)
consumeResult, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
_, err := s.FifoSvc.Consume(ctx, common.StockConsumeRequest{
UsableKey: "ADJUSTMENT_OUT",
UsableID: newLog.Id,
UsableID: adjustmentStock.Id,
ProductWarehouseID: uint(productWarehouse.Id),
Quantity: req.Quantity,
AllowPending: false, // Don't allow pending for adjustment
@@ -227,16 +227,6 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Failed to consume stock via FIFO: %v", err))
}
// Update usable tracking fields
adjustmentStock.UsageQty = consumeResult.UsageQuantity
adjustmentStock.PendingQty = consumeResult.PendingQuantity
}
// Save AdjustmentStock record
if err := s.AdjustmentStockRepository.WithTx(tx).CreateOne(ctx, adjustmentStock, nil); err != nil {
s.Log.Errorf("Failed to create adjustment stock: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create adjustment stock record")
}
// Update ProductWarehouse quantity (for backward compatibility/reporting)
@@ -22,6 +22,7 @@ import (
pfutils "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/utils"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/validations"
recordingRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
uniformityRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/uniformities/repositories"
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
@@ -866,6 +867,14 @@ func (s projectflockService) detachKandangs(ctx context.Context, dbTransaction *
}
if len(pfkIDs) > 0 {
uniformityRepo := uniformityRepository.NewUniformityRepository(s.Repository.DB())
if dbTransaction != nil {
uniformityRepo = uniformityRepository.NewUniformityRepository(dbTransaction)
}
if err := uniformityRepo.DeleteByProjectFlockKandangIDs(ctx, pfkIDs); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to remove uniformity data for project flock kandang")
}
pwRepo := s.ProductWarehouseRepo
if dbTransaction != nil {
pwRepo = productWarehouseRepository.NewProductWarehouseRepository(dbTransaction)
@@ -295,16 +295,17 @@ func (r *RecordingRepositoryImpl) GetTotalChickinByProjectFlockKandang(tx *gorm.
func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID uint) (float64, error) {
var rows []struct {
UsageQty float64
TotalQty float64
UomName string
}
if err := tx.
Table("recording_stocks").
Select("COALESCE(recording_stocks.usage_qty, 0) AS usage_qty, LOWER(uoms.name) AS uom_name").
Select("COALESCE(recording_stocks.usage_qty, 0) + COALESCE(recording_stocks.pending_qty, 0) AS total_qty, LOWER(uoms.name) AS uom_name").
Joins("JOIN product_warehouses ON product_warehouses.id = recording_stocks.product_warehouse_id").
Joins("JOIN products ON products.id = product_warehouses.product_id").
Joins("JOIN uoms ON uoms.id = products.uom_id").
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = ? AND UPPER(flags.name) = ?", entity.FlagableTypeProduct, "PAKAN").
Where("recording_stocks.recording_id = ?", recordingID).
Scan(&rows).Error; err != nil {
return 0, err
@@ -312,16 +313,16 @@ func (r *RecordingRepositoryImpl) GetFeedUsageInGrams(tx *gorm.DB, recordingID u
var total float64
for _, row := range rows {
if row.UsageQty <= 0 {
if row.TotalQty <= 0 {
continue
}
switch strings.TrimSpace(row.UomName) {
case "kilogram", "kg", "kilograms", "kilo":
total += row.UsageQty * 1000
total += row.TotalQty * 1000
case "gram", "g", "grams":
total += row.UsageQty
total += row.TotalQty
default:
total += row.UsageQty
total += row.TotalQty
}
}
return total, nil
@@ -229,7 +229,13 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
CreatedBy: actorID,
}
if err := s.Repository.CreateOne(ctx, &createdRecording, func(*gorm.DB) *gorm.DB { return tx }); err != nil {
createTx := tx.WithContext(ctx).Select(
"ProjectFlockKandangId",
"RecordDatetime",
"Day",
"CreatedBy",
)
if err := createTx.Create(&createdRecording).Error; err != nil {
if errors.Is(err, gorm.ErrDuplicatedKey) {
return fiber.NewError(
fiber.StatusBadRequest,
@@ -241,11 +247,13 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
}
mappedStocks := recordingutil.MapStocks(createdRecording.Id, req.Stocks)
stockDesired := resetStockQuantitiesForFIFO(mappedStocks, s.FifoSvc != nil)
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
s.Log.Errorf("Failed to persist stocks: %+v", err)
return err
}
applyStockDesiredQuantities(mappedStocks, stockDesired, s.FifoSvc != nil)
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
return err
}
@@ -299,9 +307,11 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
ctx := c.Context()
var recordingEntity *entity.Recording
var updatedRecording *entity.Recording
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
recording, err := s.Repository.GetByID(ctx, id, func(db *gorm.DB) *gorm.DB {
return s.Repository.WithRelations(tx)
repoTx := s.Repository.WithTx(tx)
recording, err := repoTx.GetByID(ctx, id, func(db *gorm.DB) *gorm.DB {
return s.Repository.WithRelations(db)
})
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -316,6 +326,42 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
hasDepletionChanges := req.Depletions != nil
hasEggChanges := req.Eggs != nil
var existingStocks []entity.RecordingStock
if hasStockChanges {
existingStocks, err = s.Repository.ListStocks(tx, recordingEntity.Id)
if err != nil {
s.Log.Errorf("Failed to list existing stocks: %+v", err)
return err
}
if stocksMatch(existingStocks, req.Stocks) {
hasStockChanges = false
}
}
var existingDepletions []entity.RecordingDepletion
if hasDepletionChanges {
existingDepletions, err = s.Repository.ListDepletions(tx, recordingEntity.Id)
if err != nil {
s.Log.Errorf("Failed to list existing depletions: %+v", err)
return err
}
if depletionsMatch(existingDepletions, req.Depletions) {
hasDepletionChanges = false
}
}
var existingEggs []entity.RecordingEgg
if hasEggChanges {
existingEggs, err = s.Repository.ListEggs(tx, recordingEntity.Id)
if err != nil {
s.Log.Errorf("Failed to list existing eggs: %+v", err)
return err
}
if eggsMatch(existingEggs, req.Eggs) {
hasEggChanges = false
}
}
if !hasStockChanges && !hasDepletionChanges && !hasEggChanges {
return nil
}
@@ -347,39 +393,12 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
}
if hasStockChanges {
existingStocks, err := s.Repository.ListStocks(tx, recordingEntity.Id)
if err != nil {
s.Log.Errorf("Failed to list existing stocks: %+v", err)
return err
}
if err := s.releaseRecordingStocks(ctx, tx, existingStocks); err != nil {
return err
}
if err := s.Repository.DeleteStocks(tx, recordingEntity.Id); err != nil {
s.Log.Errorf("Failed to clear stocks: %+v", err)
return err
}
mappedStocks := recordingutil.MapStocks(recordingEntity.Id, req.Stocks)
if err := s.Repository.CreateStocks(tx, mappedStocks); err != nil {
s.Log.Errorf("Failed to update stocks: %+v", err)
return err
}
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
if err := s.syncRecordingStocks(ctx, tx, recordingEntity.Id, existingStocks, req.Stocks); err != nil {
return err
}
}
if hasDepletionChanges {
existingDepletions, err := s.Repository.ListDepletions(tx, recordingEntity.Id)
if err != nil {
s.Log.Errorf("Failed to list existing depletions: %+v", err)
return err
}
if err := s.Repository.DeleteDepletions(tx, recordingEntity.Id); err != nil {
s.Log.Errorf("Failed to clear depletions: %+v", err)
return err
@@ -398,12 +417,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
}
if hasEggChanges {
existingEggs, err := s.Repository.ListEggs(tx, recordingEntity.Id)
if err != nil {
s.Log.Errorf("Failed to list existing eggs: %+v", err)
return err
}
if err := s.Repository.DeleteEggs(tx, recordingEntity.Id); err != nil {
s.Log.Errorf("Failed to clear eggs: %+v", err)
return err
@@ -421,7 +434,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
}
}
if hasStockChanges || hasDepletionChanges {
if hasStockChanges || hasDepletionChanges || hasEggChanges {
if err := s.computeAndUpdateMetrics(ctx, tx, recordingEntity); err != nil {
s.Log.Errorf("Failed to recompute recording metrics: %+v", err)
return err
@@ -470,13 +483,31 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
}
}
updated, err := repoTx.GetByID(ctx, recordingEntity.Id, func(db *gorm.DB) *gorm.DB {
return s.Repository.WithRelations(db)
})
if err != nil {
s.Log.Errorf("Failed to reload recording %d after update: %+v", recordingEntity.Id, err)
return err
}
updatedRecording = updated
return nil
})
if transactionErr != nil {
return nil, transactionErr
}
return s.GetOne(c, id)
if updatedRecording == nil {
return s.GetOne(c, id)
}
if err := s.attachLatestApproval(ctx, updatedRecording); err != nil {
return nil, err
}
if err := s.attachProductionStandard(ctx, updatedRecording); err != nil {
return nil, err
}
return updatedRecording, nil
}
func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error) {
@@ -654,12 +685,17 @@ func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.
if stock.UsageQty != nil {
desired = *stock.UsageQty
}
var pending float64
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
desiredTotal := desired + pending
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: recordingStockUsableKey,
UsableID: stock.Id,
ProductWarehouseID: stock.ProductWarehouseId,
Quantity: desired,
Quantity: desiredTotal,
AllowPending: true,
Tx: tx,
})
@@ -745,6 +781,288 @@ func (s *recordingService) adjustProductWarehouseQuantities(ctx context.Context,
return s.ProductWarehouseRepo.AdjustQuantities(ctx, deltas, func(*gorm.DB) *gorm.DB { return tx })
}
type desiredStock struct {
Usage float64
Pending float64
}
func resetStockQuantitiesForFIFO(stocks []entity.RecordingStock, enabled bool) []desiredStock {
desired := make([]desiredStock, len(stocks))
for i := range stocks {
if stocks[i].UsageQty != nil {
desired[i].Usage = *stocks[i].UsageQty
}
if stocks[i].PendingQty != nil {
desired[i].Pending = *stocks[i].PendingQty
}
if !enabled {
continue
}
zero := 0.0
stocks[i].UsageQty = &zero
stocks[i].PendingQty = &zero
}
return desired
}
func applyStockDesiredQuantities(stocks []entity.RecordingStock, desired []desiredStock, enabled bool) {
if !enabled {
return
}
for i := range stocks {
if i >= len(desired) {
break
}
usage := desired[i].Usage
pending := desired[i].Pending
stocks[i].UsageQty = &usage
stocks[i].PendingQty = &pending
}
}
func (s *recordingService) syncRecordingStocks(
ctx context.Context,
tx *gorm.DB,
recordingID uint,
existing []entity.RecordingStock,
incoming []validation.Stock,
) error {
if s.FifoSvc == nil {
if err := s.Repository.DeleteStocks(tx, recordingID); err != nil {
return err
}
mapped := recordingutil.MapStocks(recordingID, incoming)
return s.Repository.CreateStocks(tx, mapped)
}
existingByWarehouse := make(map[uint][]entity.RecordingStock)
for _, stock := range existing {
existingByWarehouse[stock.ProductWarehouseId] = append(existingByWarehouse[stock.ProductWarehouseId], stock)
}
stocksToConsume := make([]entity.RecordingStock, 0, len(incoming))
for _, item := range incoming {
list := existingByWarehouse[item.ProductWarehouseId]
var stock entity.RecordingStock
if len(list) > 0 {
stock = list[0]
existingByWarehouse[item.ProductWarehouseId] = list[1:]
} else {
zero := 0.0
stock = entity.RecordingStock{
RecordingId: recordingID,
ProductWarehouseId: item.ProductWarehouseId,
UsageQty: &zero,
PendingQty: &zero,
}
if err := tx.Create(&stock).Error; err != nil {
return err
}
}
desired := item.Qty
stock.UsageQty = &desired
if item.PendingQty != nil {
pending := *item.PendingQty
stock.PendingQty = &pending
}
stocksToConsume = append(stocksToConsume, stock)
}
var leftovers []entity.RecordingStock
for _, list := range existingByWarehouse {
leftovers = append(leftovers, list...)
}
if len(leftovers) > 0 {
if err := s.releaseRecordingStocks(ctx, tx, leftovers); err != nil {
return err
}
ids := make([]uint, 0, len(leftovers))
for _, stock := range leftovers {
if stock.Id != 0 {
ids = append(ids, stock.Id)
}
}
if len(ids) > 0 {
if err := tx.Where("id IN ?", ids).Delete(&entity.RecordingStock{}).Error; err != nil {
return err
}
}
}
if len(stocksToConsume) == 0 {
return nil
}
return s.consumeRecordingStocks(ctx, tx, stocksToConsume)
}
type eggTotals struct {
Qty int
Weight float64
}
type stockTotals struct {
Usage float64
Pending float64
Total float64
}
func summarizeExistingStocks(stocks []entity.RecordingStock) map[uint]stockTotals {
totals := make(map[uint]stockTotals)
for _, stock := range stocks {
var usage float64
var pending float64
if stock.UsageQty != nil {
usage = *stock.UsageQty
}
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
current := totals[stock.ProductWarehouseId]
current.Usage += usage
current.Pending += pending
current.Total += usage + pending
totals[stock.ProductWarehouseId] = current
}
return totals
}
func summarizeIncomingStocks(stocks []validation.Stock) map[uint]stockTotals {
totals := make(map[uint]stockTotals)
for _, stock := range stocks {
var pending float64
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
current := totals[stock.ProductWarehouseId]
current.Usage += stock.Qty
current.Pending += pending
current.Total += stock.Qty + pending
totals[stock.ProductWarehouseId] = current
}
return totals
}
func stocksMatch(existing []entity.RecordingStock, incoming []validation.Stock) bool {
hasPending := false
for _, item := range incoming {
if item.PendingQty != nil {
hasPending = true
break
}
}
existingUsage := make(map[uint]float64)
existingTotal := make(map[uint]float64)
for _, stock := range existing {
var usage float64
var pending float64
if stock.UsageQty != nil {
usage = *stock.UsageQty
}
if stock.PendingQty != nil {
pending = *stock.PendingQty
}
existingUsage[stock.ProductWarehouseId] += usage
existingTotal[stock.ProductWarehouseId] += usage + pending
}
incomingUsage := make(map[uint]float64)
incomingTotal := make(map[uint]float64)
for _, item := range incoming {
var pending float64
if item.PendingQty != nil {
pending = *item.PendingQty
}
incomingUsage[item.ProductWarehouseId] += item.Qty
incomingTotal[item.ProductWarehouseId] += item.Qty + pending
}
if hasPending {
return floatMapsMatch(existingTotal, incomingTotal)
}
return floatMapsMatch(existingUsage, incomingUsage)
}
func depletionsMatch(existing []entity.RecordingDepletion, incoming []validation.Depletion) bool {
existingTotals := make(map[uint]float64)
for _, dep := range existing {
existingTotals[dep.ProductWarehouseId] += dep.Qty
}
incomingTotals := make(map[uint]float64)
for _, dep := range incoming {
incomingTotals[dep.ProductWarehouseId] += dep.Qty
}
return floatMapsMatch(existingTotals, incomingTotals)
}
func eggsMatch(existing []entity.RecordingEgg, incoming []validation.Egg) bool {
existingTotals := make(map[uint]eggTotals)
for _, egg := range existing {
weight := 0.0
if egg.Weight != nil {
weight = *egg.Weight
}
current := existingTotals[egg.ProductWarehouseId]
current.Qty += egg.Qty
current.Weight += float64(egg.Qty) * weight
existingTotals[egg.ProductWarehouseId] = current
}
incomingTotals := make(map[uint]eggTotals)
for _, egg := range incoming {
weight := 0.0
if egg.Weight != nil {
weight = *egg.Weight
}
current := incomingTotals[egg.ProductWarehouseId]
current.Qty += egg.Qty
current.Weight += float64(egg.Qty) * weight
incomingTotals[egg.ProductWarehouseId] = current
}
if len(existingTotals) != len(incomingTotals) {
return false
}
for key, existingTotal := range existingTotals {
incomingTotal, ok := incomingTotals[key]
if !ok {
return false
}
if existingTotal.Qty != incomingTotal.Qty {
return false
}
if !floatNearlyEqual(existingTotal.Weight, incomingTotal.Weight) {
return false
}
}
return true
}
func floatMapsMatch(a, b map[uint]float64) bool {
if len(a) != len(b) {
return false
}
for key, value := range a {
other, ok := b[key]
if !ok {
return false
}
if !floatNearlyEqual(value, other) {
return false
}
}
return true
}
func floatNearlyEqual(a, b float64) bool {
return math.Abs(a-b) <= 0.000001
}
func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm.DB, recording *entity.Recording) error {
day := 0
if recording.Day != nil {
@@ -93,6 +93,10 @@ func (u *UniformityController) GetOne(c *fiber.Ctx) error {
Uniformity: result.Uniformity,
Cv: result.Cv,
}
document, documentURL, err = u.UniformityService.GetDocumentInfo(c, id)
if err != nil {
return err
}
}
standard, err := u.UniformityService.GetStandard(c, result)
@@ -74,7 +74,6 @@ type UniformityListDTO struct {
MeanDown float64 `json:"mean_down"`
StandardMeanWeight *float64 `json:"standard_mean_weight"`
StandardUniformity *float64 `json:"standard_uniformity"`
CreatedAt time.Time `json:"created_at"`
CreatedBy uint `json:"created_by"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
}
@@ -154,7 +153,6 @@ func ToUniformityListDTOs(items []entity.ProjectFlockKandangUniformity) []Unifor
UniformQty: item.UniformQty,
MeanUp: item.MeanUp,
MeanDown: item.MeanDown,
CreatedAt: item.CreatedAt,
CreatedBy: item.CreatedBy,
LatestApproval: latestApproval,
}
@@ -1,6 +1,8 @@
package repository
import (
"context"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
@@ -8,6 +10,7 @@ import (
type UniformityRepository interface {
repository.BaseRepository[entity.ProjectFlockKandangUniformity]
DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error
}
type UniformityRepositoryImpl struct {
@@ -19,3 +22,13 @@ func NewUniformityRepository(db *gorm.DB) UniformityRepository {
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlockKandangUniformity](db),
}
}
func (r *UniformityRepositoryImpl) DeleteByProjectFlockKandangIDs(ctx context.Context, projectFlockKandangIDs []uint) error {
if len(projectFlockKandangIDs) == 0 {
return nil
}
return r.DB().WithContext(ctx).
Unscoped().
Where("project_flock_kandang_id IN ?", projectFlockKandangIDs).
Delete(&entity.ProjectFlockKandangUniformity{}).Error
}
@@ -39,6 +39,7 @@ type UniformityService interface {
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlockKandangUniformity, error)
ParseBodyWeightExcel(ctx *fiber.Ctx, file *multipart.FileHeader) ([]BodyWeightExcelRow, error)
ComputeUniformity(rows []BodyWeightExcelRow) (UniformityCalculation, error)
GetDocumentInfo(ctx *fiber.Ctx, uniformityID uint) (*entity.Document, string, error)
CalculateUniformityFromDocument(ctx *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, string, error)
}
@@ -98,7 +99,7 @@ func (s uniformityService) GetAll(c *fiber.Ctx, params *validation.Query) ([]ent
if params.Week != 0 {
db = db.Where("week = ?", params.Week)
}
return db.Order("uniform_date DESC").Order("created_at DESC")
return db.Order("uniform_date DESC").Order("id DESC")
})
if err != nil {
@@ -592,28 +593,19 @@ func (s uniformityService) ComputeUniformity(rows []BodyWeightExcelRow) (Uniform
return computeUniformity(rows)
}
func (s uniformityService) GetDocumentInfo(c *fiber.Ctx, uniformityID uint) (*entity.Document, string, error) {
return s.fetchUniformityDocument(c.Context(), uniformityID, true)
}
func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniformityID uint) (UniformityCalculation, *entity.Document, string, error) {
if s.DocumentSvc == nil {
return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
}
documents, err := s.DocumentSvc.ListByTarget(c.Context(), "UNIFORMITY", uint64(uniformityID))
document, url, err := s.fetchUniformityDocument(c.Context(), uniformityID, false)
if err != nil {
return UniformityCalculation{}, nil, "", err
}
if len(documents) == 0 {
if document == nil || url == "" {
return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
}
document := documents[0]
url, err := s.DocumentSvc.PresignURL(c.Context(), document, 15*time.Minute)
if err != nil {
return UniformityCalculation{}, nil, "", err
}
if url == "" {
return UniformityCalculation{}, nil, "", fiber.NewError(fiber.StatusBadRequest, "Uniformity document URL not available")
}
req, err := http.NewRequestWithContext(c.Context(), http.MethodGet, url, nil)
if err != nil {
return UniformityCalculation{}, nil, "", err
@@ -638,7 +630,35 @@ func (s uniformityService) CalculateUniformityFromDocument(c *fiber.Ctx, uniform
return UniformityCalculation{}, nil, "", err
}
return calculation, &document, url, nil
return calculation, document, url, nil
}
func (s uniformityService) fetchUniformityDocument(ctx context.Context, uniformityID uint, allowMissing bool) (*entity.Document, string, error) {
if s.DocumentSvc == nil {
return nil, "", fiber.NewError(fiber.StatusInternalServerError, "Document service not available")
}
documents, err := s.DocumentSvc.ListByTarget(ctx, "UNIFORMITY", uint64(uniformityID))
if err != nil {
return nil, "", err
}
if len(documents) == 0 {
if allowMissing {
return nil, "", nil
}
return nil, "", fiber.NewError(fiber.StatusNotFound, "Uniformity document not found")
}
document := documents[0]
url, err := s.DocumentSvc.PresignURL(ctx, document, 15*time.Minute)
if err != nil {
return nil, "", err
}
if url == "" {
return nil, "", fiber.NewError(fiber.StatusBadRequest, "Uniformity document URL not available")
}
return &document, url, nil
}
func (s *uniformityService) createUniformityApproval(
@@ -180,7 +180,10 @@ func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
req.Items = []validation.ReceivePurchaseItemRequest{singleItem}
}
}
req.TravelDocuments = form.File["documents"]
req.TravelDocuments = form.File["travel_documents"]
if len(req.TravelDocuments) == 0 {
req.TravelDocuments = form.File["documents"]
}
result, err := ctrl.service.ReceiveProducts(c, uint(id), req)
if err != nil {
return err
@@ -999,6 +999,22 @@ func (s *purchaseService) uploadTravelDocument(
return "", errors.New("document service not available")
}
documents, err := s.DocumentSvc.ListByTarget(ctx, string(utils.DocumentableTypePurchaseItem), uint64(itemID))
if err != nil {
return "", err
}
if len(documents) > 0 {
var ids []uint
for _, doc := range documents {
if doc.Type == string(utils.DocumentTypePurchaseTravel) {
ids = append(ids, doc.Id)
}
}
if err := s.DocumentSvc.DeleteDocuments(ctx, ids, true); err != nil {
return "", err
}
}
documentFiles := []commonSvc.DocumentFile{{
File: file,
Type: string(utils.DocumentTypePurchaseTravel),
@@ -1015,7 +1031,7 @@ func (s *purchaseService) uploadTravelDocument(
if len(results) == 0 {
return "", errors.New("upload result is empty")
}
return results[0].URL, nil
return results[0].Document.Path, nil
}
func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.DeletePurchaseItemsRequest) (*entity.Purchase, error) {
@@ -1499,10 +1515,56 @@ func (s *purchaseService) loadPurchase(
if err := s.attachLatestApproval(ctx, purchase); err != nil {
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err)
}
s.applyTravelDocumentURLs(ctx, purchase)
return purchase, nil
}
func (s *purchaseService) applyTravelDocumentURLs(ctx context.Context, purchase *entity.Purchase) {
if purchase == nil || s.DocumentSvc == nil {
return
}
for i := range purchase.Items {
item := &purchase.Items[i]
documents, err := s.DocumentSvc.ListByTarget(ctx, string(utils.DocumentableTypePurchaseItem), uint64(item.Id))
if err != nil {
s.Log.Warnf("Unable to load travel documents for purchase item %d: %+v", item.Id, err)
} else {
var targetDoc *entity.Document
for j := len(documents) - 1; j >= 0; j-- {
if documents[j].Type == string(utils.DocumentTypePurchaseTravel) {
targetDoc = &documents[j]
break
}
}
if targetDoc != nil {
url, err := s.DocumentSvc.PresignURL(ctx, *targetDoc, 15*time.Minute)
if err != nil {
s.Log.Warnf("Unable to presign travel document for purchase item %d: %+v", item.Id, err)
} else if url != "" {
item.TravelNumberDocs = &url
continue
}
}
}
path := item.TravelNumberDocs
if path == nil || strings.TrimSpace(*path) == "" {
continue
}
url, err := commonSvc.ResolveDocumentURL(ctx, s.DocumentSvc, *path, 15*time.Minute)
if err != nil {
s.Log.Warnf("Unable to presign travel document for purchase item %d: %+v", item.Id, err)
continue
}
if url == "" {
continue
}
item.TravelNumberDocs = &url
}
}
func collectPFKIDsFromPurchase(p *entity.Purchase) []uint {
seen := make(map[uint]struct{})
ids := make([]uint, 0)
@@ -171,6 +171,9 @@ func (h *Controller) Refresh(c *fiber.Ctx) error {
if resp.StatusCode >= 400 {
utils.Log.Warnf("token refresh response status %d", resp.StatusCode)
if resp.StatusCode == fiber.StatusTooManyRequests {
return fiber.NewError(fiber.StatusTooManyRequests, "Too many attempts, please slow down")
}
return fiber.NewError(fiber.StatusUnauthorized, "unauthenticated")
}
@@ -42,7 +42,7 @@ func (r *UserRepositoryImpl) GetByIdUser(
modifier func(*gorm.DB) *gorm.DB,
) (*entity.User, error) {
return r.BaseRepositoryImpl.First(ctx, func(db *gorm.DB) *gorm.DB {
return db.Where("id_user = ?", idUser)
return db.Where("id_user::bigint = ?::bigint", idUser)
})
}
@@ -93,7 +93,7 @@ func (r *UserRepositoryImpl) UpsertByIdUser(ctx context.Context, user *entity.Us
}
func (r *UserRepositoryImpl) SoftDeleteByIdUser(ctx context.Context, idUser int64) error {
query := r.DB().WithContext(ctx).Where("id_user = ?", idUser)
query := r.DB().WithContext(ctx).Where("id_user::bigint = ?::bigint", idUser)
result := query.Delete(&entity.User{})
if result.Error != nil {
return result.Error
+2 -2
View File
@@ -426,12 +426,12 @@ const (
DocumentTypeTransfer DocumentType = "STOCK_TRANSFER_DOCUMENT"
DocumentTypeExpense DocumentType = "EXPENSE_DOCUMENT"
DocumentTypeExpenseRealization DocumentType = "EXPENSE_REALIZATION_DOCUMENT"
DocumentTypePurchaseTravel DocumentType = "PURCHASE_TRAVEL_DOCUMENT"
DocumentTypePurchaseTravel DocumentType = "PURCHASE_TRAVEL_DOCUMENT"
DocumentableTypeTransfer DocumentableType = "STOCK_TRANSFER"
DocumentableTypeExpense DocumentableType = "EXPENSE"
DocumentableTypeExpenseRealization DocumentableType = "EXPENSE_REALIZATION"
DocumentableTypePurchaseItem DocumentableType = "PURCHASE_ITEM"
DocumentableTypePurchaseItem DocumentableType = "PURCHASE_ITEM"
)
// -------------------------------------------------------------------