mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-24 07:15:43 +00:00
Merge branch 'development' of https://gitlab.com/mbugroup/lti-api into HOTFIX/BE/marketing
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
+81
-41
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user