diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index aa0dc969..6a4778a3 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -9,6 +9,7 @@ workflow: include: - local: "ci/development.yml" rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' - if: '$CI_COMMIT_BRANCH == "development"' - local: "ci/staging.yml" diff --git a/ci/staging.yml b/ci/staging.yml index 511a9eff..e3eaabb0 100644 --- a/ci/staging.yml +++ b/ci/staging.yml @@ -6,31 +6,31 @@ stages: default: tags: - - self-hosted-prod + - self-hosted-stg workflow: rules: - - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"' + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"' when: always - when: never variables: 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_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" # ========================= -# BUILD (AUTO) +# BUILD (AUTO) # ========================= -build_production: +build_staging: stage: build rules: - - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"' + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"' script: | set -e docker info @@ -49,54 +49,93 @@ build_production: # ========================= -# MIGRATE (PRODUCTION - MANUAL) +# MIGRATE (AUTO) # ========================= -migrate_production: +migrate_staging: stage: migrate rules: - - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"' - when: manual - allow_failure: false + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"' needs: - - job: build_production + - job: build_staging artifacts: false script: | set -e - cd /opt/deploy/lti - test -f .env || (echo "❌ .env not found" && exit 1) + echo "✅ Running migrations (staging) ..." + 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 . ./.env set +a - # Validasi env wajib - : "${DB_HOST:?DB_HOST not set}" - : "${DB_PORT:?DB_PORT not set}" - : "${DB_USER:?DB_USER not set}" - : "${DB_PASSWORD:?DB_PASSWORD not set}" - : "${DB_NAME:?DB_NAME not set}" + # ✅ validasi + test -n "$DB_HOST" || (echo "❌ DB_HOST empty" && exit 1) + test -n "$DB_PORT" || (echo "❌ DB_PORT empty" && exit 1) + test -n "$DB_USER" || (echo "❌ DB_USER empty" && exit 1) + test -n "$DB_PASSWORD" || (echo "❌ DB_PASSWORD empty" && exit 1) + 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}" + export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE:-disable}" + echo "✅ DATABASE_URL=$DATABASE_URL" - echo "✅ Running migrations (production)..." - docker run --rm \ + # ✅ Pastikan postgres & redis ON (sesuaikan nama service compose kamu!) + 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" \ 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_production: +deploy_staging: stage: deploy rules: - - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "production"' + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"' needs: - - job: migrate_production + - job: migrate_staging artifacts: false - - job: build_production + - job: build_staging artifacts: false script: | set -e @@ -115,19 +154,20 @@ deploy_production: # ========================= # SEED (MANUAL) # ========================= -seed_production: +seed_staging: stage: seed rules: - - if: '$CI_COMMIT_BRANCH == "production"' - when: manual + - if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"' + needs: + - job: deploy_staging + artifacts: false + when: manual + allow_failure: false script: | 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) - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" - - docker compose --env-file .env pull seed - docker compose --env-file .env run --rm seed - - + docker compose -f "$COMPOSE_FILE" pull seed || true + docker compose -f "$COMPOSE_FILE" run --rm seed% diff --git a/internal/common/service/common.fifo.service.go b/internal/common/service/common.fifo.service.go index b99e6c35..14cbb5c1 100644 --- a/internal/common/service/common.fifo.service.go +++ b/internal/common/service/common.fifo.service.go @@ -25,6 +25,7 @@ type FifoService interface { Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error) ReleaseUsage(ctx context.Context, req StockReleaseRequest) error + AdjustStockableQuantity(ctx context.Context, req StockAdjustRequest) error } type fifoService struct { @@ -95,6 +96,15 @@ type StockReplenishRequest struct { Tx *gorm.DB } +type StockAdjustRequest struct { + StockableKey fifo.StockableKey + StockableID uint + ProductWarehouseID uint + Quantity float64 + Note *string + Tx *gorm.DB +} + type PendingResolution struct { UsableKey fifo.UsableKey UsableID uint @@ -137,6 +147,37 @@ type StockReleaseRequest struct { Reason *string 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) { if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" { diff --git a/internal/modules/closings/controllers/closing.controller.go b/internal/modules/closings/controllers/closing.controller.go index 29c89f33..b1b02886 100644 --- a/internal/modules/closings/controllers/closing.controller.go +++ b/internal/modules/closings/controllers/closing.controller.go @@ -28,10 +28,31 @@ func NewClosingController(closingService service.ClosingService, sapronakService } 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{ - Page: c.QueryInt("page", 1), - Limit: c.QueryInt("limit", 10), - Search: c.Query("search", ""), + Page: c.QueryInt("page", 1), + Limit: c.QueryInt("limit", 10), + Search: c.Query("search", ""), + ProjectStatus: projectStatus, + LocationID: locationID, } if query.Page < 1 || query.Limit < 1 { diff --git a/internal/modules/closings/dto/closing.dto.go b/internal/modules/closings/dto/closing.dto.go index 1c191d29..82e11f49 100644 --- a/internal/modules/closings/dto/closing.dto.go +++ b/internal/modules/closings/dto/closing.dto.go @@ -98,26 +98,26 @@ type ClosingEggSalesDTO struct { } type ClosingPerformanceDTO struct { - Depletion float64 `json:"depletion"` - Age float64 `json:"age_day"` - MortalityStd float64 `json:"mor_std"` - MortalityAct float64 `json:"mor_act"` - DeffMortality float64 `json:"mor_diff"` - FcrStd float64 `json:"fcr_std"` - FcrAct float64 `json:"fcr_act"` - DeffFcr float64 `json:"fcr_diff"` - AwgAct float64 `json:"awg_act"` - AwgStd float64 `json:"awg_std"` - FeedIntake float64 `json:"feed_intake"` - FeedIntakeStd float64 `json:"feed_intake_std"` - HenDayAct *float64 `json:"hen_day_act,omitempty"` - HendayStd float64 `json:"hen_day_std"` - EggMass *float64 `json:"egg_mass,omitempty"` - EggMassStd float64 `json:"egg_mass_std"` - EggWeight *float64 `json:"egg_weight,omitempty"` - EggWeightStd float64 `json:"egg_weight_std"` - HenHouseAct *float64 `json:"hen_housed_act,omitempty"` - HenHouseStd float64 `json:"hen_housed_std"` + Depletion float64 `json:"depletion"` + Age float64 `json:"age_day"` + MortalityStd float64 `json:"mor_std"` + MortalityAct float64 `json:"mor_act"` + DeffMortality float64 `json:"mor_diff"` + FcrStd float64 `json:"fcr_std"` + FcrAct float64 `json:"fcr_act"` + DeffFcr float64 `json:"fcr_diff"` + AwgAct float64 `json:"awg_act"` + AwgStd float64 `json:"awg_std"` + FeedIntake float64 `json:"feed_intake"` + FeedIntakeStd float64 `json:"feed_intake_std"` + HenDayAct float64 `json:"hen_day_act,omitempty"` + HendayStd float64 `json:"hen_day_std"` + EggMass float64 `json:"egg_mass,omitempty"` + EggMassStd float64 `json:"egg_mass_std"` + EggWeight float64 `json:"egg_weight,omitempty"` + EggWeightStd float64 `json:"egg_weight_std"` + HenHouseAct float64 `json:"hen_housed_act,omitempty"` + HenHouseStd float64 `json:"hen_housed_std"` } type ClosingSalesGroupDTO struct { diff --git a/internal/modules/closings/services/closing.service.go b/internal/modules/closings/services/closing.service.go index 443eec7f..372d38fd 100644 --- a/internal/modules/closings/services/closing.service.go +++ b/internal/modules/closings/services/closing.service.go @@ -99,9 +99,31 @@ func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]dto.Cl } 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 { 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 != "" { return db.Where("flock_name ILIKE ?", "%"+params.Search+"%") } @@ -930,19 +952,19 @@ func (s closingService) GetClosingDataProduksi(c *fiber.Ctx, projectFlockID uint if !isGrowing { if targetAverages.HenDayCount > 0 { henDayAct := targetAverages.HenDayAvg - performance.HenDayAct = &henDayAct + performance.HenDayAct = henDayAct } if targetAverages.HenHouseCount > 0 { henHouseAct := targetAverages.HenHouseAvg - performance.HenHouseAct = &henHouseAct + performance.HenHouseAct = henHouseAct } if targetAverages.EggWeightCount > 0 { eggWeight := targetAverages.EggWeightAvg - performance.EggWeight = &eggWeight + performance.EggWeight = eggWeight } if targetAverages.EggMassCount > 0 { eggMass := targetAverages.EggMassAvg - performance.EggMass = &eggMass + performance.EggMass = eggMass } } performance.DeffFcr = performance.FcrStd - performance.FcrAct diff --git a/internal/modules/closings/validations/closing.validation.go b/internal/modules/closings/validations/closing.validation.go index 454bbdfc..9d3ad573 100644 --- a/internal/modules/closings/validations/closing.validation.go +++ b/internal/modules/closings/validations/closing.validation.go @@ -9,9 +9,11 @@ type Update struct { } type Query struct { - Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` - Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` - Search string `query:"search" validate:"omitempty,max=50"` + Page int `query:"page" validate:"omitempty,number,min=1,gt=0"` + Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"` + 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 ( diff --git a/internal/modules/dashboards/repositories/dashboard_stats.repository.go b/internal/modules/dashboards/repositories/dashboard_stats.repository.go index 7582680b..828dd96c 100644 --- a/internal/modules/dashboards/repositories/dashboard_stats.repository.go +++ b/internal/modules/dashboards/repositories/dashboard_stats.repository.go @@ -285,7 +285,7 @@ func (r *DashboardRepositoryImpl) SumEggProductionWeightGrams(ctx context.Contex db := r.DB().WithContext(ctx). 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 project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_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"). Select(` ((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 project_flock_kandangs AS pfk ON pfk.id = r.project_flock_kandangs_id"). Joins("JOIN kandangs AS k ON k.id = pfk.kandang_id"). diff --git a/internal/modules/production/recordings/repositories/recording.repository.go b/internal/modules/production/recordings/repositories/recording.repository.go index 9e783134..6cb65c6c 100644 --- a/internal/modules/production/recordings/repositories/recording.repository.go +++ b/internal/modules/production/recordings/repositories/recording.repository.go @@ -171,6 +171,7 @@ func (r *RecordingRepositoryImpl) GenerateNextDay(tx *gorm.DB, projectFlockKanda var days []int if err := tx.Model(&entity.Recording{}). Where("project_flock_kandangs_id = ?", projectFlockKandangId). + Where("deleted_at IS NULL"). Where("day IS NOT NULL"). Pluck("day", &days).Error; err != nil { return 0, err @@ -399,7 +400,7 @@ func (r *RecordingRepositoryImpl) GetEggSummaryByRecording(tx *gorm.DB, recordin } err = tx. 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). Scan(&result).Error if err != nil { @@ -485,7 +486,7 @@ func (r *RecordingRepositoryImpl) GetTotalEggProductionWeightByProjectFlockID(ct var result float64 err := r.DB().WithContext(ctx). 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 project_flock_kandangs ON project_flock_kandangs.id = recordings.project_flock_kandangs_id"). Where("project_flock_kandangs.project_flock_id = ?", projectFlockID). diff --git a/internal/modules/production/recordings/services/recording.service.go b/internal/modules/production/recordings/services/recording.service.go index 80611109..a5486ab7 100644 --- a/internal/modules/production/recordings/services/recording.service.go +++ b/internal/modules/production/recordings/services/recording.service.go @@ -1157,7 +1157,7 @@ func eggsMatch(existing []entity.RecordingEgg, incoming []validation.Egg) bool { } current := existingTotals[egg.ProductWarehouseId] current.Qty += egg.Qty - current.Weight += float64(egg.Qty) * weight + current.Weight += weight existingTotals[egg.ProductWarehouseId] = current } @@ -1169,7 +1169,7 @@ func eggsMatch(existing []entity.RecordingEgg, incoming []validation.Egg) bool { } current := incomingTotals[egg.ProductWarehouseId] current.Qty += egg.Qty - current.Weight += float64(egg.Qty) * weight + current.Weight += weight incomingTotals[egg.ProductWarehouseId] = current } @@ -1328,7 +1328,7 @@ func (s *recordingService) computeAndUpdateMetrics(ctx context.Context, tx *gorm var eggMass float64 if remainingChick > 0 && totalEggWeightGrams > 0 { - eggMass = totalEggWeightGrams / remainingChick + eggMass = (totalEggWeightGrams / remainingChick) / 1000 updates["egg_mass"] = eggMass recording.EggMass = &eggMass } else { diff --git a/internal/modules/purchases/services/purchase.service.go b/internal/modules/purchases/services/purchase.service.go index b0914853..b7efbc05 100644 --- a/internal/modules/purchases/services/purchase.service.go +++ b/internal/modules/purchases/services/purchase.service.go @@ -844,6 +844,11 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation pwID uint qty float64 }, 0, len(prepared)) + fifoSubs := make([]struct { + itemID uint + pwID uint + qty float64 + }, 0, len(prepared)) for _, prep := range prepared { item := prep.item @@ -877,9 +882,18 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation totalQtyDeltas[item.Id] += deltaQty } case deltaQty < 0 && newPWID != nil: - deltas[*newPWID] += deltaQty // negative - affected[*newPWID] = struct{}{} - totalQtyDeltas[item.Id] += deltaQty + if s.FifoSvc != nil { + fifoSubs = append(fifoSubs, struct { + 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 @@ -919,10 +933,6 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation return err } - if err := pwRepoTx.CleanupEmpty(c.Context(), affected); err != nil { - return err - } - if len(priceUpdates) > 0 { if err := repoTx.UpdatePricing(c.Context(), purchase.Id, priceUpdates); err != nil { return err @@ -967,6 +977,26 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation 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