mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'development' into dev/gio
This commit is contained in:
@@ -1,13 +0,0 @@
|
|||||||
# .air.toml
|
|
||||||
root = "."
|
|
||||||
tmp_dir = "tmp"
|
|
||||||
|
|
||||||
[build]
|
|
||||||
cmd = "go build -buildvcs=false -o ./tmp/main ./cmd/api"
|
|
||||||
bin = "tmp/main"
|
|
||||||
full_bin = "APP_ENV=dev ./tmp/main"
|
|
||||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
|
||||||
exclude_dir = ["vendor", "tmp"]
|
|
||||||
|
|
||||||
[log]
|
|
||||||
time = true
|
|
||||||
+3
-1
@@ -13,7 +13,8 @@ bin/
|
|||||||
Makefile
|
Makefile
|
||||||
docker-compose.local.yml
|
docker-compose.local.yml
|
||||||
docker-compose.yaml
|
docker-compose.yaml
|
||||||
Dockerfile.local
|
Dockerfile
|
||||||
|
.gitlab-ci.yml
|
||||||
# Go build cache
|
# Go build cache
|
||||||
.gocache/
|
.gocache/
|
||||||
vendor
|
vendor
|
||||||
@@ -27,3 +28,4 @@ coverage/
|
|||||||
.vscode/
|
.vscode/
|
||||||
.idea/
|
.idea/
|
||||||
*.swp
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
|||||||
+164
-81
@@ -1,90 +1,173 @@
|
|||||||
stages:
|
stages:
|
||||||
|
- build
|
||||||
|
- migrate
|
||||||
- deploy
|
- deploy
|
||||||
|
- seed
|
||||||
|
|
||||||
deploy-dev:
|
default:
|
||||||
|
tags:
|
||||||
|
- self-hosted-stg
|
||||||
|
|
||||||
|
workflow:
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||||
|
when: always
|
||||||
|
- when: never
|
||||||
|
|
||||||
|
variables:
|
||||||
|
DOCKER_BUILDKIT: "1"
|
||||||
|
|
||||||
|
IMAGE_TAG: "staging_${CI_COMMIT_SHORT_SHA}"
|
||||||
|
IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}"
|
||||||
|
IMAGE_LATEST: "${CI_REGISTRY_IMAGE}:staging_latest"
|
||||||
|
|
||||||
|
DEPLOY_DIR: "/opt/deploy/stg-lti-api"
|
||||||
|
COMPOSE_FILE: "docker-compose.yaml"
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# BUILD (AUTO)
|
||||||
|
# =========================
|
||||||
|
build_staging:
|
||||||
|
stage: build
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
docker info
|
||||||
|
|
||||||
|
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||||
|
|
||||||
|
echo "✅ Build image: $IMAGE_NAME"
|
||||||
|
docker build -t "$IMAGE_NAME" -f Dockerfile .
|
||||||
|
|
||||||
|
echo "✅ Push image: $IMAGE_NAME"
|
||||||
|
docker push "$IMAGE_NAME"
|
||||||
|
|
||||||
|
echo "✅ Tag latest: $IMAGE_LATEST"
|
||||||
|
docker tag "$IMAGE_NAME" "$IMAGE_LATEST"
|
||||||
|
docker push "$IMAGE_LATEST"
|
||||||
|
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# MIGRATE (AUTO)
|
||||||
|
# =========================
|
||||||
|
migrate_staging:
|
||||||
|
stage: migrate
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||||
|
needs:
|
||||||
|
- job: build_staging
|
||||||
|
artifacts: false
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
|
||||||
|
export DATABASE_URL="postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}?sslmode=${DB_SSLMODE:-disable}"
|
||||||
|
echo "✅ DATABASE_URL=$DATABASE_URL"
|
||||||
|
|
||||||
|
# ✅ 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 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_staging:
|
||||||
stage: deploy
|
stage: deploy
|
||||||
image: alpine:3.20
|
rules:
|
||||||
variables:
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||||
DEPLOY_APP: "LTI-MBUGROUP"
|
needs:
|
||||||
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga
|
- job: migrate_staging
|
||||||
GIT_SUBMODULE_STRATEGY: recursive
|
artifacts: false
|
||||||
GIT_DEPTH: "1"
|
- job: build_staging
|
||||||
|
artifacts: false
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
docker info
|
||||||
|
echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
|
||||||
|
|
||||||
before_script:
|
cd "$DEPLOY_DIR"
|
||||||
- echo "🧰 Installing dependencies..."
|
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found in $DEPLOY_DIR" && exit 1)
|
||||||
- apk update && apk add --no-cache openssh git curl bash
|
test -f .env || (echo "❌ .env not found in $DEPLOY_DIR" && exit 1)
|
||||||
|
|
||||||
# Setup SSH di runner
|
docker compose -f "$COMPOSE_FILE" pull
|
||||||
- mkdir -p ~/.ssh
|
docker compose -f "$COMPOSE_FILE" up -d --force-recreate
|
||||||
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa
|
docker image prune -f
|
||||||
- chmod 600 ~/.ssh/id_rsa
|
|
||||||
- eval "$(ssh-agent -s)"
|
|
||||||
- ssh-add ~/.ssh/id_rsa
|
|
||||||
|
|
||||||
# Trust host keys (server + gitlab) biar SSH gak nanya interaktif
|
|
||||||
- ssh-keyscan -H "$SERVER_IP" >> ~/.ssh/known_hosts
|
|
||||||
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
|
||||||
|
|
||||||
script:
|
# =========================
|
||||||
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
|
# SEED (MANUAL)
|
||||||
|
# =========================
|
||||||
|
seed_staging:
|
||||||
|
stage: seed
|
||||||
|
rules:
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "push" && $CI_COMMIT_BRANCH == "staging"'
|
||||||
|
needs:
|
||||||
|
- job: deploy_staging
|
||||||
|
artifacts: false
|
||||||
|
when: manual
|
||||||
|
allow_failure: false
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
cd "$DEPLOY_DIR"
|
||||||
|
test -f "$COMPOSE_FILE" || (echo "❌ $COMPOSE_FILE not found" && exit 1)
|
||||||
|
test -f .env || (echo "❌ .env not found" && exit 1)
|
||||||
|
|
||||||
- >
|
docker compose -f "$COMPOSE_FILE" pull seed || true
|
||||||
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" "
|
docker compose -f "$COMPOSE_FILE" run --rm seed
|
||||||
set -e
|
|
||||||
|
|
||||||
cd /home/devops/docker/deployment/development/lti-api
|
|
||||||
|
|
||||||
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS)
|
|
||||||
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git
|
|
||||||
|
|
||||||
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH)
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
|
|
||||||
|
|
||||||
# Fetch/reset pakai SSH
|
|
||||||
GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no' git fetch origin development
|
|
||||||
git reset --hard origin/development
|
|
||||||
|
|
||||||
docker compose restart dev-api-lti || docker compose up -d dev-api-lti
|
|
||||||
"; then
|
|
||||||
STATUS='success';
|
|
||||||
else
|
|
||||||
STATUS='failed';
|
|
||||||
fi;
|
|
||||||
|
|
||||||
RUN_URL="${CI_PROJECT_URL}/-/pipelines/${CI_PIPELINE_ID}";
|
|
||||||
|
|
||||||
if [ "$STATUS" = "success" ]; then
|
|
||||||
COLOR=3066993;
|
|
||||||
TITLE="✅ Deployment API Succeeded";
|
|
||||||
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` completed successfully.";
|
|
||||||
else
|
|
||||||
COLOR=15158332;
|
|
||||||
TITLE="❌ Deployment API Failed Gaes";
|
|
||||||
DESC="Deployment job on branch \`${CI_COMMIT_REF_NAME}\` failed.";
|
|
||||||
fi;
|
|
||||||
|
|
||||||
echo "{
|
|
||||||
\"username\": \"CI Bot\",
|
|
||||||
\"embeds\": [{
|
|
||||||
\"title\": \"$TITLE\",
|
|
||||||
\"description\": \"$DESC\",
|
|
||||||
\"color\": $COLOR,
|
|
||||||
\"fields\": [
|
|
||||||
{\"name\": \"Repository\", \"value\": \"${CI_PROJECT_PATH}\", \"inline\": true},
|
|
||||||
{\"name\": \"Actor\", \"value\": \"${GITLAB_USER_LOGIN}\", \"inline\": true},
|
|
||||||
{\"name\": \"Commit\", \"value\": \"${CI_COMMIT_SHA}\", \"inline\": false},
|
|
||||||
{\"name\": \"Pipeline\", \"value\": \"[Open run](${RUN_URL})\", \"inline\": false}
|
|
||||||
]
|
|
||||||
}]
|
|
||||||
}" > payload.json;
|
|
||||||
|
|
||||||
echo "📡 Sending notification to Discord...";
|
|
||||||
curl -sS -H "Content-Type: application/json" \
|
|
||||||
-d @payload.json "$DISCORD_WEBHOOK_URL";
|
|
||||||
|
|
||||||
only:
|
|
||||||
- development
|
|
||||||
|
|
||||||
environment:
|
|
||||||
name: development
|
|
||||||
+29
-11
@@ -1,20 +1,38 @@
|
|||||||
FROM golang:1.23-alpine
|
# =========================
|
||||||
|
# Builder stage
|
||||||
|
# =========================
|
||||||
|
FROM golang:1.23-alpine AS builder
|
||||||
|
|
||||||
# Install dependensi dasar
|
RUN apk add --no-cache git ca-certificates tzdata
|
||||||
RUN apk add --no-cache git curl bash build-base
|
WORKDIR /app
|
||||||
|
|
||||||
# Install Air (pakai repo baru air-verse)
|
|
||||||
RUN go install github.com/air-verse/air@v1.52.3
|
|
||||||
|
|
||||||
WORKDIR /lti-api
|
|
||||||
|
|
||||||
# Cache dependencies
|
|
||||||
COPY go.mod go.sum ./
|
COPY go.mod go.sum ./
|
||||||
RUN go mod download
|
RUN go mod download
|
||||||
|
|
||||||
# Copy source code
|
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
|
# Build API binary
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -trimpath -ldflags="-s -w" -o lti-api ./cmd/api
|
||||||
|
|
||||||
|
# Build SEED binary (pastikan cmd/seed ada)
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
|
||||||
|
go build -trimpath -ldflags="-s -w" -o lti-seed ./cmd/seed
|
||||||
|
|
||||||
|
# =========================
|
||||||
|
# Runtime stage
|
||||||
|
# =========================
|
||||||
|
FROM alpine:3.20
|
||||||
|
|
||||||
|
RUN apk add --no-cache ca-certificates tzdata curl bash postgresql-client \
|
||||||
|
&& adduser -D -H -u 10001 appuser
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/lti-api /app/lti-api
|
||||||
|
COPY --from=builder /app/lti-seed /app/lti-seed
|
||||||
|
|
||||||
|
USER appuser
|
||||||
EXPOSE 8081
|
EXPOSE 8081
|
||||||
|
|
||||||
CMD ["air", "-c", ".air.toml"]
|
CMD ["/app/lti-api"]
|
||||||
|
|||||||
@@ -1,77 +0,0 @@
|
|||||||
services:
|
|
||||||
postgresdb:
|
|
||||||
image: postgres:alpine
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "${DB_PORT_HOST:-5542}:5432"
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${DB_USER:-postgres}
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-postgres}
|
|
||||||
POSTGRES_DB: ${DB_NAME:-db_lti_erp}
|
|
||||||
volumes:
|
|
||||||
- dbdata:/var/lib/postgresql/data
|
|
||||||
- ./internal/database/init:/docker-entrypoint-initdb.d
|
|
||||||
networks: [go-network]
|
|
||||||
healthcheck:
|
|
||||||
test:
|
|
||||||
[
|
|
||||||
"CMD-SHELL",
|
|
||||||
"pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}",
|
|
||||||
]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
redis:
|
|
||||||
image: redis:7-alpine
|
|
||||||
restart: unless-stopped
|
|
||||||
ports:
|
|
||||||
- "${REDIS_PORT_HOST:-6381}:6379"
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
|
|
||||||
interval: 5s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 10
|
|
||||||
networks: [go-network]
|
|
||||||
|
|
||||||
app:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile.local
|
|
||||||
image: cosmtrek/air:v1.52.3
|
|
||||||
working_dir: /lti-api
|
|
||||||
volumes:
|
|
||||||
- .:/lti-api
|
|
||||||
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
|
|
||||||
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
|
|
||||||
command: air -c .air.toml
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
DB_HOST: postgresdb
|
|
||||||
DB_PORT: 5432
|
|
||||||
DB_USER: ${DB_USER:-postgres}
|
|
||||||
DB_PASSWORD: ${DB_PASSWORD:-postgres}
|
|
||||||
DB_NAME: ${DB_NAME:-db_lti_erp}
|
|
||||||
REDIS_URL: ${REDIS_URL:-redis://redis:6379/0}
|
|
||||||
ports:
|
|
||||||
- "${APP_PORT:-8081}:8081"
|
|
||||||
depends_on:
|
|
||||||
postgresdb:
|
|
||||||
condition: service_healthy
|
|
||||||
networks: [go-network]
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 10
|
|
||||||
start_period: 10s
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
dbdata:
|
|
||||||
go-mod-cache:
|
|
||||||
go-build-cache:
|
|
||||||
|
|
||||||
networks:
|
|
||||||
go-network:
|
|
||||||
name: lti-api_go-network
|
|
||||||
driver: bridge
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
services:
|
|
||||||
dev-api-lti:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
container_name: dev-api-lti
|
|
||||||
working_dir: /lti-api
|
|
||||||
command: ["/bin/sh", "scripts/entrypoint.sh"]
|
|
||||||
ports:
|
|
||||||
- "8081:8081"
|
|
||||||
env_file:
|
|
||||||
- .env
|
|
||||||
environment:
|
|
||||||
# override agar koneksi ke container internal
|
|
||||||
DB_HOST: dev-postgres-lti
|
|
||||||
DB_PORT: 5432
|
|
||||||
REDIS_URL: redis://dev-redis-lti:6379/0
|
|
||||||
volumes:
|
|
||||||
- .:/lti-api
|
|
||||||
- ./.air.toml:/lti-api/.air.toml:ro
|
|
||||||
- ./internal/config/jwtRS256.key:/run/keys/jwtRS256.key
|
|
||||||
- ./internal/config/jwtRS256.key.pub:/run/keys/jwtRS256.key.pub
|
|
||||||
depends_on:
|
|
||||||
- dev-postgres-lti
|
|
||||||
- dev-redis-lti
|
|
||||||
networks:
|
|
||||||
- lti-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "wget -qO- http://localhost:8081/healthz || exit 1"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 10
|
|
||||||
start_period: 10s
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: "2.0"
|
|
||||||
memory: 2G
|
|
||||||
reservations:
|
|
||||||
cpus: "1.0"
|
|
||||||
memory: 512M
|
|
||||||
|
|
||||||
dev-postgres-lti:
|
|
||||||
image: postgres:15-alpine
|
|
||||||
container_name: dev-postgres-lti
|
|
||||||
restart: always
|
|
||||||
env_file:
|
|
||||||
- credential/.env.db
|
|
||||||
ports:
|
|
||||||
- "5433:5432"
|
|
||||||
volumes:
|
|
||||||
- dev-postgres-lti-data:/var/lib/postgresql/data
|
|
||||||
- ./credential:/docker-entrypoint-initdb.d:ro
|
|
||||||
networks:
|
|
||||||
- lti-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-postgres} -d ${DB_NAME:-db_lti_erp}"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
start_period: 5s
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: "1.0"
|
|
||||||
memory: 2G
|
|
||||||
reservations:
|
|
||||||
cpus: "0.5"
|
|
||||||
memory: 512M
|
|
||||||
|
|
||||||
dev-redis-lti:
|
|
||||||
image: redis:7-alpine
|
|
||||||
container_name: dev-redis-lti
|
|
||||||
restart: always
|
|
||||||
ports:
|
|
||||||
- "6380:6379"
|
|
||||||
networks:
|
|
||||||
- lti-network
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 10
|
|
||||||
deploy:
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
cpus: "0.5"
|
|
||||||
memory: 512M
|
|
||||||
reservations:
|
|
||||||
cpus: "0.2"
|
|
||||||
memory: 256M
|
|
||||||
|
|
||||||
networks:
|
|
||||||
lti-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
dev-postgres-lti-data:
|
|
||||||
@@ -22,18 +22,19 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
P_ExpenseGetAll = "lti.expense.list"
|
P_ExpenseGetAll = "lti.expense.list"
|
||||||
P_ExpenseCreateOne = "lti.expense.create"
|
P_ExpenseCreateOne = "lti.expense.create"
|
||||||
P_ExpenseUpdateOne = "lti.expense.update"
|
P_ExpenseUpdateOne = "lti.expense.update"
|
||||||
P_ExpenseGetOne = "lti.expense.detail"
|
P_ExpenseGetOne = "lti.expense.detail"
|
||||||
P_ExpenseDeleteOne = "lti.expense.delete"
|
P_ExpenseDeleteOne = "lti.expense.delete"
|
||||||
P_ExpenseApprovalManager = "lti.expense.approve.manager"
|
P_ExpenseApprovalHeadArea = "lti.expense.approve.head_area"
|
||||||
P_ExpenseApprovalFinance = "lti.expense.approve.finance"
|
P_ExpenseApprovalFinance = "lti.expense.approve.finance"
|
||||||
P_ExpenseCreateRealizations = "lti.expense.create.realization"
|
P_ExpenseApprovalUnitVicePresident = "lti.expense.approve.unit_vice_president"
|
||||||
P_ExpenseUpdateRealizations = "lti.expense.update.realization"
|
P_ExpenseCreateRealizations = "lti.expense.create.realization"
|
||||||
P_ExpenseCompleteExpense = "lti.expense.complete.expense"
|
P_ExpenseUpdateRealizations = "lti.expense.update.realization"
|
||||||
P_ExpenseDocument = "lti.expense.document"
|
P_ExpenseCompleteExpense = "lti.expense.complete.expense"
|
||||||
P_ExpenseDocumentRealizations = "lti.expense.document.realization"
|
P_ExpenseDocument = "lti.expense.document"
|
||||||
|
P_ExpenseDocumentRealizations = "lti.expense.document.realization"
|
||||||
)
|
)
|
||||||
const (
|
const (
|
||||||
P_AdjustmentGetAll = "lti.inventory.list"
|
P_AdjustmentGetAll = "lti.inventory.list"
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package dailyChecklists
|
package dailyChecklists
|
||||||
|
|
||||||
import (
|
import (
|
||||||
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/controllers"
|
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/controllers"
|
||||||
dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
dailyChecklist "gitlab.com/mbugroup/lti-api.git/internal/modules/daily-checklists/services"
|
||||||
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
@@ -13,7 +13,7 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist.
|
|||||||
ctrl := controller.NewDailyChecklistController(s)
|
ctrl := controller.NewDailyChecklistController(s)
|
||||||
|
|
||||||
route := v1.Group("/daily-checklists")
|
route := v1.Group("/daily-checklists")
|
||||||
// route.Use(m.Auth(u))
|
route.Use(m.Auth(u))
|
||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Get("/report", ctrl.GetReport)
|
route.Get("/report", ctrl.GetReport)
|
||||||
@@ -22,7 +22,7 @@ func DailyChecklistRoutes(v1 fiber.Router, u user.UserService, s dailyChecklist.
|
|||||||
|
|
||||||
route.Get("/report", ctrl.GetReport)
|
route.Get("/report", ctrl.GetReport)
|
||||||
|
|
||||||
// create daily checklist
|
// upsert daily checklist
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
|
|
||||||
// get detail data daily checklist by id
|
// get detail data daily checklist by id
|
||||||
|
|||||||
@@ -229,10 +229,12 @@ func (u *ExpenseController) Approval(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
path := c.Path()
|
path := c.Path()
|
||||||
approvalType := ""
|
approvalType := ""
|
||||||
if strings.Contains(path, "/approvals/manager") {
|
if strings.Contains(path, "/approvals/head-area") {
|
||||||
approvalType = "manager"
|
approvalType = "head-area"
|
||||||
} else if strings.Contains(path, "/approvals/finance") {
|
} else if strings.Contains(path, "/approvals/finance") {
|
||||||
approvalType = "finance"
|
approvalType = "finance"
|
||||||
|
} else if strings.Contains(path, "/approvals/unit-vice-president") {
|
||||||
|
approvalType = "unit-vice-president"
|
||||||
} else {
|
} else {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval path")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval path")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,8 +27,11 @@ func ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService
|
|||||||
route.Get("/:id", m.RequirePermissions(m.P_ExpenseGetOne), ctrl.GetOne)
|
route.Get("/:id", m.RequirePermissions(m.P_ExpenseGetOne), ctrl.GetOne)
|
||||||
route.Patch("/:id", m.RequirePermissions(m.P_ExpenseUpdateOne), ctrl.UpdateOne)
|
route.Patch("/:id", m.RequirePermissions(m.P_ExpenseUpdateOne), ctrl.UpdateOne)
|
||||||
route.Delete("/:id", m.RequirePermissions(m.P_ExpenseDeleteOne), ctrl.DeleteOne)
|
route.Delete("/:id", m.RequirePermissions(m.P_ExpenseDeleteOne), ctrl.DeleteOne)
|
||||||
route.Post("/approvals/manager", m.RequirePermissions(m.P_ExpenseApprovalManager), ctrl.Approval)
|
|
||||||
|
route.Post("/approvals/head-area", m.RequirePermissions(m.P_ExpenseApprovalHeadArea), ctrl.Approval)
|
||||||
route.Post("/approvals/finance", m.RequirePermissions(m.P_ExpenseApprovalFinance), ctrl.Approval)
|
route.Post("/approvals/finance", m.RequirePermissions(m.P_ExpenseApprovalFinance), ctrl.Approval)
|
||||||
|
route.Post("/approvals/unit-vice-president", m.RequirePermissions(m.P_ExpenseApprovalUnitVicePresident), ctrl.Approval)
|
||||||
|
|
||||||
route.Post("/:id/realizations", m.RequirePermissions(m.P_ExpenseCreateRealizations), ctrl.CreateRealization)
|
route.Post("/:id/realizations", m.RequirePermissions(m.P_ExpenseCreateRealizations), ctrl.CreateRealization)
|
||||||
route.Patch("/:id/realizations", m.RequirePermissions(m.P_ExpenseUpdateRealizations), ctrl.UpdateRealization)
|
route.Patch("/:id/realizations", m.RequirePermissions(m.P_ExpenseUpdateRealizations), ctrl.UpdateRealization)
|
||||||
route.Post("/:id/complete", m.RequirePermissions(m.P_ExpenseCompleteExpense), ctrl.CompleteExpense)
|
route.Post("/:id/complete", m.RequirePermissions(m.P_ExpenseCompleteExpense), ctrl.CompleteExpense)
|
||||||
|
|||||||
@@ -1049,21 +1049,30 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
|||||||
}
|
}
|
||||||
|
|
||||||
var stepNumber approvalutils.ApprovalStep
|
var stepNumber approvalutils.ApprovalStep
|
||||||
if approvalType == "manager" {
|
if approvalType == "head-area" {
|
||||||
|
|
||||||
stepNumber = utils.ExpenseStepManager
|
stepNumber = utils.ExpenseStepHeadArea
|
||||||
if latestApproval.StepNumber != uint16(utils.ExpenseStepPengajuan) {
|
if latestApproval.StepNumber != uint16(utils.ExpenseStepPengajuan) {
|
||||||
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
||||||
return fiber.NewError(fiber.StatusBadRequest,
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
fmt.Sprintf("Cannot process at Manager step. Latest approval is at %s step. Expected previous step: Pengajuan", currentStepName))
|
fmt.Sprintf("Cannot process at Head Area step. Latest approval is at %s step. Expected previous step: Pengajuan", currentStepName))
|
||||||
}
|
}
|
||||||
|
} else if approvalType == "unit-vice-president" {
|
||||||
|
|
||||||
|
stepNumber = utils.ExpenseStepUnitVicePresident
|
||||||
|
if latestApproval.StepNumber != uint16(utils.ExpenseStepHeadArea) {
|
||||||
|
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Cannot process at Unit Vice President step. Latest approval is at %s step. Expected previous step: Head Area", currentStepName))
|
||||||
|
}
|
||||||
|
|
||||||
} else if approvalType == "finance" {
|
} else if approvalType == "finance" {
|
||||||
|
|
||||||
stepNumber = utils.ExpenseStepFinance
|
stepNumber = utils.ExpenseStepFinance
|
||||||
if latestApproval.StepNumber != uint16(utils.ExpenseStepManager) {
|
if latestApproval.StepNumber != uint16(utils.ExpenseStepUnitVicePresident) {
|
||||||
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
|
||||||
return fiber.NewError(fiber.StatusBadRequest,
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
fmt.Sprintf("Cannot process at Finance step. Latest approval is at %s step. Expected previous step: Manager", currentStepName))
|
fmt.Sprintf("Cannot process at Finance step. Latest approval is at %s step. Expected previous step: Unit Vice President", currentStepName))
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid approval type: %v", approvalType))
|
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid approval type: %v", approvalType))
|
||||||
|
|||||||
@@ -39,12 +39,12 @@ type TransferExpenseReceivingPayload struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type groupedTransferItem struct {
|
type groupedTransferItem struct {
|
||||||
detail *entity.StockTransferDetail
|
detail *entity.StockTransferDetail
|
||||||
payload TransferExpenseReceivingPayload
|
payload TransferExpenseReceivingPayload
|
||||||
projectFK *uint
|
projectFK *uint
|
||||||
kandangID *uint
|
kandangID *uint
|
||||||
totalPrice float64
|
totalPrice float64
|
||||||
shippingCostTotal float64
|
shippingCostTotal float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func groupingKey(supplierID uint, date time.Time, warehouseID uint) string {
|
func groupingKey(supplierID uint, date time.Time, warehouseID uint) string {
|
||||||
@@ -84,7 +84,6 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
|
|||||||
expenseIDs := make(map[uint64]struct{})
|
expenseIDs := make(map[uint64]struct{})
|
||||||
expenseNonstockIDs := make([]uint64, 0)
|
expenseNonstockIDs := make([]uint64, 0)
|
||||||
|
|
||||||
|
|
||||||
for _, item := range items {
|
for _, item := range items {
|
||||||
if item.ExpenseNonstockId != nil && *item.ExpenseNonstockId != 0 {
|
if item.ExpenseNonstockId != nil && *item.ExpenseNonstockId != 0 {
|
||||||
expenseNonstockIDs = append(expenseNonstockIDs, *item.ExpenseNonstockId)
|
expenseNonstockIDs = append(expenseNonstockIDs, *item.ExpenseNonstockId)
|
||||||
@@ -92,7 +91,7 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(expenseNonstockIDs) > 0 {
|
if len(expenseNonstockIDs) > 0 {
|
||||||
|
|
||||||
for _, nsID := range expenseNonstockIDs {
|
for _, nsID := range expenseNonstockIDs {
|
||||||
var expenseID uint64
|
var expenseID uint64
|
||||||
if err := tx.Model(&entity.ExpenseNonstock{}).
|
if err := tx.Model(&entity.ExpenseNonstock{}).
|
||||||
@@ -106,13 +105,11 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil {
|
if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
|
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
|
||||||
for expenseID := range expenseIDs {
|
for expenseID := range expenseIDs {
|
||||||
var count int64
|
var count int64
|
||||||
@@ -122,7 +119,6 @@ func (b *transferExpenseBridge) OnItemsDeleted(ctx context.Context, _ uint64, it
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if count == 0 {
|
if count == 0 {
|
||||||
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
|
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -220,7 +216,6 @@ func (b *transferExpenseBridge) createExpenseViaService(
|
|||||||
for _, gi := range items {
|
for _, gi := range items {
|
||||||
note := fmt.Sprintf("stock_transfer_detail:%d", gi.detail.Id)
|
note := fmt.Sprintf("stock_transfer_detail:%d", gi.detail.Id)
|
||||||
|
|
||||||
|
|
||||||
price := gi.shippingCostTotal
|
price := gi.shippingCostTotal
|
||||||
if gi.payload.TransportPerItem != nil {
|
if gi.payload.TransportPerItem != nil {
|
||||||
price = *gi.payload.TransportPerItem * gi.payload.DeliveredQty
|
price = *gi.payload.TransportPerItem * gi.payload.DeliveredQty
|
||||||
@@ -228,7 +223,7 @@ func (b *transferExpenseBridge) createExpenseViaService(
|
|||||||
|
|
||||||
costItems = append(costItems, expenseValidation.CostItem{
|
costItems = append(costItems, expenseValidation.CostItem{
|
||||||
NonstockID: expeditionNonstockID,
|
NonstockID: expeditionNonstockID,
|
||||||
Quantity: 1,
|
Quantity: 1,
|
||||||
Price: price,
|
Price: price,
|
||||||
Notes: note,
|
Notes: note,
|
||||||
})
|
})
|
||||||
@@ -251,14 +246,16 @@ func (b *transferExpenseBridge) createExpenseViaService(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
action := entity.ApprovalActionApproved
|
action := entity.ApprovalActionApproved
|
||||||
actorID := uint(transfer.CreatedBy)
|
actorID := uint(transfer.CreatedBy)
|
||||||
if actorID == 0 {
|
if actorID == 0 {
|
||||||
actorID = 1
|
actorID = 1
|
||||||
}
|
}
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil {
|
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepHeadArea, &action, actorID, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepUnitVicePresident, &action, actorID, nil); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||||
@@ -328,7 +325,6 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64
|
|||||||
|
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
|
|
||||||
|
|
||||||
transfer, err := b.transferRepo.GetByID(ctx, uint(transferID), func(db *gorm.DB) *gorm.DB {
|
transfer, err := b.transferRepo.GetByID(ctx, uint(transferID), func(db *gorm.DB) *gorm.DB {
|
||||||
return db.
|
return db.
|
||||||
Preload("Details").
|
Preload("Details").
|
||||||
@@ -348,11 +344,10 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64
|
|||||||
for i := range transfer.Details {
|
for i := range transfer.Details {
|
||||||
detailMap[transfer.Details[i].Id] = &transfer.Details[i]
|
detailMap[transfer.Details[i].Id] = &transfer.Details[i]
|
||||||
|
|
||||||
|
|
||||||
for _, deliveryItem := range transfer.Details[i].DeliveryItems {
|
for _, deliveryItem := range transfer.Details[i].DeliveryItems {
|
||||||
if deliveryItem.StockTransferDelivery != nil {
|
if deliveryItem.StockTransferDelivery != nil {
|
||||||
shippingCostMap[transfer.Details[i].Id] = deliveryItem.StockTransferDelivery.ShippingCostTotal
|
shippingCostMap[transfer.Details[i].Id] = deliveryItem.StockTransferDelivery.ShippingCostTotal
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -395,17 +390,14 @@ func (b *transferExpenseBridge) OnItemsDelivered(c *fiber.Ctx, transferID uint64
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
shippingCostTotal := shippingCostMap[detail.Id]
|
shippingCostTotal := shippingCostMap[detail.Id]
|
||||||
|
|
||||||
|
|
||||||
totalPrice := shippingCostTotal
|
totalPrice := shippingCostTotal
|
||||||
if payload.TransportPerItem != nil {
|
if payload.TransportPerItem != nil {
|
||||||
|
|
||||||
totalPrice = *payload.TransportPerItem * payload.DeliveredQty
|
totalPrice = *payload.TransportPerItem * payload.DeliveredQty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
warehouseID := uint(payload.WarehouseID)
|
warehouseID := uint(payload.WarehouseID)
|
||||||
if warehouseID == 0 && transfer.ToWarehouse != nil {
|
if warehouseID == 0 && transfer.ToWarehouse != nil {
|
||||||
warehouseID = uint(transfer.ToWarehouse.Id)
|
warehouseID = uint(transfer.ToWarehouse.Id)
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ type Update struct {
|
|||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=500"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
LocationId int `query:"location_id" validate:"omitempty,number,gt=0"`
|
LocationId int `query:"location_id" validate:"omitempty,number,gt=0"`
|
||||||
PicId int `query:"pic_id" validate:"omitempty,number,gt=0"`
|
PicId int `query:"pic_id" validate:"omitempty,number,gt=0"`
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ type Update struct {
|
|||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=500"`
|
||||||
Search string `query:"search" validate:"omitempty,max=50"`
|
Search string `query:"search" validate:"omitempty,max=50"`
|
||||||
AreaId int `query:"area_id" validate:"omitempty,number,gt=0"`
|
AreaId int `query:"area_id" validate:"omitempty,number,gt=0"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
|
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
|
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
rChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services"
|
sChickin "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/services"
|
||||||
@@ -38,6 +39,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
|
projectflockpopulationrepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
|
||||||
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
projectFlockRepo := rProjectFlock.NewProjectflockRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
productRepo := rProduct.NewProductRepository(db)
|
||||||
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
|
||||||
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
|
||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
@@ -88,6 +90,7 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
kandangRepo,
|
kandangRepo,
|
||||||
warehouseRepo,
|
warehouseRepo,
|
||||||
productWarehouseRepo,
|
productWarehouseRepo,
|
||||||
|
productRepo,
|
||||||
projectFlockRepo,
|
projectFlockRepo,
|
||||||
projectflockkandangrepo,
|
projectflockkandangrepo,
|
||||||
projectflockpopulationrepo,
|
projectflockpopulationrepo,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
KandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
|
rProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/chickins/validations"
|
||||||
@@ -44,6 +45,7 @@ type chickinService struct {
|
|||||||
KandangRepo KandangRepo.KandangRepository
|
KandangRepo KandangRepo.KandangRepository
|
||||||
WarehouseRepo rWarehouse.WarehouseRepository
|
WarehouseRepo rWarehouse.WarehouseRepository
|
||||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||||
|
ProductRepo rProduct.ProductRepository
|
||||||
ProjectFlockRepo rProjectFlock.ProjectflockRepository
|
ProjectFlockRepo rProjectFlock.ProjectflockRepository
|
||||||
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
|
ProjectflockKandangRepo rProjectFlock.ProjectFlockKandangRepository
|
||||||
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
|
ProjectflockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
|
||||||
@@ -52,7 +54,7 @@ type chickinService struct {
|
|||||||
StockLogRepo rStockLogs.StockLogRepository
|
StockLogRepo rStockLogs.StockLogRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoSvc commonSvc.FifoService) ChickinService {
|
func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo KandangRepo.KandangRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, productRepo rProduct.ProductRepository, projectFlockRepo rProjectFlock.ProjectflockRepository, projectflockkandangRepo rProjectFlock.ProjectFlockKandangRepository, projectflockpopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectChickinDetailRepo repository.ProjectChickinDetailRepository, validate *validator.Validate, fifoSvc commonSvc.FifoService) ChickinService {
|
||||||
return &chickinService{
|
return &chickinService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
@@ -60,6 +62,7 @@ func NewChickinService(repo repository.ProjectChickinRepository, kandangRepo Kan
|
|||||||
KandangRepo: kandangRepo,
|
KandangRepo: kandangRepo,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
|
ProductRepo: productRepo,
|
||||||
ProjectFlockRepo: projectFlockRepo,
|
ProjectFlockRepo: projectFlockRepo,
|
||||||
ProjectflockKandangRepo: projectflockkandangRepo,
|
ProjectflockKandangRepo: projectflockkandangRepo,
|
||||||
ProjectflockPopulationRepo: projectflockpopulationRepo,
|
ProjectflockPopulationRepo: projectflockpopulationRepo,
|
||||||
@@ -99,7 +102,6 @@ func (s chickinService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity
|
|||||||
return db.Order("created_at DESC").Order("updated_at DESC")
|
return db.Order("created_at DESC").Order("updated_at DESC")
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get chickins: %+v", err)
|
|
||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
return chickins, total, nil
|
return chickins, total, nil
|
||||||
@@ -347,7 +349,6 @@ func (s chickinService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
return nil, fiber.NewError(fiber.StatusNotFound, "Chickin not found")
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to update chickin: %+v", err)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -380,7 +381,6 @@ func (s chickinService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
warehouseDeltas := make(map[uint]float64)
|
warehouseDeltas := make(map[uint]float64)
|
||||||
warehouseDeltas[chickin.ProductWarehouseId] += currentUsageQty
|
warehouseDeltas[chickin.ProductWarehouseId] += currentUsageQty
|
||||||
if err := s.adjustProductWarehouseQuantities(c.Context(), s.Repository.DB(), warehouseDeltas); err != nil {
|
if err := s.adjustProductWarehouseQuantities(c.Context(), s.Repository.DB(), warehouseDeltas); err != nil {
|
||||||
s.Log.Errorf("Failed to adjust product warehouses for deleted chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -449,6 +449,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
chickinRepoTx := repository.NewChickinRepository(dbTransaction)
|
||||||
|
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
|
||||||
|
|
||||||
for _, approvableID := range approvableIDs {
|
for _, approvableID := range approvableIDs {
|
||||||
if _, err := approvalSvc.CreateApproval(
|
if _, err := approvalSvc.CreateApproval(
|
||||||
@@ -479,39 +480,55 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
|
|
||||||
category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category))
|
category := strings.ToUpper(strings.TrimSpace(kandangForApproval.ProjectFlock.Category))
|
||||||
|
|
||||||
|
var targetFlag utils.FlagType
|
||||||
if category == string(utils.ProjectFlockCategoryGrowing) {
|
if category == string(utils.ProjectFlockCategoryGrowing) {
|
||||||
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
|
targetFlag = utils.FlagPullet
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
|
|
||||||
}
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
|
||||||
}
|
|
||||||
|
|
||||||
pfkID := approvableID
|
|
||||||
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "PULLET", dbTransaction, actorID, &pfkID)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create PULLET product warehouse")
|
|
||||||
}
|
|
||||||
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
|
|
||||||
}
|
|
||||||
} else if category == string(utils.ProjectFlockCategoryLaying) {
|
} else if category == string(utils.ProjectFlockCategoryLaying) {
|
||||||
warehouse, err := s.WarehouseRepo.GetByKandangID(c.Context(), kandangForApproval.KandangId)
|
targetFlag = utils.FlagLayer
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, chickin := range chickins {
|
||||||
|
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(c.Context(), chickin.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to check population for chickin %d", chickin.Id))
|
||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse for kandang %d not found", kandangForApproval.KandangId))
|
}
|
||||||
}
|
if populationExists {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
pfkID := approvableID
|
sourcePW, err := s.ProductWarehouseRepo.GetByID(c.Context(), chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
|
||||||
targetPW, err := s.getOrCreateProductWarehouse(c, warehouse.Id, "LAYER", dbTransaction, actorID, &pfkID)
|
return db.Preload("Product.Flags")
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get/create LAYER product warehouse")
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to get product warehouse for chickin %d", chickin.Id))
|
||||||
}
|
}
|
||||||
if err := s.convertChickinsToTarget(c, chickins, targetPW, dbTransaction, actorID); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to convert chickins to target")
|
if err := s.autoAddFlagToProduct(c.Context(), dbTransaction, sourcePW.Product.Id, targetFlag); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to auto-add flag to product %d", sourcePW.Product.Id))
|
||||||
|
}
|
||||||
|
|
||||||
|
population := &entity.ProjectFlockPopulation{
|
||||||
|
ProjectChickinId: chickin.Id,
|
||||||
|
ProductWarehouseId: sourcePW.Id,
|
||||||
|
TotalQty: 0,
|
||||||
|
TotalUsedQty: 0,
|
||||||
|
Notes: chickin.Notes,
|
||||||
|
CreatedBy: actorID,
|
||||||
|
}
|
||||||
|
if err := ProjectFlockPopulationRepotx.CreateOne(c.Context(), population, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to create population for chickin %d", chickin.Id))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := chickinRepoTx.PatchOne(c.Context(), chickin.Id, map[string]any{
|
||||||
|
"pending_usage_qty": 0,
|
||||||
|
}, nil); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to reset pending usage qty for chickin %d", chickin.Id))
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ReplenishChickinStocks(c.Context(), dbTransaction, &chickin, sourcePW, population, actorID); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, fmt.Sprintf("Failed to replenish stock for chickin %d", chickin.Id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -534,7 +551,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
warehouseDeltas := make(map[uint]float64)
|
warehouseDeltas := make(map[uint]float64)
|
||||||
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
|
warehouseDeltas[chickin.ProductWarehouseId] += chickin.UsageQty
|
||||||
if err := s.adjustProductWarehouseQuantities(c.Context(), dbTransaction, warehouseDeltas); err != nil {
|
if err := s.adjustProductWarehouseQuantities(c.Context(), dbTransaction, warehouseDeltas); err != nil {
|
||||||
s.Log.Errorf("Failed to adjust product warehouses for rejected chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,104 +584,35 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
return updated, nil
|
return updated, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *chickinService) getOrCreateProductWarehouse(ctx *fiber.Ctx, warehouseId uint, categoryCode string, dbTransaction *gorm.DB, actorID uint, projectFlockKandangId *uint) (*entity.ProductWarehouse, error) {
|
// autoAddFlagToProduct adds target flag to product if not already present (idempotent)
|
||||||
|
func (s *chickinService) autoAddFlagToProduct(ctx context.Context, tx *gorm.DB, productID uint, targetFlag utils.FlagType) error {
|
||||||
products, err := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(ctx.Context(), categoryCode, warehouseId)
|
if s.ProductRepo == nil {
|
||||||
if err == nil && len(products) > 0 {
|
return nil
|
||||||
existingPW := &products[0]
|
|
||||||
|
|
||||||
if existingPW.ProjectFlockKandangId == nil && projectFlockKandangId != nil {
|
|
||||||
existingPW.ProjectFlockKandangId = projectFlockKandangId
|
|
||||||
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).UpdateOne(ctx.Context(), existingPW.Id, existingPW, nil); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to update %s product warehouse with project_flock_kandang_id: %w", categoryCode, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return existingPW, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
product, err := s.ProductWarehouseRepo.GetFirstProductByFlag(ctx.Context(), categoryCode)
|
currentFlags, err := s.ProductRepo.GetFlags(ctx, productID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get %s product: %w", categoryCode, err)
|
return fmt.Errorf("failed to get product flags: %w", err)
|
||||||
}
|
|
||||||
if product == nil {
|
|
||||||
return nil, fmt.Errorf("no %s product found in system", categoryCode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
newPW := &entity.ProductWarehouse{
|
hasTargetFlag := false
|
||||||
ProductId: product.Id,
|
currentFlagNames := make([]string, 0, len(currentFlags))
|
||||||
WarehouseId: warehouseId,
|
for _, flag := range currentFlags {
|
||||||
ProjectFlockKandangId: projectFlockKandangId,
|
currentFlagNames = append(currentFlagNames, flag.Name)
|
||||||
Quantity: 0,
|
if flag.Name == string(targetFlag) {
|
||||||
|
hasTargetFlag = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.ProductWarehouseRepo.WithTx(dbTransaction).CreateOne(ctx.Context(), newPW, nil); err != nil {
|
if hasTargetFlag {
|
||||||
return nil, fmt.Errorf("failed to create %s product warehouse: %w", categoryCode, err)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return newPW, nil
|
newFlags := append(currentFlagNames, string(targetFlag))
|
||||||
}
|
if err := s.ProductRepo.SyncFlags(ctx, tx, productID, newFlags); err != nil {
|
||||||
|
return fmt.Errorf("failed to sync flags: %w", err)
|
||||||
func (s *chickinService) convertChickinsToTarget(ctx *fiber.Ctx, chickins []entity.ProjectChickin, targetPW *entity.ProductWarehouse, dbTransaction *gorm.DB, actorID uint) error {
|
|
||||||
|
|
||||||
if targetPW == nil || targetPW.Id == 0 {
|
|
||||||
return fmt.Errorf("invalid target product warehouse")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ProjectFlockPopulationRepotx := s.ProjectflockPopulationRepo.WithTx(dbTransaction)
|
|
||||||
chickinRepoTx := s.Repository.WithTx(dbTransaction)
|
|
||||||
|
|
||||||
var totalQuantityAdded float64
|
|
||||||
|
|
||||||
for _, chickin := range chickins {
|
|
||||||
|
|
||||||
populationExists, err := ProjectFlockPopulationRepotx.ExistsByProjectChickinID(ctx.Context(), chickin.Id)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to check population existence for chickin %d: %w", chickin.Id, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if populationExists {
|
|
||||||
s.Log.Infof("population already exists for chickin %d, skipping", chickin.Id)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
quantityToConvert := chickin.UsageQty
|
|
||||||
|
|
||||||
population := &entity.ProjectFlockPopulation{
|
|
||||||
ProjectChickinId: chickin.Id,
|
|
||||||
ProductWarehouseId: targetPW.Id,
|
|
||||||
TotalQty: 0, // Will be set by FIFO Replenish
|
|
||||||
TotalUsedQty: 0,
|
|
||||||
Notes: chickin.Notes,
|
|
||||||
CreatedBy: actorID,
|
|
||||||
}
|
|
||||||
if err := ProjectFlockPopulationRepotx.CreateOne(ctx.Context(), population, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset PendingUsageQty to 0 since population has been created
|
|
||||||
if err := chickinRepoTx.PatchOne(ctx.Context(), chickin.Id, map[string]any{
|
|
||||||
"pending_usage_qty": 0,
|
|
||||||
}, nil); err != nil {
|
|
||||||
return fmt.Errorf("failed to reset pending usage qty for chickin %d: %w", chickin.Id, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replenish stock to target ProductWarehouse based on source flag
|
|
||||||
// StockableKey is PROJECT_CHICKIN but StockableID refers to Population ID
|
|
||||||
if err := s.ReplenishChickinStocks(ctx.Context(), dbTransaction, &chickin, targetPW, population, actorID); err != nil {
|
|
||||||
s.Log.Errorf("Failed to replenish stock for chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
totalQuantityAdded += quantityToConvert
|
|
||||||
}
|
|
||||||
|
|
||||||
// NOTE: ProductWarehouse target sudah ditambah melalui ReplenishChickinStocks
|
|
||||||
// yang dipanggil di atas untuk setiap chickin berdasarkan flag source:
|
|
||||||
// - DOC → replenish ke PULLET
|
|
||||||
// - PULLET → replenish ke LAYER
|
|
||||||
// - LAYER → tidak perlu replenish (sudah final)
|
|
||||||
// - DOC+PULLET+LAYER → replenish ke dirinya sendiri
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,9 +621,6 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Log.Infof("ConsumeChickinStocks: chickin_id=%d, product_warehouse_id=%d, desired_qty=%.3f",
|
|
||||||
chickin.Id, chickin.ProductWarehouseId, desiredQty)
|
|
||||||
|
|
||||||
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
|
||||||
UsableKey: chickinUsableKey,
|
UsableKey: chickinUsableKey,
|
||||||
UsableID: chickin.Id,
|
UsableID: chickin.Id,
|
||||||
@@ -686,13 +630,9 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
Tx: tx,
|
Tx: tx,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to consume FIFO stock for chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
s.Log.Infof("ConsumeChickinStocks result: usage_qty=%.3f, pending_qty=%.3f, allocated_allocations=%d",
|
|
||||||
result.UsageQuantity, result.PendingQuantity, len(result.AddedAllocations))
|
|
||||||
|
|
||||||
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
if err := s.Repository.UpdateUsageFields(ctx, tx, chickin.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -706,10 +646,7 @@ func (s *chickinService) ConsumeChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
Notes: fmt.Sprintf("Chickin #%d", chickin.Id),
|
Notes: fmt.Sprintf("Chickin #%d", chickin.Id),
|
||||||
}
|
}
|
||||||
if err := s.StockLogRepo.CreateOne(ctx, decreaseLog, nil); err != nil {
|
s.StockLogRepo.CreateOne(ctx, decreaseLog, nil)
|
||||||
s.Log.Errorf("Failed to create stock log for chickin %d: %+v", chickin.Id, err)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -720,93 +657,17 @@ func (s *chickinService) ReplenishChickinStocks(ctx context.Context, tx *gorm.DB
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
sourcePW, err := s.ProductWarehouseRepo.GetByID(ctx, chickin.ProductWarehouseId, func(db *gorm.DB) *gorm.DB {
|
_, err := s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
||||||
return db.Preload("Product.Flags")
|
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
||||||
|
StockableID: population.Id,
|
||||||
|
ProductWarehouseID: targetPW.Id,
|
||||||
|
Quantity: chickin.UsageQty,
|
||||||
|
Tx: tx,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if sourcePW == nil || sourcePW.Product.Id == 0 {
|
|
||||||
return fmt.Errorf("source product warehouse or product not found for chickin %d", chickin.Id)
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceFlags := sourcePW.Product.Flags
|
|
||||||
if len(sourceFlags) == 0 {
|
|
||||||
s.Log.Warnf("Source product %d has no flags, skipping replenish for chickin %d", sourcePW.Product.Id, chickin.Id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
hasDoc := false
|
|
||||||
hasPullet := false
|
|
||||||
hasLayer := false
|
|
||||||
for _, flag := range sourceFlags {
|
|
||||||
flagName := utils.FlagType(flag.Name)
|
|
||||||
if flagName == utils.FlagDOC {
|
|
||||||
hasDoc = true
|
|
||||||
} else if flagName == utils.FlagPullet {
|
|
||||||
hasPullet = true
|
|
||||||
} else if flagName == utils.FlagLayer {
|
|
||||||
hasLayer = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasDoc && hasPullet && hasLayer {
|
|
||||||
s.Log.Infof("Chickin %d has mixed flags (DOC+PULLET+LAYER), replenishing to source PW %d", chickin.Id, sourcePW.Id)
|
|
||||||
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
|
||||||
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
|
||||||
StockableID: population.Id,
|
|
||||||
ProductWarehouseID: sourcePW.Id,
|
|
||||||
Quantity: chickin.UsageQty,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to replenish stock to source PW for chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// LAYER only - no replenish needed
|
|
||||||
if hasLayer && !hasDoc && !hasPullet {
|
|
||||||
s.Log.Infof("Chickin %d has LAYER flag only, skipping replenish", chickin.Id)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasDoc && !hasPullet && !hasLayer {
|
|
||||||
s.Log.Infof("Chickin %d has DOC flag, replenishing to PULLET PW %d", chickin.Id, targetPW.Id)
|
|
||||||
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
|
||||||
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
|
||||||
StockableID: population.Id,
|
|
||||||
ProductWarehouseID: targetPW.Id,
|
|
||||||
Quantity: chickin.UsageQty,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to replenish stock to PULLET PW for chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasPullet && !hasDoc && !hasLayer {
|
|
||||||
s.Log.Infof("Chickin %d has PULLET flag, replenishing to LAYER PW %d", chickin.Id, targetPW.Id)
|
|
||||||
_, err = s.FifoSvc.Replenish(ctx, commonSvc.StockReplenishRequest{
|
|
||||||
StockableKey: fifo.StockableKeyProjectFlockPopulation,
|
|
||||||
StockableID: population.Id,
|
|
||||||
ProductWarehouseID: targetPW.Id,
|
|
||||||
Quantity: chickin.UsageQty,
|
|
||||||
Tx: tx,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to replenish stock to LAYER PW for chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Other combinations (e.g., DOC + PULLET without LAYER) - skip for now
|
|
||||||
s.Log.Warnf("Chickin %d has unsupported flag combination, skipping replenish", chickin.Id)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -825,7 +686,6 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
UsableID: chickin.Id,
|
UsableID: chickin.Id,
|
||||||
Tx: tx,
|
Tx: tx,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
s.Log.Errorf("Failed to release FIFO stock for chickin %d: %+v", chickin.Id, err)
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -842,9 +702,7 @@ func (s *chickinService) ReleaseChickinStocks(ctx context.Context, tx *gorm.DB,
|
|||||||
CreatedBy: actorID,
|
CreatedBy: actorID,
|
||||||
Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
|
Notes: fmt.Sprintf("Chickin #%d - Stock released", chickin.Id),
|
||||||
}
|
}
|
||||||
if err := s.StockLogRepo.CreateOne(ctx, increaseLog, nil); err != nil {
|
s.StockLogRepo.CreateOne(ctx, increaseLog, nil)
|
||||||
s.Log.Errorf("Failed to create stock log for released chickin %d: %+v", chickin.Id, err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -618,7 +618,10 @@ func (b *expenseBridge) createExpenseViaService(
|
|||||||
actorID = 1
|
actorID = 1
|
||||||
}
|
}
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(b.db))
|
||||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepManager, &action, actorID, nil); err != nil {
|
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepHeadArea, &action, actorID, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepUnitVicePresident, &action, actorID, nil); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
if _, err := approvalSvc.CreateApproval(ctx, utils.ApprovalWorkflowExpense, uint(detail.Id), utils.ExpenseStepFinance, &action, actorID, nil); err != nil {
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
type DebtSupplierRowDTO struct {
|
type DebtSupplierRowDTO struct {
|
||||||
PrNumber string `json:"pr_number"`
|
PrNumber string `json:"pr_number"`
|
||||||
PoNumber string `json:"po_number"`
|
PoNumber string `json:"po_number"`
|
||||||
PrDate string `json:"pr_date"`
|
|
||||||
PoDate string `json:"po_date"`
|
PoDate string `json:"po_date"`
|
||||||
|
ReceivedDate string `json:"received_date"`
|
||||||
Aging int `json:"aging"`
|
Aging int `json:"aging"`
|
||||||
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
Area *areaDTO.AreaRelationDTO `json:"area,omitempty"`
|
||||||
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
|
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse,omitempty"`
|
||||||
@@ -21,6 +21,7 @@ type DebtSupplierRowDTO struct {
|
|||||||
DebtPrice float64 `json:"debt_price"`
|
DebtPrice float64 `json:"debt_price"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
TravelNumber string `json:"travel_number"`
|
TravelNumber string `json:"travel_number"`
|
||||||
|
Balance float64 `json:"balance"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type DebtSupplierTotalDTO struct {
|
type DebtSupplierTotalDTO struct {
|
||||||
@@ -31,7 +32,8 @@ type DebtSupplierTotalDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DebtSupplierDTO struct {
|
type DebtSupplierDTO struct {
|
||||||
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
|
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
|
||||||
Rows []DebtSupplierRowDTO `json:"rows"`
|
InitialBalance float64 `json:"initial_balance"`
|
||||||
Total DebtSupplierTotalDTO `json:"total"`
|
Rows []DebtSupplierRowDTO `json:"rows"`
|
||||||
|
Total DebtSupplierTotalDTO `json:"total"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,10 @@ import (
|
|||||||
type DebtSupplierRepository interface {
|
type DebtSupplierRepository interface {
|
||||||
GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.DebtSupplierQuery) ([]entity.Supplier, int64, error)
|
GetSuppliersWithPurchases(ctx context.Context, offset, limit int, filters *validation.DebtSupplierQuery) ([]entity.Supplier, int64, error)
|
||||||
GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error)
|
GetPurchasesBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Purchase, error)
|
||||||
|
GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error)
|
||||||
GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error)
|
GetPaymentTotalsByReferences(ctx context.Context, supplierIDs []uint, references []string) (map[string]float64, error)
|
||||||
|
GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
||||||
|
GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type debtSupplierRepositoryImpl struct {
|
type debtSupplierRepositoryImpl struct {
|
||||||
@@ -28,10 +31,10 @@ func NewDebtSupplierRepository(db *gorm.DB) DebtSupplierRepository {
|
|||||||
|
|
||||||
func resolveDebtSupplierDateColumn(filterBy string) string {
|
func resolveDebtSupplierDateColumn(filterBy string) string {
|
||||||
switch strings.ToLower(strings.TrimSpace(filterBy)) {
|
switch strings.ToLower(strings.TrimSpace(filterBy)) {
|
||||||
|
case "receive_date":
|
||||||
|
return "purchases.receive_date"
|
||||||
case "po_date":
|
case "po_date":
|
||||||
return "purchases.po_date"
|
return "purchases.po_date"
|
||||||
case "pr_date":
|
|
||||||
return "purchases.created_at"
|
|
||||||
case "do_date", "received_date", "":
|
case "do_date", "received_date", "":
|
||||||
return "purchase_items.received_date"
|
return "purchase_items.received_date"
|
||||||
default:
|
default:
|
||||||
@@ -157,6 +160,39 @@ func (r *debtSupplierRepositoryImpl) GetPurchasesBySuppliers(ctx context.Context
|
|||||||
return purchases, nil
|
return purchases, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *debtSupplierRepositoryImpl) GetPaymentsBySuppliers(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]entity.Payment, error) {
|
||||||
|
if len(supplierIDs) == 0 {
|
||||||
|
return []entity.Payment{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.Payment{}).
|
||||||
|
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
||||||
|
Where("direction = ?", "OUT").
|
||||||
|
Where("party_id IN ?", supplierIDs)
|
||||||
|
|
||||||
|
if strings.TrimSpace(filters.StartDate) != "" {
|
||||||
|
if dateFrom, err := utils.ParseDateString(filters.StartDate); err == nil {
|
||||||
|
db = db.Where("DATE(payment_date) >= ?", dateFrom)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(filters.EndDate) != "" {
|
||||||
|
if dateTo, err := utils.ParseDateString(filters.EndDate); err == nil {
|
||||||
|
db = db.Where("DATE(payment_date) <= ?", dateTo)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var payments []entity.Payment
|
||||||
|
if err := db.
|
||||||
|
Order("payment_date ASC, id ASC").
|
||||||
|
Find(&payments).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return payments, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *debtSupplierRepositoryImpl) getPurchaseIDs(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]uint, error) {
|
func (r *debtSupplierRepositoryImpl) getPurchaseIDs(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) ([]uint, error) {
|
||||||
dateColumn := resolveDebtSupplierDateColumn(filters.FilterBy)
|
dateColumn := resolveDebtSupplierDateColumn(filters.FilterBy)
|
||||||
|
|
||||||
@@ -219,3 +255,76 @@ func (r *debtSupplierRepositoryImpl) GetPaymentTotalsByReferences(ctx context.Co
|
|||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *debtSupplierRepositoryImpl) GetPurchaseTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
|
||||||
|
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
|
||||||
|
return map[uint]float64{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dateFrom, err := utils.ParseDateString(filters.StartDate)
|
||||||
|
if err != nil {
|
||||||
|
return map[uint]float64{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dateColumn := resolveDebtSupplierDateColumn(filters.FilterBy)
|
||||||
|
|
||||||
|
type purchaseTotalRow struct {
|
||||||
|
SupplierID uint `gorm:"column:supplier_id"`
|
||||||
|
Total float64 `gorm:"column:total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]purchaseTotalRow, 0)
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Table("purchases").
|
||||||
|
Select("purchases.supplier_id AS supplier_id, SUM(purchase_items.total_price) AS total").
|
||||||
|
Joins("JOIN purchase_items ON purchase_items.purchase_id = purchases.id").
|
||||||
|
Where("purchases.supplier_id IN ?", supplierIDs).
|
||||||
|
Where(fmt.Sprintf("DATE(%s) < ?", dateColumn), dateFrom).
|
||||||
|
Group("purchases.supplier_id").
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[uint]float64, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
result[row.SupplierID] = row.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *debtSupplierRepositoryImpl) GetPaymentTotalsBeforeDate(ctx context.Context, supplierIDs []uint, filters *validation.DebtSupplierQuery) (map[uint]float64, error) {
|
||||||
|
if len(supplierIDs) == 0 || strings.TrimSpace(filters.StartDate) == "" {
|
||||||
|
return map[uint]float64{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
dateFrom, err := utils.ParseDateString(filters.StartDate)
|
||||||
|
if err != nil {
|
||||||
|
return map[uint]float64{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type paymentTotalRow struct {
|
||||||
|
SupplierID uint `gorm:"column:supplier_id"`
|
||||||
|
Total float64 `gorm:"column:total"`
|
||||||
|
}
|
||||||
|
|
||||||
|
rows := make([]paymentTotalRow, 0)
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.Payment{}).
|
||||||
|
Select("party_id AS supplier_id, SUM(nominal) AS total").
|
||||||
|
Where("party_type = ?", string(utils.PaymentPartySupplier)).
|
||||||
|
Where("direction = ?", "OUT").
|
||||||
|
Where("party_id IN ?", supplierIDs).
|
||||||
|
Where("DATE(payment_date) < ?", dateFrom).
|
||||||
|
Group("party_id").
|
||||||
|
Scan(&rows).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[uint]float64, len(rows))
|
||||||
|
for _, row := range rows {
|
||||||
|
result[row.SupplierID] = row.Total
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -642,8 +642,8 @@ func (s *repportService) GetPurchaseSupplier(c *fiber.Ctx, params *validation.Pu
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) {
|
func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSupplierQuery) ([]dto.DebtSupplierDTO, int64, error) {
|
||||||
if params.FilterBy == "" {
|
if params.FilterBy == "" || strings.EqualFold(strings.TrimSpace(params.FilterBy), "do_date") {
|
||||||
params.FilterBy = "do_date"
|
params.FilterBy = "received_date"
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.Validate.Struct(params); err != nil {
|
if err := s.Validate.Struct(params); err != nil {
|
||||||
@@ -675,6 +675,11 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
payments, err := s.DebtSupplierRepo.GetPaymentsBySuppliers(c.Context(), supplierIDs, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs))
|
purchasesBySupplier := make(map[uint][]entity.Purchase, len(supplierIDs))
|
||||||
references := make([]string, 0)
|
references := make([]string, 0)
|
||||||
seenRefs := make(map[string]struct{})
|
seenRefs := make(map[string]struct{})
|
||||||
@@ -697,6 +702,21 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
return nil, 0, err
|
return nil, 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
paymentsBySupplier := make(map[uint][]entity.Payment, len(supplierIDs))
|
||||||
|
for _, payment := range payments {
|
||||||
|
paymentsBySupplier[payment.PartyId] = append(paymentsBySupplier[payment.PartyId], payment)
|
||||||
|
}
|
||||||
|
|
||||||
|
initialPurchaseTotals, err := s.DebtSupplierRepo.GetPurchaseTotalsBeforeDate(c.Context(), supplierIDs, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
initialPaymentTotals, err := s.DebtSupplierRepo.GetPaymentTotalsBeforeDate(c.Context(), supplierIDs, params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
location, err := time.LoadLocation("Asia/Jakarta")
|
location, err := time.LoadLocation("Asia/Jakarta")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
|
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "failed to load timezone configuration")
|
||||||
@@ -710,29 +730,81 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initialBalance := initialPaymentTotals[supplierID] - initialPurchaseTotals[supplierID]
|
||||||
|
|
||||||
items := purchasesBySupplier[supplierID]
|
items := purchasesBySupplier[supplierID]
|
||||||
rows := make([]dto.DebtSupplierRowDTO, 0, len(items))
|
paymentItems := paymentsBySupplier[supplierID]
|
||||||
|
rows := make([]dto.DebtSupplierRowDTO, 0, len(items)+len(paymentItems))
|
||||||
total := dto.DebtSupplierTotalDTO{}
|
total := dto.DebtSupplierTotalDTO{}
|
||||||
|
|
||||||
|
type debtSupplierRowItem struct {
|
||||||
|
Row dto.DebtSupplierRowDTO
|
||||||
|
SortTime time.Time
|
||||||
|
Order int
|
||||||
|
DeltaBalance float64
|
||||||
|
CountTotals bool
|
||||||
|
}
|
||||||
|
|
||||||
|
combinedRows := make([]debtSupplierRowItem, 0, len(items)+len(paymentItems))
|
||||||
for _, purchase := range items {
|
for _, purchase := range items {
|
||||||
row := buildDebtSupplierRow(purchase, paymentTotals, now, location)
|
row := buildDebtSupplierRow(purchase, paymentTotals, now, location)
|
||||||
rows = append(rows, row)
|
sortTime := resolveDebtSupplierSortTime(purchase, params.FilterBy, location)
|
||||||
|
combinedRows = append(combinedRows, debtSupplierRowItem{
|
||||||
|
Row: row,
|
||||||
|
SortTime: sortTime,
|
||||||
|
Order: 0,
|
||||||
|
DeltaBalance: -row.TotalPrice,
|
||||||
|
CountTotals: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
if row.Aging > total.Aging {
|
for _, payment := range paymentItems {
|
||||||
total.Aging = row.Aging
|
row := buildDebtSupplierPaymentRow(payment, location)
|
||||||
|
sortTime := payment.PaymentDate.In(location)
|
||||||
|
combinedRows = append(combinedRows, debtSupplierRowItem{
|
||||||
|
Row: row,
|
||||||
|
SortTime: sortTime,
|
||||||
|
Order: 1,
|
||||||
|
DeltaBalance: payment.Nominal,
|
||||||
|
CountTotals: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.SliceStable(combinedRows, func(i, j int) bool {
|
||||||
|
if combinedRows[i].SortTime.Equal(combinedRows[j].SortTime) {
|
||||||
|
return combinedRows[i].Order < combinedRows[j].Order
|
||||||
|
}
|
||||||
|
return combinedRows[i].SortTime.Before(combinedRows[j].SortTime)
|
||||||
|
})
|
||||||
|
|
||||||
|
balance := initialBalance
|
||||||
|
for i := range combinedRows {
|
||||||
|
balance += combinedRows[i].DeltaBalance
|
||||||
|
combinedRows[i].Row.Balance = balance
|
||||||
|
|
||||||
|
if combinedRows[i].CountTotals {
|
||||||
|
row := combinedRows[i].Row
|
||||||
|
if row.Aging > total.Aging {
|
||||||
|
total.Aging = row.Aging
|
||||||
|
}
|
||||||
|
total.TotalPrice += row.TotalPrice
|
||||||
|
total.PaymentPrice += row.PaymentPrice
|
||||||
|
total.DebtPrice += row.DebtPrice
|
||||||
|
} else {
|
||||||
|
combinedRows[i].Row.DebtPrice = balance
|
||||||
}
|
}
|
||||||
total.TotalPrice += row.TotalPrice
|
|
||||||
total.PaymentPrice += row.PaymentPrice
|
|
||||||
total.DebtPrice += row.DebtPrice
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sortDesc := strings.EqualFold(params.SortOrder, "desc")
|
sortDesc := strings.EqualFold(params.SortOrder, "desc")
|
||||||
sort.SliceStable(rows, func(i, j int) bool {
|
if sortDesc {
|
||||||
if sortDesc {
|
for i := len(combinedRows) - 1; i >= 0; i-- {
|
||||||
return rows[i].PrDate > rows[j].PrDate
|
rows = append(rows, combinedRows[i].Row)
|
||||||
}
|
}
|
||||||
return rows[i].PrDate < rows[j].PrDate
|
} else {
|
||||||
})
|
for i := range combinedRows {
|
||||||
|
rows = append(rows, combinedRows[i].Row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var supplierDTORef *supplierDTO.SupplierRelationDTO
|
var supplierDTORef *supplierDTO.SupplierRelationDTO
|
||||||
if supplier.Id != 0 {
|
if supplier.Id != 0 {
|
||||||
@@ -741,9 +813,10 @@ func (s *repportService) GetDebtSupplier(c *fiber.Ctx, params *validation.DebtSu
|
|||||||
}
|
}
|
||||||
|
|
||||||
result = append(result, dto.DebtSupplierDTO{
|
result = append(result, dto.DebtSupplierDTO{
|
||||||
Supplier: supplierDTORef,
|
Supplier: supplierDTORef,
|
||||||
Rows: rows,
|
InitialBalance: initialBalance,
|
||||||
Total: total,
|
Rows: rows,
|
||||||
|
Total: total,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -769,6 +842,7 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
|||||||
|
|
||||||
totalPrice := 0.0
|
totalPrice := 0.0
|
||||||
travelNumber := "-"
|
travelNumber := "-"
|
||||||
|
receivedDate := ""
|
||||||
var area *areaDTO.AreaRelationDTO
|
var area *areaDTO.AreaRelationDTO
|
||||||
var warehouse *warehouseDTO.WarehouseRelationDTO
|
var warehouse *warehouseDTO.WarehouseRelationDTO
|
||||||
|
|
||||||
@@ -787,8 +861,19 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
earliestReceived := time.Time{}
|
||||||
for _, item := range purchase.Items {
|
for _, item := range purchase.Items {
|
||||||
totalPrice += item.TotalPrice
|
totalPrice += item.TotalPrice
|
||||||
|
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
received := item.ReceivedDate.In(loc)
|
||||||
|
if earliestReceived.IsZero() || received.Before(earliestReceived) {
|
||||||
|
earliestReceived = received
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !earliestReceived.IsZero() {
|
||||||
|
receivedDate = earliestReceived.Format("2006-01-02")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -820,8 +905,8 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
|||||||
return dto.DebtSupplierRowDTO{
|
return dto.DebtSupplierRowDTO{
|
||||||
PrNumber: prNumber,
|
PrNumber: prNumber,
|
||||||
PoNumber: poNumber,
|
PoNumber: poNumber,
|
||||||
PrDate: prDate.Format("2006-01-02"),
|
|
||||||
PoDate: poDate,
|
PoDate: poDate,
|
||||||
|
ReceivedDate: receivedDate,
|
||||||
Aging: aging,
|
Aging: aging,
|
||||||
Area: area,
|
Area: area,
|
||||||
Warehouse: warehouse,
|
Warehouse: warehouse,
|
||||||
@@ -835,6 +920,62 @@ func buildDebtSupplierRow(purchase entity.Purchase, paymentTotals map[string]flo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func buildDebtSupplierPaymentRow(payment entity.Payment, loc *time.Location) dto.DebtSupplierRowDTO {
|
||||||
|
referenceNumber := ""
|
||||||
|
if payment.ReferenceNumber != nil {
|
||||||
|
referenceNumber = *payment.ReferenceNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
prNumber := payment.PaymentCode
|
||||||
|
if strings.TrimSpace(prNumber) == "" {
|
||||||
|
prNumber = referenceNumber
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto.DebtSupplierRowDTO{
|
||||||
|
PrNumber: prNumber,
|
||||||
|
PoNumber: referenceNumber,
|
||||||
|
PoDate: "-",
|
||||||
|
ReceivedDate: payment.PaymentDate.In(loc).Format("2006-01-02"),
|
||||||
|
Aging: 0,
|
||||||
|
Area: nil,
|
||||||
|
Warehouse: nil,
|
||||||
|
DueDate: "-",
|
||||||
|
DueStatus: "-",
|
||||||
|
TotalPrice: 0,
|
||||||
|
PaymentPrice: payment.Nominal,
|
||||||
|
DebtPrice: 0,
|
||||||
|
Status: "Pembayaran",
|
||||||
|
TravelNumber: "-",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resolveDebtSupplierSortTime(purchase entity.Purchase, filterBy string, loc *time.Location) time.Time {
|
||||||
|
switch strings.ToLower(strings.TrimSpace(filterBy)) {
|
||||||
|
case "po_date":
|
||||||
|
if purchase.PoDate != nil && !purchase.PoDate.IsZero() {
|
||||||
|
return purchase.PoDate.In(loc)
|
||||||
|
}
|
||||||
|
case "pr_date":
|
||||||
|
return purchase.CreatedAt.In(loc)
|
||||||
|
default:
|
||||||
|
earliest := time.Time{}
|
||||||
|
for _, item := range purchase.Items {
|
||||||
|
if item.ReceivedDate == nil || item.ReceivedDate.IsZero() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
received := item.ReceivedDate.In(loc)
|
||||||
|
if earliest.IsZero() || received.Before(earliest) {
|
||||||
|
earliest = received
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !earliest.IsZero() {
|
||||||
|
return earliest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return purchase.CreatedAt.In(loc)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
|
func (s *repportService) GetHppPerKandang(ctx *fiber.Ctx) (*dto.HppPerKandangResponseData, *dto.HppPerKandangMetaDTO, error) {
|
||||||
params, filters, err := s.parseHppPerKandangQuery(ctx)
|
params, filters, err := s.parseHppPerKandangQuery(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ type DebtSupplierQuery struct {
|
|||||||
SupplierIDs []int64 `query:"-" validate:"omitempty,dive,gt=0"`
|
SupplierIDs []int64 `query:"-" validate:"omitempty,dive,gt=0"`
|
||||||
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
StartDate string `query:"start_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
EndDate string `query:"end_date" validate:"omitempty,datetime=2006-01-02"`
|
||||||
FilterBy string `query:"filter_by" validate:"omitempty,oneof=do_date po_date pr_date"`
|
FilterBy string `query:"filter_by" validate:"omitempty,oneof=received_date po_date pr_date do_date"`
|
||||||
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
|
SortOrder string `query:"sort_order" validate:"omitempty,oneof=asc desc"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+13
-11
@@ -354,20 +354,22 @@ var MarketingApprovalSteps = map[approvalutils.ApprovalStep]string{
|
|||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ApprovalWorkflowExpense approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("EXPENSES")
|
ApprovalWorkflowExpense approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("EXPENSES")
|
||||||
ExpenseStepPengajuan approvalutils.ApprovalStep = 1
|
ExpenseStepPengajuan approvalutils.ApprovalStep = 1
|
||||||
ExpenseStepManager approvalutils.ApprovalStep = 2
|
ExpenseStepHeadArea approvalutils.ApprovalStep = 2
|
||||||
ExpenseStepFinance approvalutils.ApprovalStep = 3
|
ExpenseStepUnitVicePresident approvalutils.ApprovalStep = 3
|
||||||
ExpenseStepRealisasi approvalutils.ApprovalStep = 4
|
ExpenseStepFinance approvalutils.ApprovalStep = 4
|
||||||
ExpenseStepSelesai approvalutils.ApprovalStep = 5
|
ExpenseStepRealisasi approvalutils.ApprovalStep = 5
|
||||||
|
ExpenseStepSelesai approvalutils.ApprovalStep = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{
|
var ExpenseApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||||
ExpenseStepPengajuan: "Pengajuan",
|
ExpenseStepPengajuan: "Pengajuan",
|
||||||
ExpenseStepManager: "Approval Manager",
|
ExpenseStepHeadArea: "Approval Head Area",
|
||||||
ExpenseStepFinance: "Approval Finance",
|
ExpenseStepUnitVicePresident: "Approval Business Unit Vice President",
|
||||||
ExpenseStepRealisasi: "Realisasi",
|
ExpenseStepFinance: "Approval Finance",
|
||||||
ExpenseStepSelesai: "Selesai",
|
ExpenseStepRealisasi: "Realisasi",
|
||||||
|
ExpenseStepSelesai: "Selesai",
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user