Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into HOTFIX/BE/marketing

This commit is contained in:
aguhh18
2026-01-22 14:32:39 +07:00
11 changed files with 243 additions and 85 deletions
+1
View File
@@ -9,6 +9,7 @@ workflow:
include: include:
- local: "ci/development.yml" - local: "ci/development.yml"
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
- if: '$CI_COMMIT_BRANCH == "development"' - if: '$CI_COMMIT_BRANCH == "development"'
- local: "ci/staging.yml" - local: "ci/staging.yml"
+80 -40
View File
@@ -6,31 +6,31 @@ stages:
default: default:
tags: tags:
- self-hosted-prod - self-hosted-stg
workflow: workflow:
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"' - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
when: always when: always
- when: never - when: never
variables: variables:
DOCKER_BUILDKIT: "1" DOCKER_BUILDKIT: "1"
IMAGE_TAG: "production_${CI_COMMIT_SHORT_SHA}" IMAGE_TAG: "staging_${CI_COMMIT_SHORT_SHA}"
IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}" IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}"
IMAGE_LATEST: "${CI_REGISTRY_IMAGE}:production_latest" IMAGE_LATEST: "${CI_REGISTRY_IMAGE}:staging_latest"
DEPLOY_DIR: "/opt/deploy/lti" DEPLOY_DIR: "/opt/deploy/stg-lti-api"
COMPOSE_FILE: "docker-compose.yaml" COMPOSE_FILE: "docker-compose.yaml"
# ========================= # =========================
# BUILD (AUTO) # BUILD (AUTO)
# ========================= # =========================
build_production: build_staging:
stage: build stage: build
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"' - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
script: | script: |
set -e set -e
docker info docker info
@@ -49,54 +49,93 @@ build_production:
# ========================= # =========================
# MIGRATE (PRODUCTION - MANUAL) # MIGRATE (AUTO)
# ========================= # =========================
migrate_production: migrate_staging:
stage: migrate stage: migrate
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"' - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
when: manual
allow_failure: false
needs: needs:
- job: build_production - job: build_staging
artifacts: false artifacts: false
script: | script: |
set -e set -e
cd /opt/deploy/lti echo "✅ Running migrations (staging) ..."
test -f .env || (echo "❌ .env not found" && exit 1)
cd "$DEPLOY_DIR"
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
# ✅ load env dari server
set -a set -a
. ./.env . ./.env
set +a set +a
# Validasi env wajib # ✅ validasi
: "${DB_HOST:?DB_HOST not set}" test -n "$DB_HOST" || (echo "❌ DB_HOST empty" && exit 1)
: "${DB_PORT:?DB_PORT not set}" test -n "$DB_PORT" || (echo "❌ DB_PORT empty" && exit 1)
: "${DB_USER:?DB_USER not set}" test -n "$DB_USER" || (echo "❌ DB_USER empty" && exit 1)
: "${DB_PASSWORD:?DB_PASSWORD not set}" test -n "$DB_PASSWORD" || (echo "❌ DB_PASSWORD empty" && exit 1)
: "${DB_NAME:?DB_NAME not set}" test -n "$DB_NAME" || (echo "❌ DB_NAME empty" && exit 1)
DB_SSLMODE="${DB_SSLMODE:-require}" export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE:-disable}"
export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE}" echo "✅ DATABASE_URL=$DATABASE_URL"
echo "✅ Running migrations (production)..." # ✅ Pastikan postgres & redis ON (sesuaikan nama service compose kamu!)
docker run --rm \ echo "✅ Ensuring postgres & redis running ..."
docker compose -f "$COMPOSE_FILE" up -d stg-postgres-lti stg-redis-lti || true
# ✅ Ambil network key dari compose
COMPOSE_NETWORK_KEY="$(docker compose -f "$COMPOSE_FILE" config | awk '/networks:/ {getline; print $1}' | tr -d ':')"
echo "✅ Compose network key: $COMPOSE_NETWORK_KEY"
# ✅ Cari network name yang dipakai docker
NETWORK_NAME="$(docker network ls --format '{{.Name}}' | grep "_${COMPOSE_NETWORK_KEY}$" | head -n 1)"
test -n "$NETWORK_NAME" || (echo "❌ Cannot find docker network for compose ($COMPOSE_NETWORK_KEY)" && exit 1)
echo "✅ Docker network detected: $NETWORK_NAME"
# ✅ Migrations dari repo (CI workspace)
echo "✅ Checking migrations from repo..."
ls -lah "$CI_PROJECT_DIR/internal/database/migrations"
echo "✅ Running migrations via migrate/migrate container"
set +e
out=$(docker run --rm \
--network "$NETWORK_NAME" \
-v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \ -v "$CI_PROJECT_DIR/internal/database/migrations:/migrations:ro" \
migrate/migrate:v4.15.2 \ migrate/migrate:v4.15.2 \
-path=/migrations -database "$DATABASE_URL" up -path=/migrations -database "$DATABASE_URL" up 2>&1)
code=$?
set -e
echo "$out"
# ✅ Handle no change dengan benar (tidak false-success)
if echo "$out" | grep -qi "no change"; then
echo "✅ No change (already up to date)"
exit 0
fi
if [ $code -ne 0 ]; then
echo "❌ Migration failed with exit code $code"
exit $code
fi
echo "✅ Migration applied successfully"
# ========================= # =========================
# DEPLOY (AUTO) # DEPLOY (AUTO)
# ========================= # =========================
deploy_production: deploy_staging:
stage: deploy stage: deploy
rules: rules:
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"' - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
needs: needs:
- job: migrate_production - job: migrate_staging
artifacts: false artifacts: false
- job: build_production - job: build_staging
artifacts: false artifacts: false
script: | script: |
set -e set -e
@@ -115,19 +154,20 @@ deploy_production:
# ========================= # =========================
# SEED (MANUAL) # SEED (MANUAL)
# ========================= # =========================
seed_production: seed_staging:
stage: seed stage: seed
rules: rules:
- if: '$CI_COMMIT_BRANCH == "production"' - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
when: manual needs:
- job: deploy_staging
artifacts: false
when: manual
allow_failure: false
script: | script: |
set -e set -e
cd /opt/deploy/lti cd "$DEPLOY_DIR"
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found" && exit 1)
test -f .env || (echo "❌ .env not found" && exit 1) test -f .env || (echo "❌ .env not found" && exit 1)
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" docker compose -f "$COMPOSE_FILE" pull seed || true
docker compose -f "$COMPOSE_FILE" run --rm seed%
docker compose --env-file .env pull seed
docker compose --env-file .env run --rm seed
@@ -25,6 +25,7 @@ type FifoService interface {
Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error)
Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error) Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error)
ReleaseUsage(ctx context.Context, req StockReleaseRequest) error ReleaseUsage(ctx context.Context, req StockReleaseRequest) error
AdjustStockableQuantity(ctx context.Context, req StockAdjustRequest) error
} }
type fifoService struct { type fifoService struct {
@@ -95,6 +96,15 @@ type StockReplenishRequest struct {
Tx *gorm.DB Tx *gorm.DB
} }
type StockAdjustRequest struct {
StockableKey fifo.StockableKey
StockableID uint
ProductWarehouseID uint
Quantity float64
Note *string
Tx *gorm.DB
}
type PendingResolution struct { type PendingResolution struct {
UsableKey fifo.UsableKey UsableKey fifo.UsableKey
UsableID uint UsableID uint
@@ -137,6 +147,37 @@ type StockReleaseRequest struct {
Reason *string Reason *string
Tx *gorm.DB Tx *gorm.DB
} }
func (s *fifoService) AdjustStockableQuantity(ctx context.Context, req StockAdjustRequest) error {
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
return errors.New("stockable key and id are required")
}
if req.ProductWarehouseID == 0 {
return errors.New("product warehouse id is required")
}
if req.Quantity == 0 {
return nil
}
if req.Quantity > 0 {
return errors.New("quantity must be negative")
}
cfg, ok := fifo.Stockable(req.StockableKey)
if !ok {
return fmt.Errorf("stockable %q is not registered", req.StockableKey)
}
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
if err := s.incrementStockableQty(ctx, tx, cfg, req.StockableID, req.Quantity); err != nil {
return err
}
return s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
req.ProductWarehouseID: req.Quantity,
}, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
})
})
}
func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) { func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) {
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" { if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
@@ -28,10 +28,31 @@ func NewClosingController(closingService service.ClosingService, sapronakService
} }
func (u *ClosingController) GetAll(c *fiber.Ctx) error { func (u *ClosingController) GetAll(c *fiber.Ctx) error {
var projectStatus *int
if raw := c.Query("project_status"); raw != "" {
statusValue, err := strconv.Atoi(raw)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid project_status")
}
projectStatus = &statusValue
}
var locationID *uint
if raw := c.Query("location_id"); raw != "" {
locationValue, err := strconv.Atoi(raw)
if err != nil || locationValue <= 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid location_id")
}
locationUint := uint(locationValue)
locationID = &locationUint
}
query := &validation.Query{ query := &validation.Query{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""), Search: c.Query("search", ""),
ProjectStatus: projectStatus,
LocationID: locationID,
} }
if query.Page < 1 || query.Limit < 1 { if query.Page < 1 || query.Limit < 1 {
+20 -20
View File
@@ -98,26 +98,26 @@ type ClosingEggSalesDTO struct {
} }
type ClosingPerformanceDTO struct { type ClosingPerformanceDTO struct {
Depletion float64 `json:"depletion"` Depletion float64 `json:"depletion"`
Age float64 `json:"age_day"` Age float64 `json:"age_day"`
MortalityStd float64 `json:"mor_std"` MortalityStd float64 `json:"mor_std"`
MortalityAct float64 `json:"mor_act"` MortalityAct float64 `json:"mor_act"`
DeffMortality float64 `json:"mor_diff"` DeffMortality float64 `json:"mor_diff"`
FcrStd float64 `json:"fcr_std"` FcrStd float64 `json:"fcr_std"`
FcrAct float64 `json:"fcr_act"` FcrAct float64 `json:"fcr_act"`
DeffFcr float64 `json:"fcr_diff"` DeffFcr float64 `json:"fcr_diff"`
AwgAct float64 `json:"awg_act"` AwgAct float64 `json:"awg_act"`
AwgStd float64 `json:"awg_std"` AwgStd float64 `json:"awg_std"`
FeedIntake float64 `json:"feed_intake"` FeedIntake float64 `json:"feed_intake"`
FeedIntakeStd float64 `json:"feed_intake_std"` FeedIntakeStd float64 `json:"feed_intake_std"`
HenDayAct *float64 `json:"hen_day_act,omitempty"` HenDayAct float64 `json:"hen_day_act,omitempty"`
HendayStd float64 `json:"hen_day_std"` HendayStd float64 `json:"hen_day_std"`
EggMass *float64 `json:"egg_mass,omitempty"` EggMass float64 `json:"egg_mass,omitempty"`
EggMassStd float64 `json:"egg_mass_std"` EggMassStd float64 `json:"egg_mass_std"`
EggWeight *float64 `json:"egg_weight,omitempty"` EggWeight float64 `json:"egg_weight,omitempty"`
EggWeightStd float64 `json:"egg_weight_std"` EggWeightStd float64 `json:"egg_weight_std"`
HenHouseAct *float64 `json:"hen_housed_act,omitempty"` HenHouseAct float64 `json:"hen_housed_act,omitempty"`
HenHouseStd float64 `json:"hen_housed_std"` HenHouseStd float64 `json:"hen_housed_std"`
} }
type ClosingSalesGroupDTO struct { type ClosingSalesGroupDTO struct {
@@ -99,9 +99,31 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl
} }
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
statusFilter := ""
if params.ProjectStatus != nil {
switch *params.ProjectStatus {
case 1:
statusFilter = "Pengajuan"
case 2:
statusFilter = "Aktif"
}
}
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB { closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withClosingRelations(db) db = s.withClosingRelations(db)
if params.LocationID != nil {
db = db.Where("location_id = ?", *params.LocationID)
}
if statusFilter != "" {
latestApprovalSubQuery := s.Repository.DB().
WithContext(c.Context()).
Table("approvals").
Select("DISTINCT ON (approvable_id) approvable_id, step_name, id").
Where("approvable_type = ?", utils.ApprovalWorkflowProjectFlock.String()).
Order("approvable_id, id DESC")
db = db.Joins("JOIN (?) AS latest_approval ON latest_approval.approvable_id = project_flocks.id", latestApprovalSubQuery).
Where("LOWER(latest_approval.step_name) = LOWER(?)", statusFilter)
}
if params.Search != "" { if params.Search != "" {
return db.Where("flock_name ILIKE ?", "%"+params.Search+"%") return db.Where("flock_name ILIKE ?", "%"+params.Search+"%")
} }
@@ -930,19 +952,19 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint
if !isGrowing { if !isGrowing {
if targetAverages.HenDayCount > 0 { if targetAverages.HenDayCount > 0 {
henDayAct := targetAverages.HenDayAvg henDayAct := targetAverages.HenDayAvg
performance.HenDayAct = &henDayAct performance.HenDayAct = henDayAct
} }
if targetAverages.HenHouseCount > 0 { if targetAverages.HenHouseCount > 0 {
henHouseAct := targetAverages.HenHouseAvg henHouseAct := targetAverages.HenHouseAvg
performance.HenHouseAct = &henHouseAct performance.HenHouseAct = henHouseAct
} }
if targetAverages.EggWeightCount > 0 { if targetAverages.EggWeightCount > 0 {
eggWeight := targetAverages.EggWeightAvg eggWeight := targetAverages.EggWeightAvg
performance.EggWeight = &eggWeight performance.EggWeight = eggWeight
} }
if targetAverages.EggMassCount > 0 { if targetAverages.EggMassCount > 0 {
eggMass := targetAverages.EggMassAvg eggMass := targetAverages.EggMassAvg
performance.EggMass = &eggMass performance.EggMass = eggMass
} }
} }
performance.DeffFcr = performance.FcrStd - performance.FcrAct performance.DeffFcr = performance.FcrStd - performance.FcrAct
@@ -9,9 +9,11 @@ type Update struct {
} }
type Query struct { type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"` Search string `query:"search" validate:"omitempty,max=50"`
ProjectStatus *int `query:"project_status" validate:"omitempty,oneof=1 2"`
LocationID *uint `query:"location_id" validate:"omitempty,gt=0"`
} }
const ( const (
@@ -285,7 +285,7 @@ func (r *DashboardRepositoryImpl) SumEggProductionWeightGrams(ctx context.Contex
db := r.DB().WithContext(ctx). db := r.DB().WithContext(ctx).
Table("recording_eggs AS re"). Table("recording_eggs AS re").
Select("COALESCE(SUM(re.qty * re.weight), 0)"). Select("COALESCE(SUM(re.weight * 1000), 0)").
Joins("JOIN recordings AS r ON r.id = re.recording_id"). Joins("JOIN recordings AS r ON r.id = re.recording_id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
@@ -648,7 +648,7 @@ func (r *DashboardRepositoryImpl) GetEggWeightWeeklyGrams(ctx context.Context, s
Table("recording_eggs AS re"). Table("recording_eggs AS re").
Select(` Select(`
((r.day - 1) / 7 + 1) AS week, ((r.day - 1) / 7 + 1) AS week,
COALESCE(SUM(re.qty * re.weight), 0) AS egg_weight_grams`). COALESCE(SUM(re.weight * 1000), 0) AS egg_weight_grams`).
Joins("JOIN recordings AS r ON r.id = re.recording_id"). Joins("JOIN recordings AS r ON r.id = re.recording_id").
Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). Joins("JOIN project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id").
Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id").
@@ -171,6 +171,7 @@ func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKanda
var days []int var days []int
if err := tx.Model(&entity.Recording{}). if err := tx.Model(&entity.Recording{}).
Where("project_flock_kandangs_id = ?", projectFlockKandangId). Where("project_flock_kandangs_id = ?", projectFlockKandangId).
Where("deleted_at IS NULL").
Where("day IS NOT NULL"). Where("day IS NOT NULL").
Pluck("day", &days).Error; err != nil { Pluck("day", &days).Error; err != nil {
return 0, err return 0, err
@@ -399,7 +400,7 @@ func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordin
} }
err = tx. err = tx.
Table("recording_eggs"). Table("recording_eggs").
Select("COALESCE(SUM(recording_eggs.qty), 0) AS total_qty, COALESCE(SUM(recording_eggs.qty * COALESCE(recording_eggs.weight, 0)), 0) AS total_weight_grams"). Select("COALESCE(SUM(recording_eggs.qty), 0) AS total_qty, COALESCE(SUM(COALESCE(recording_eggs.weight, 0) * 1000), 0) AS total_weight_grams").
Where("recording_eggs.recording_id = ?", recordingID). Where("recording_eggs.recording_id = ?", recordingID).
Scan(&result).Error Scan(&result).Error
if err != nil { if err != nil {
@@ -485,7 +486,7 @@ func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ct
var result float64 var result float64
err := r.DB().WithContext(ctx). err := r.DB().WithContext(ctx).
Table("recording_eggs"). Table("recording_eggs").
Select("COALESCE(SUM(recording_eggs.qty * recording_eggs.weight), 0) / 1000"). Select("COALESCE(SUM(recording_eggs.weight), 0)").
Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id"). Joins("JOIN recordings ON recordings.id = recording_eggs.recording_id").
Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id"). Joins("JOIN project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id").
Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). Where("project_flock_kandangs.project_flock_id = ?", projectFlockID).
@@ -1157,7 +1157,7 @@ func eggsMatch(existing []entity.RecordingEgg, incoming []validation.Egg) bool {
} }
current := existingTotals[egg.ProductWarehouseId] current := existingTotals[egg.ProductWarehouseId]
current.Qty += egg.Qty current.Qty += egg.Qty
current.Weight += float64(egg.Qty) * weight current.Weight += weight
existingTotals[egg.ProductWarehouseId] = current existingTotals[egg.ProductWarehouseId] = current
} }
@@ -1169,7 +1169,7 @@ func eggsMatch(existing []entity.RecordingEgg, incoming []validation.Egg) bool {
} }
current := incomingTotals[egg.ProductWarehouseId] current := incomingTotals[egg.ProductWarehouseId]
current.Qty += egg.Qty current.Qty += egg.Qty
current.Weight += float64(egg.Qty) * weight current.Weight += weight
incomingTotals[egg.ProductWarehouseId] = current incomingTotals[egg.ProductWarehouseId] = current
} }
@@ -1328,7 +1328,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm
var eggMass float64 var eggMass float64
if remainingChick > 0 && totalEggWeightGrams > 0 { if remainingChick > 0 && totalEggWeightGrams > 0 {
eggMass = totalEggWeightGrams / remainingChick eggMass = (totalEggWeightGrams / remainingChick) / 1000
updates["egg_mass"] = eggMass updates["egg_mass"] = eggMass
recording.EggMass = &eggMass recording.EggMass = &eggMass
} else { } else {
@@ -844,6 +844,11 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
pwID uint pwID uint
qty float64 qty float64
}, 0, len(prepared)) }, 0, len(prepared))
fifoSubs := make([]struct {
itemID uint
pwID uint
qty float64
}, 0, len(prepared))
for _, prep := range prepared { for _, prep := range prepared {
item := prep.item item := prep.item
@@ -877,9 +882,18 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
totalQtyDeltas[item.Id] += deltaQty totalQtyDeltas[item.Id] += deltaQty
} }
case deltaQty < 0 && newPWID != nil: case deltaQty < 0 && newPWID != nil:
deltas[*newPWID] += deltaQty // negative if s.FifoSvc != nil {
affected[*newPWID] = struct{}{} fifoSubs = append(fifoSubs, struct {
totalQtyDeltas[item.Id] += deltaQty itemID uint
pwID uint
qty float64
}{itemID: item.Id, pwID: *newPWID, qty: deltaQty})
affected[*newPWID] = struct{}{}
} else {
deltas[*newPWID] += deltaQty // negative
affected[*newPWID] = struct{}{}
totalQtyDeltas[item.Id] += deltaQty
}
} }
dateCopy := prep.receivedDate dateCopy := prep.receivedDate
@@ -919,10 +933,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
return err return err
} }
if err := pwRepoTx.CleanupEmpty(c.Context(), affected); err != nil {
return err
}
if len(priceUpdates) > 0 { if len(priceUpdates) > 0 {
if err := repoTx.UpdatePricing(c.Context(), purchase.Id, priceUpdates); err != nil { if err := repoTx.UpdatePricing(c.Context(), purchase.Id, priceUpdates); err != nil {
return err return err
@@ -967,6 +977,26 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
return err return err
} }
} }
for _, adj := range fifoSubs {
if adj.pwID == 0 || adj.qty >= 0 {
continue
}
if err := s.FifoSvc.AdjustStockableQuantity(c.Context(), commonSvc.StockAdjustRequest{
StockableKey: fifo.StockableKeyPurchaseItems,
StockableID: adj.itemID,
ProductWarehouseID: adj.pwID,
Quantity: adj.qty,
Tx: tx,
}); err != nil {
return err
}
}
}
if len(affected) > 0 {
if err := pwRepoTx.CleanupEmpty(c.Context(), affected); err != nil {
return err
}
} }
return nil return nil