Compare commits

..

15 Commits

Author SHA1 Message Date
kris 1f003c512d Update .gitlab-ci.yml file 2025-12-17 08:12:29 +00:00
kris 3ca95750a7 Update .gitlab-ci.yml file 2025-12-17 08:04:18 +00:00
kris 5dd2dbfa98 Update .gitlab-ci.yml file 2025-12-15 05:15:55 +00:00
M1 AIR 00bfd5ca45 . 2025-12-15 12:10:54 +07:00
M1 AIR 3e8bcb3f64 . 2025-12-15 12:08:24 +07:00
M1 AIR cfc4846170 . 2025-12-15 12:06:30 +07:00
M1 AIR c8f96c6086 . 2025-12-15 12:00:46 +07:00
M1 AIR 930fe25589 . 2025-12-15 11:54:55 +07:00
M1 AIR 4137008b92 . 2025-12-15 11:52:02 +07:00
kris 477f65554e Update .gitlab-ci.yml file 2025-12-15 04:50:12 +00:00
M1 AIR 6fcbcfd554 . 2025-12-15 11:43:37 +07:00
M1 AIR 99c34c5793 . 2025-12-15 11:37:11 +07:00
M1 AIR ecc9a51ea7 . 2025-12-15 11:34:45 +07:00
M1 AIR 75ade0d8b3 . 2025-12-15 11:32:43 +07:00
GitLab Deploy Bot b6a60d5009 remove 2025-12-15 09:25:50 +07:00
106 changed files with 1212 additions and 3817 deletions
-13
View File
@@ -1,13 +0,0 @@
# .air.toml
root = "."
tmp_dir = "tmp"
[build]
cmd = "go build -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
+54 -73
View File
@@ -1,90 +1,71 @@
stages: stages:
- build
- deploy - deploy
deploy-dev: variables:
DOCKER_BUILDKIT: "1"
DOCKER_DRIVER: overlay2
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
IMAGE_TAG: "stg-ec2_${CI_COMMIT_SHORT_SHA}"
IMAGE_NAME: "${CI_REGISTRY_IMAGE}:${IMAGE_TAG}"
IMAGE_LATEST_STG_EC2: "${CI_REGISTRY_IMAGE}:stg-ec2_latest"
build:stg-ec2:
stage: build
image: docker:27.0.3
services:
- name: docker:27.0.3-dind
command: ["--mtu=1460"]
rules:
- if: '$CI_COMMIT_BRANCH == "stg-ec2"'
before_script:
- docker info
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
script:
- docker build -t "$IMAGE_NAME" -f Dockerfile .
- docker push "$IMAGE_NAME"
- docker tag "$IMAGE_NAME" "$IMAGE_LATEST_STG_EC2"
- docker push "$IMAGE_LATEST_STG_EC2"
deploy:stg-ec2:
stage: deploy stage: deploy
image: alpine:3.20 image: alpine:3.20
variables: rules:
DEPLOY_APP: "LTI-MBUGROUP" - if: '$CI_COMMIT_BRANCH == "stg-ec2"'
# Opsional: kalau pakai submodule, ini bikin clone submodule pakai SSH juga needs:
GIT_SUBMODULE_STRATEGY: recursive - job: build:stg-ec2
GIT_DEPTH: "1"
before_script: before_script:
- echo "🧰 Installing dependencies..." - apk add --no-cache openssh-client bash ca-certificates
- apk update && apk add --no-cache openssh git curl bash
# Setup SSH di runner
- mkdir -p ~/.ssh - mkdir -p ~/.ssh
- echo "$SSH_PRIVATE_KEY" | tr -d '\r' > ~/.ssh/id_rsa - chmod 700 ~/.ssh
# SSH_PRIVATE_KEY = multiline private key (bukan File)
- printf "%s\n" "$SSH_PRIVATE_KEY" > ~/.ssh/id_rsa
- sed -i 's/\r$//' ~/.ssh/id_rsa
- chmod 600 ~/.ssh/id_rsa - chmod 600 ~/.ssh/id_rsa
- head -n 1 ~/.ssh/id_rsa
- tail -n 1 ~/.ssh/id_rsa
- eval "$(ssh-agent -s)" - eval "$(ssh-agent -s)"
- ssh-add ~/.ssh/id_rsa - 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 "$SERVER_IP" >> ~/.ssh/known_hosts
- ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts
script: script:
- echo "🚀 Deploying latest code to $SERVER_USER@$SERVER_IP"
- > - >
if ssh -o StrictHostKeyChecking=no "$SERVER_USER@$SERVER_IP" " ssh "$SERVER_USER@$SERVER_IP"
set -e "export CI_REGISTRY_USER='$CI_REGISTRY_USER';
export CI_REGISTRY_PASSWORD='$CI_REGISTRY_PASSWORD';
cd /home/devops/docker/deployment/development/lti-api export CI_REGISTRY='$CI_REGISTRY';
set -e;
# Pastikan remote origin SSH (antisipasi kalau pernah ke-set HTTPS) cd /home/ubuntu/docker/deployment/staging/stg-lti-api;
git remote set-url origin git@gitlab.com:mbugroup/lti-api.git echo \"\$CI_REGISTRY_PASSWORD\" | docker login -u \"\$CI_REGISTRY_USER\" --password-stdin \"\$CI_REGISTRY\";
docker compose pull;
# Pastikan server percaya gitlab.com juga (untuk git fetch via SSH) docker compose up -d;
mkdir -p ~/.ssh docker image prune -f"
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: environment:
name: development name: staging
+26 -11
View File
@@ -1,20 +1,35 @@
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 binary dari cmd/api
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w" -o lti-api ./cmd/api
# =========================
# Runtime stage
# =========================
FROM alpine:3.20
RUN apk add --no-cache ca-certificates tzdata curl \
&& adduser -D -H -u 10001 appuser
WORKDIR /app
COPY --from=builder /app/lti-api /app/lti-api
USER appuser
# Samakan dengan APP_PORT default kamu (8081)
EXPOSE 8081 EXPOSE 8081
CMD ["air", "-c", ".air.toml"] CMD ["/app/lti-api"]
-3
View File
@@ -1,3 +0,0 @@
POSTGRES_USER=postgres
POSTGRES_PASSWORD=Postgres@Secure2025!
POSTGRES_DB=db_lti_erp
-47
View File
@@ -1,47 +0,0 @@
-- ============================================================
-- 🧩 INIT SCRIPT: CREATE LIMITED APP USER FOR LTI API
-- ============================================================
-- Buat user aplikasi jika belum ada
DO
$$
BEGIN
IF NOT EXISTS (SELECT FROM pg_catalog.pg_roles WHERE rolname = 'app_lti_user') THEN
CREATE ROLE app_lti_user WITH LOGIN PASSWORD 'AppLti@Secure2025!' NOINHERIT NOCREATEROLE NOCREATEDB NOSUPERUSER;
RAISE NOTICE '✅ Role app_lti_user created successfully.';
ELSE
RAISE NOTICE '️ Role app_lti_user already exists.';
END IF;
END
$$;
-- Buat database jika belum ada
DO
$$
BEGIN
IF NOT EXISTS (SELECT FROM pg_database WHERE datname = 'db_lti_erp') THEN
CREATE DATABASE db_lti_erp OWNER app_lti_user;
RAISE NOTICE '✅ Database db_lti_erp created and owned by app_lti_user.';
ELSE
RAISE NOTICE '️ Database db_lti_erp already exists.';
END IF;
END
$$;
\connect db_lti_erp
-- Beri hak CRUD untuk app_lti_user
GRANT CONNECT ON DATABASE db_lti_erp TO app_lti_user;
GRANT USAGE ON SCHEMA public TO app_lti_user;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_lti_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_lti_user;
-- Set default privileges agar tabel baru juga bisa diakses
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_lti_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA public
GRANT USAGE, SELECT ON SEQUENCES TO app_lti_user;
-- Tampilkan hasil
\du app_lti_user
-77
View File
@@ -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
-98
View File
@@ -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:
@@ -1,75 +0,0 @@
package repository
import (
"context"
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type StockAllocationRepository interface {
BaseRepository[entity.StockAllocation]
FindActiveByUsable(ctx context.Context, usableType string, usableID uint, modifier func(*gorm.DB) *gorm.DB) ([]entity.StockAllocation, error)
ReleaseByUsable(ctx context.Context, usableType string, usableID uint, note *string, modifier func(*gorm.DB) *gorm.DB) error
}
type StockAllocationRepositoryImpl struct {
*BaseRepositoryImpl[entity.StockAllocation]
}
func NewStockAllocationRepository(db *gorm.DB) StockAllocationRepository {
return &StockAllocationRepositoryImpl{
BaseRepositoryImpl: NewBaseRepository[entity.StockAllocation](db),
}
}
func (r *StockAllocationRepositoryImpl) FindActiveByUsable(
ctx context.Context,
usableType string,
usableID uint,
modifier func(*gorm.DB) *gorm.DB,
) ([]entity.StockAllocation, error) {
var allocations []entity.StockAllocation
q := r.DB().WithContext(ctx).
Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive)
if modifier != nil {
q = modifier(q)
}
if err := q.Order("created_at ASC").Find(&allocations).Error; err != nil {
return nil, err
}
return allocations, nil
}
func (r *StockAllocationRepositoryImpl) ReleaseByUsable(
ctx context.Context,
usableType string,
usableID uint,
note *string,
modifier func(*gorm.DB) *gorm.DB,
) error {
now := time.Now()
updates := map[string]any{
"status": entity.StockAllocationStatusReleased,
"released_at": now,
}
if note != nil {
updates["note"] = *note
}
q := r.DB().WithContext(ctx).
Model(&entity.StockAllocation{}).
Where("usable_type = ? AND usable_id = ? AND status = ?", usableType, usableID, entity.StockAllocationStatusActive)
if modifier != nil {
q = modifier(q)
}
return q.Updates(updates).Error
}
@@ -1,820 +0,0 @@
package service
import (
"context"
"errors"
"fmt"
"math"
"sort"
"strings"
"time"
"github.com/sirupsen/logrus"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
"gitlab.com/mbugroup/lti-api.git/internal/entities"
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type FifoService interface {
RegisterStockable(cfg fifo.StockableConfig) error
RegisterUsable(cfg fifo.UsableConfig) error
Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error)
Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error)
ReleaseUsage(ctx context.Context, req StockReleaseRequest) error
}
type fifoService struct {
db *gorm.DB
logger *logrus.Logger
allocations commonRepo.StockAllocationRepository
productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
defaultOrderBy []string
pendingBatchPerUsable int
maxLotsPerStockable int
defaultAllocationNotes string
}
func NewFifoService(
db *gorm.DB,
allocations commonRepo.StockAllocationRepository,
productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository,
logger *logrus.Logger,
) FifoService {
if logger == nil {
logger = logrus.StandardLogger()
}
return &fifoService{
db: db,
logger: logger,
allocations: allocations,
productWarehouseRepo: productWarehouseRepo,
defaultOrderBy: []string{"created_at ASC", "id ASC"},
pendingBatchPerUsable: 25,
maxLotsPerStockable: 50,
}
}
func (s *fifoService) withTransaction(
ctx context.Context,
tx *gorm.DB,
fn func(*gorm.DB) error,
) error {
if tx != nil {
return fn(tx.WithContext(ctx))
}
return s.db.WithContext(ctx).Transaction(func(inner *gorm.DB) error {
return fn(inner)
})
}
func (s *fifoService) txOrDB(tx, db *gorm.DB) *gorm.DB {
if tx != nil {
return tx
}
return db
}
func (s *fifoService) RegisterStockable(cfg fifo.StockableConfig) error {
return fifo.RegisterStockable(cfg)
}
func (s *fifoService) RegisterUsable(cfg fifo.UsableConfig) error {
return fifo.RegisterUsable(cfg)
}
type StockReplenishRequest struct {
StockableKey fifo.StockableKey
StockableID uint
ProductWarehouseID uint
Quantity float64
Note *string
Tx *gorm.DB
}
type PendingResolution struct {
UsableKey fifo.UsableKey
UsableID uint
Quantity float64
}
type StockReplenishResult struct {
AddedQuantity float64
PendingResolved []PendingResolution
RemainingPending float64
}
type StockConsumeRequest struct {
UsableKey fifo.UsableKey
UsableID uint
ProductWarehouseID uint
Quantity float64
AllowPending bool
Note *string
Tx *gorm.DB
}
type AllocationDetail struct {
StockableKey fifo.StockableKey
StockableID uint
Quantity float64
}
type StockConsumeResult struct {
RequestedQuantity float64
UsageQuantity float64
PendingQuantity float64
AddedAllocations []AllocationDetail
ReleasedQuantity float64
}
type StockReleaseRequest struct {
UsableKey fifo.UsableKey
UsableID uint
Reason *string
Tx *gorm.DB
}
func (s *fifoService) Replenish(ctx context.Context, req StockReplenishRequest) (*StockReplenishResult, error) {
if req.StockableID == 0 || strings.TrimSpace(req.StockableKey.String()) == "" {
return nil, errors.New("stockable key and id are required")
}
if req.ProductWarehouseID == 0 {
return nil, errors.New("product warehouse id is required")
}
if req.Quantity <= 0 {
return nil, errors.New("quantity must be greater than zero")
}
cfg, ok := fifo.Stockable(req.StockableKey)
if !ok {
return nil, fmt.Errorf("stockable %q is not registered", req.StockableKey)
}
result := &StockReplenishResult{
AddedQuantity: req.Quantity,
}
err := 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
}
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
req.ProductWarehouseID: req.Quantity,
}, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
return err
}
resolved, err := s.resolvePendingForWarehouse(ctx, tx, req.ProductWarehouseID)
if err != nil {
return err
}
result.PendingResolved = resolved
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
func (s *fifoService) Consume(ctx context.Context, req StockConsumeRequest) (*StockConsumeResult, error) {
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
return nil, errors.New("usable key and id are required")
}
if req.Quantity < 0 {
return nil, errors.New("quantity must be zero or greater")
}
cfg, ok := fifo.Usable(req.UsableKey)
if !ok {
return nil, fmt.Errorf("usable %q is not registered", req.UsableKey)
}
result := &StockConsumeResult{
RequestedQuantity: req.Quantity,
}
err := s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
ctxRow, err := s.loadUsableContext(ctx, tx, cfg, req.UsableID)
if err != nil {
return err
}
productWarehouseID := ctxRow.ProductWarehouseID
if productWarehouseID == 0 {
return fmt.Errorf("usable %q (id: %d) has no product warehouse reference", req.UsableKey, req.UsableID)
}
if req.ProductWarehouseID != 0 && req.ProductWarehouseID != productWarehouseID {
return fmt.Errorf("usable %q (id: %d) references product warehouse %d but %d was provided", req.UsableKey, req.UsableID, productWarehouseID, req.ProductWarehouseID)
}
currentUsage := ctxRow.UsageQty
currentPending := ctxRow.PendingQty
currentTotal := currentUsage + currentPending
delta := req.Quantity - currentTotal
var (
usageDelta float64
pendingDelta float64
addedAlloc []AllocationDetail
releasedAmount float64
)
switch {
case delta > 0:
allocationRes, err := s.allocateFromStock(ctx, tx, productWarehouseID, req.UsableKey, req.UsableID, delta)
if err != nil {
return err
}
if allocationRes.pending > 0 && !req.AllowPending {
return fmt.Errorf("insufficient stock: requested %.3f, allocated %.3f", req.Quantity, currentUsage+allocationRes.allocated)
}
usageDelta += allocationRes.allocated
pendingDelta += allocationRes.pending
addedAlloc = allocationRes.allocations
if allocationRes.allocated > 0 {
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
productWarehouseID: -allocationRes.allocated,
}, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
return err
}
}
case delta < 0:
reductionTarget := -delta
if currentPending > 0 {
pendingReduction := math.Min(currentPending, reductionTarget)
if pendingReduction > 0 {
pendingDelta -= pendingReduction
reductionTarget -= pendingReduction
}
}
if reductionTarget > 0 {
released, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, reductionTarget)
if err != nil {
return err
}
if released+1e-6 < reductionTarget {
return fmt.Errorf("unable to release %.3f from usable %d, only %.3f available", reductionTarget, req.UsableID, released)
}
usageDelta -= released
releasedAmount = released
}
default:
// no change
}
if err := s.applyUsableDeltas(ctx, tx, cfg, req.UsableID, usageDelta, pendingDelta); err != nil {
return err
}
result.AddedAllocations = addedAlloc
result.ReleasedQuantity = releasedAmount
result.UsageQuantity = currentUsage + usageDelta
result.PendingQuantity = currentPending + pendingDelta
return nil
})
if err != nil {
return nil, err
}
return result, nil
}
func (s *fifoService) ReleaseUsage(ctx context.Context, req StockReleaseRequest) error {
if req.UsableID == 0 || strings.TrimSpace(req.UsableKey.String()) == "" {
return errors.New("usable key and id are required")
}
return s.withTransaction(ctx, req.Tx, func(tx *gorm.DB) error {
cfg, ok := fifo.Usable(req.UsableKey)
if !ok {
return fmt.Errorf("usable %q is not registered", req.UsableKey)
}
ctxRow, err := s.loadUsableContext(ctx, tx, cfg, req.UsableID)
if err != nil {
return err
}
var usageDelta, pendingDelta float64
if ctxRow.UsageQty > 0 {
if _, err := s.releaseUsagePortion(ctx, tx, req.UsableKey, req.UsableID, ctxRow.UsageQty); err != nil {
return err
}
usageDelta -= ctxRow.UsageQty
}
if ctxRow.PendingQty > 0 {
pendingDelta -= ctxRow.PendingQty
}
if err := s.applyUsableDeltas(ctx, tx, cfg, req.UsableID, usageDelta, pendingDelta); err != nil {
return err
}
return s.allocations.ReleaseByUsable(ctx, req.UsableKey.String(), req.UsableID, req.Reason, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
})
})
}
// --- helpers ---
type usableContextRow struct {
ProductWarehouseID uint
UsageQty float64
PendingQty float64
}
func (s *fifoService) loadUsableContext(ctx context.Context, tx *gorm.DB, cfg fifo.UsableConfig, id uint) (*usableContextRow, error) {
var row usableContextRow
query := tx.Table(cfg.Table).
Select(fmt.Sprintf("%s AS product_warehouse_id, COALESCE(%s,0) AS usage_qty, COALESCE(%s,0) AS pending_qty", cfg.Columns.ProductWarehouseID, cfg.Columns.UsageQuantity, cfg.Columns.PendingQuantity)).
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id).
Clauses(clause.Locking{Strength: "UPDATE"})
if cfg.Scope != nil {
query = cfg.Scope(query)
}
if err := query.Take(&row).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("usable record %d not found", id)
}
return nil, err
}
return &row, nil
}
func (s *fifoService) incrementStockableQty(ctx context.Context, tx *gorm.DB, cfg fifo.StockableConfig, id uint, qty float64) error {
column := cfg.Columns.TotalQuantity
query := tx.Table(cfg.Table).
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
if cfg.Scope != nil {
query = cfg.Scope(query)
}
updates := map[string]any{
column: gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", column), qty),
}
if cfg.Columns.TotalUsedQuantity != "" {
updates[cfg.Columns.TotalUsedQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0)", cfg.Columns.TotalUsedQuantity))
}
return query.Updates(updates).Error
}
func (s *fifoService) incrementStockableUsage(ctx context.Context, tx *gorm.DB, cfg fifo.StockableConfig, id uint, qty float64) error {
if qty == 0 {
return nil
}
column := cfg.Columns.TotalUsedQuantity
query := tx.Table(cfg.Table).
Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
if cfg.Scope != nil {
query = cfg.Scope(query)
}
return query.Update(column, gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", column), qty)).Error
}
type allocationOutcome struct {
allocated float64
pending float64
allocations []AllocationDetail
}
type stockLot struct {
StockableKey fifo.StockableKey
RecordID uint
AvailableQty float64
CreatedAt time.Time
}
func (s *fifoService) allocateFromStock(
ctx context.Context,
tx *gorm.DB,
productWarehouseID uint,
usableKey fifo.UsableKey,
usableID uint,
requestQty float64,
) (*allocationOutcome, error) {
lots, err := s.fetchStockLots(ctx, tx, productWarehouseID)
if err != nil {
return nil, err
}
if len(lots) == 0 {
return &allocationOutcome{pending: requestQty}, nil
}
var (
remaining = requestQty
applied float64
allocations []*entities.StockAllocation
allocationSummaries []AllocationDetail
usageAdjustments = make(map[fifo.StockableKey]map[uint]float64)
)
for _, lot := range lots {
if remaining <= 0 {
break
}
if lot.AvailableQty <= 0 {
continue
}
portion := lot.AvailableQty
if portion > remaining {
portion = remaining
}
applied += portion
remaining -= portion
allocationSummaries = append(allocationSummaries, AllocationDetail{
StockableKey: lot.StockableKey,
StockableID: lot.RecordID,
Quantity: portion,
})
allocations = append(allocations, &entities.StockAllocation{
ProductWarehouseId: productWarehouseID,
StockableType: lot.StockableKey.String(),
StockableId: lot.RecordID,
UsableType: usableKey.String(),
UsableId: usableID,
Qty: portion,
Status: entities.StockAllocationStatusActive,
})
if _, ok := usageAdjustments[lot.StockableKey]; !ok {
usageAdjustments[lot.StockableKey] = make(map[uint]float64)
}
usageAdjustments[lot.StockableKey][lot.RecordID] += portion
}
if len(allocations) > 0 {
if err := s.allocations.CreateMany(ctx, allocations, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
return nil, err
}
for key, deltas := range usageAdjustments {
cfg, ok := fifo.Stockable(key)
if !ok {
continue
}
for id, qty := range deltas {
if err := s.incrementStockableUsage(ctx, tx, cfg, id, qty); err != nil {
return nil, err
}
}
}
}
return &allocationOutcome{
allocated: applied,
pending: remaining,
allocations: allocationSummaries,
}, nil
}
func (s *fifoService) fetchStockLots(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]stockLot, error) {
configs := fifo.Stockables()
if len(configs) == 0 {
return nil, nil
}
var lots []stockLot
for key, cfg := range configs {
selectStmt := fmt.Sprintf(
"%s AS id, %s AS available_qty, %s AS created_at",
cfg.Columns.ID,
fmt.Sprintf("%s - COALESCE(%s,0)", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity),
cfg.Columns.CreatedAt,
)
var rows []struct {
ID uint
AvailableQty float64
CreatedAt time.Time
}
query := tx.Table(cfg.Table).
Select(selectStmt).
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
Where(fmt.Sprintf("%s > %s", cfg.Columns.TotalQuantity, cfg.Columns.TotalUsedQuantity))
if cfg.Scope != nil {
query = cfg.Scope(query)
}
for _, order := range s.orderClauses(cfg.OrderBy) {
query = query.Order(order)
}
query = query.Limit(s.maxLotsPerStockable)
if err := query.Find(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
if row.AvailableQty <= 0 {
continue
}
lots = append(lots, stockLot{
StockableKey: key,
RecordID: row.ID,
AvailableQty: row.AvailableQty,
CreatedAt: row.CreatedAt,
})
}
}
if len(lots) == 0 {
return nil, nil
}
sort.SliceStable(lots, func(i, j int) bool {
if lots[i].CreatedAt.Equal(lots[j].CreatedAt) {
return lots[i].RecordID < lots[j].RecordID
}
return lots[i].CreatedAt.Before(lots[j].CreatedAt)
})
return lots, nil
}
func (s *fifoService) applyUsableDeltas(ctx context.Context, tx *gorm.DB, cfg fifo.UsableConfig, id uint, usageDelta, pendingDelta float64) error {
if usageDelta == 0 && pendingDelta == 0 {
return nil
}
updates := map[string]any{}
if usageDelta != 0 {
updates[cfg.Columns.UsageQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", cfg.Columns.UsageQuantity), usageDelta)
}
if pendingDelta != 0 {
updates[cfg.Columns.PendingQuantity] = gorm.Expr(fmt.Sprintf("COALESCE(%s,0) + ?", cfg.Columns.PendingQuantity), pendingDelta)
}
query := tx.Table(cfg.Table).Where(fmt.Sprintf("%s = ?", cfg.Columns.ID), id)
if cfg.Scope != nil {
query = cfg.Scope(query)
}
return query.Updates(updates).Error
}
type pendingCandidate struct {
UsableKey fifo.UsableKey
Config fifo.UsableConfig
UsableID uint
Pending float64
CreatedAt time.Time
}
func (s *fifoService) resolvePendingForWarehouse(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]PendingResolution, error) {
candidates, err := s.fetchPendingCandidates(ctx, tx, productWarehouseID)
if err != nil {
return nil, err
}
if len(candidates) == 0 {
return nil, nil
}
var resolutions []PendingResolution
for _, candidate := range candidates {
if candidate.Pending <= 0 {
continue
}
outcome, err := s.allocateFromStock(ctx, tx, productWarehouseID, candidate.UsableKey, candidate.UsableID, candidate.Pending)
if err != nil {
return nil, err
}
if outcome.allocated <= 0 {
break
}
if err := s.applyUsableDeltas(ctx, tx, candidate.Config, candidate.UsableID, outcome.allocated, -outcome.allocated); err != nil {
return nil, err
}
if err := s.productWarehouseRepo.AdjustQuantities(ctx, map[uint]float64{
productWarehouseID: -outcome.allocated,
}, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
return nil, err
}
resolutions = append(resolutions, PendingResolution{
UsableKey: candidate.UsableKey,
UsableID: candidate.UsableID,
Quantity: outcome.allocated,
})
if outcome.pending > 0 {
// No more stock available for this warehouse at the moment.
break
}
}
return resolutions, nil
}
func (s *fifoService) releaseUsagePortion(
ctx context.Context,
tx *gorm.DB,
usableKey fifo.UsableKey,
usableID uint,
target float64,
) (float64, error) {
if target <= 0 {
return 0, nil
}
allocations, err := s.allocations.FindActiveByUsable(ctx, usableKey.String(), usableID, func(db *gorm.DB) *gorm.DB {
target := s.txOrDB(tx, db)
return target.Clauses(clause.Locking{Strength: "UPDATE"})
})
if err != nil {
return 0, err
}
if len(allocations) == 0 {
return 0, nil
}
var (
remaining = target
totalReleased float64
warehouseAdjustments = make(map[uint]float64)
stockableAdjustments = make(map[fifo.StockableKey]map[uint]float64)
)
now := time.Now()
for i := len(allocations) - 1; i >= 0 && remaining > 0; i-- {
allocation := allocations[i]
releaseAmt := allocation.Qty
if releaseAmt > remaining {
releaseAmt = remaining
}
remaining -= releaseAmt
totalReleased += releaseAmt
warehouseAdjustments[allocation.ProductWarehouseId] += releaseAmt
key := fifo.StockableKey(allocation.StockableType)
if _, ok := stockableAdjustments[key]; !ok {
stockableAdjustments[key] = make(map[uint]float64)
}
stockableAdjustments[key][allocation.StockableId] += releaseAmt
if releaseAmt == allocation.Qty {
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
"status": entities.StockAllocationStatusReleased,
"released_at": now,
}, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
return 0, err
}
} else {
if err := s.allocations.PatchOne(ctx, allocation.Id, map[string]any{
"quantity": allocation.Qty - releaseAmt,
}, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
return 0, err
}
}
}
if totalReleased == 0 {
return 0, nil
}
for key, deltas := range stockableAdjustments {
cfg, ok := fifo.Stockable(key)
if !ok {
continue
}
for id, qty := range deltas {
if err := s.incrementStockableUsage(ctx, tx, cfg, id, -qty); err != nil {
return 0, err
}
}
}
if len(warehouseAdjustments) > 0 {
if err := s.productWarehouseRepo.AdjustQuantities(ctx, warehouseAdjustments, func(db *gorm.DB) *gorm.DB {
return s.txOrDB(tx, db)
}); err != nil {
return 0, err
}
for warehouseID := range warehouseAdjustments {
if _, err := s.resolvePendingForWarehouse(ctx, tx, warehouseID); err != nil {
return 0, err
}
}
}
return totalReleased, nil
}
func (s *fifoService) fetchPendingCandidates(ctx context.Context, tx *gorm.DB, productWarehouseID uint) ([]pendingCandidate, error) {
configs := fifo.Usables()
if len(configs) == 0 {
return nil, nil
}
var candidates []pendingCandidate
for key, cfg := range configs {
selectStmt := fmt.Sprintf(
"%s AS id, %s AS pending_qty, %s AS created_at",
cfg.Columns.ID,
cfg.Columns.PendingQuantity,
cfg.Columns.CreatedAt,
)
var rows []struct {
ID uint
Pending float64
CreatedAt time.Time
}
query := tx.Table(cfg.Table).
Select(selectStmt).
Where(fmt.Sprintf("%s = ?", cfg.Columns.ProductWarehouseID), productWarehouseID).
Where(fmt.Sprintf("%s > 0", cfg.Columns.PendingQuantity)).
Limit(s.pendingBatchPerUsable)
if cfg.Scope != nil {
query = cfg.Scope(query)
}
for _, order := range s.orderClauses(cfg.OrderBy) {
query = query.Order(order)
}
if err := query.Find(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
if row.Pending <= 0 {
continue
}
candidates = append(candidates, pendingCandidate{
UsableKey: key,
Config: cfg,
UsableID: row.ID,
Pending: row.Pending,
CreatedAt: row.CreatedAt,
})
}
}
if len(candidates) == 0 {
return nil, nil
}
sort.SliceStable(candidates, func(i, j int) bool {
if candidates[i].CreatedAt.Equal(candidates[j].CreatedAt) {
return candidates[i].UsableID < candidates[j].UsableID
}
return candidates[i].CreatedAt.Before(candidates[j].CreatedAt)
})
return candidates, nil
}
func (s *fifoService) orderClauses(custom []string) []string {
if len(custom) > 0 {
return custom
}
return s.defaultOrderBy
}
@@ -2,42 +2,42 @@
CREATE TABLE users ( CREATE TABLE users (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
id_user BIGINT NOT NULL, id_user BIGINT NOT NULL,
name VARCHAR(50) NOT NULL, name VARCHAR NOT NULL,
email VARCHAR(50) NOT NULL, email VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ deleted_at TIMESTAMPTZ
); );
CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) CREATE UNIQUE INDEX users_id_user_unique ON users (id_user) WHERE deleted_at IS NULL;
WHERE
deleted_at IS NULL;
CREATE UNIQUE INDEX users_email_unique ON users (email) CREATE UNIQUE INDEX users_email_unique ON users (email) WHERE deleted_at IS NULL;
WHERE
deleted_at IS NULL;
-- FLAGS -- FLAGS
CREATE TABLE flags ( CREATE TABLE flags (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL, name VARCHAR NOT NULL,
flagable_id BIGINT NOT NULL, flagable_id BIGINT NOT NULL,
flagable_type VARCHAR(50) NOT NULL, flagable_type VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW () updated_at TIMESTAMPTZ DEFAULT NOW()
); );
CREATE UNIQUE INDEX flags_unique_flagable ON flags (name, flagable_id, flagable_type); CREATE UNIQUE INDEX flags_unique_flagable ON flags (
name,
flagable_id,
flagable_type
);
CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id); CREATE INDEX flags_flagable_lookup ON flags (flagable_type, flagable_id);
-- PRODUCT CATEGORIES -- PRODUCT CATEGORIES
CREATE TABLE product_categories ( CREATE TABLE product_categories (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL, name VARCHAR NOT NULL,
code VARCHAR(10) NOT NULL, code VARCHAR(10) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -53,9 +53,9 @@ WHERE
-- UOM -- UOM
CREATE TABLE uoms ( CREATE TABLE uoms (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL, name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -67,12 +67,12 @@ WHERE
-- BANKS -- BANKS
CREATE TABLE banks ( CREATE TABLE banks (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL, name VARCHAR NOT NULL,
alias VARCHAR(5) NOT NULL, alias VARCHAR(5) NOT NULL,
owner VARCHAR(50), owner VARCHAR,
account_number VARCHAR(50) NOT NULL, account_number VARCHAR(50) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -84,9 +84,9 @@ WHERE
-- AREAS -- AREAS
CREATE TABLE areas ( CREATE TABLE areas (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL, name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -98,11 +98,11 @@ WHERE
-- LOCATIONS -- LOCATIONS
CREATE TABLE locations ( CREATE TABLE locations (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL, name VARCHAR NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE, area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -114,11 +114,11 @@ WHERE
-- KANDANG -- KANDANG
CREATE TABLE kandangs ( CREATE TABLE kandangs (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL, name VARCHAR NOT NULL,
location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE, location_id BIGINT NOT NULL REFERENCES locations (id) ON DELETE RESTRICT ON UPDATE CASCADE,
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE, pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -130,13 +130,13 @@ WHERE
-- WAREHOUSES -- WAREHOUSES
CREATE TABLE warehouses ( CREATE TABLE warehouses (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL, name VARCHAR NOT NULL,
type VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL,
area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE, area_id BIGINT NOT NULL REFERENCES areas (id) ON DELETE RESTRICT ON UPDATE CASCADE,
location_id BIGINT REFERENCES locations (id) ON DELETE SET NULL ON UPDATE CASCADE, location_id BIGINT REFERENCES locations (id) ON DELETE SET NULL ON UPDATE CASCADE,
kandang_id BIGINT REFERENCES kandangs (id) ON DELETE SET NULL ON UPDATE CASCADE, kandang_id BIGINT REFERENCES kandangs (id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -148,16 +148,16 @@ WHERE
-- CUSTOMERS -- CUSTOMERS
CREATE TABLE customers ( CREATE TABLE customers (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL, name VARCHAR NOT NULL,
pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE, pic_id BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
type VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
phone VARCHAR(20) NOT NULL, phone VARCHAR(20) NOT NULL,
email VARCHAR(50) NOT NULL, email VARCHAR NOT NULL,
account_number VARCHAR(50) NOT NULL, account_number VARCHAR(50) NOT NULL,
balance NUMERIC(15, 3) DEFAULT 0, balance NUMERIC(15, 3) DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -169,10 +169,10 @@ WHERE
-- NONSTOCK -- NONSTOCK
CREATE TABLE nonstocks ( CREATE TABLE nonstocks (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL, name VARCHAR NOT NULL,
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE, uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -184,9 +184,9 @@ WHERE
-- FCR -- FCR
CREATE TABLE fcrs ( CREATE TABLE fcrs (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL, name VARCHAR NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -201,29 +201,29 @@ CREATE TABLE fcr_standards (
weight NUMERIC(15, 3) NOT NULL, weight NUMERIC(15, 3) NOT NULL,
fcr_number NUMERIC(15, 3) NOT NULL, fcr_number NUMERIC(15, 3) NOT NULL,
mortality NUMERIC(15, 3) NOT NULL, mortality NUMERIC(15, 3) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ deleted_at TIMESTAMPTZ
); );
-- SUPPLIERS -- SUPPLIERS
CREATE TABLE suppliers ( CREATE TABLE suppliers (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL, name VARCHAR NOT NULL,
alias VARCHAR(5) NOT NULL, alias VARCHAR(5) NOT NULL,
pic VARCHAR(50) NOT NULL, pic VARCHAR NOT NULL,
type VARCHAR(50) NOT NULL, type VARCHAR(50) NOT NULL,
category VARCHAR(20) NOT NULL, category VARCHAR(20) NOT NULL,
hatchery VARCHAR(50), hatchery VARCHAR,
phone VARCHAR(20) NOT NULL, phone VARCHAR(20) NOT NULL,
email VARCHAR(50) NOT NULL, email VARCHAR NOT NULL,
address TEXT NOT NULL, address TEXT NOT NULL,
npwp VARCHAR(50), npwp VARCHAR(50),
account_number VARCHAR(50), account_number VARCHAR(50),
balance NUMERIC(15, 3) DEFAULT 0, balance NUMERIC(15, 3) DEFAULT 0,
due_date INT NOT NULL, due_date INT NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -235,15 +235,15 @@ WHERE
CREATE TABLE nonstock_suppliers ( CREATE TABLE nonstock_suppliers (
nonstock_id BIGINT NOT NULL REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE, nonstock_id BIGINT NOT NULL REFERENCES nonstocks (id) ON DELETE CASCADE ON UPDATE CASCADE,
supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE, supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (nonstock_id, supplier_id) PRIMARY KEY (nonstock_id, supplier_id)
); );
-- PRODUCTS -- PRODUCTS
CREATE TABLE products ( CREATE TABLE products (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL, name VARCHAR NOT NULL,
brand VARCHAR(50) NOT NULL, brand VARCHAR NOT NULL,
sku VARCHAR(100), sku VARCHAR(100),
uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE, uom_id BIGINT NOT NULL REFERENCES uoms (id) ON DELETE RESTRICT ON UPDATE CASCADE,
product_category_id BIGINT NOT NULL REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE, product_category_id BIGINT NOT NULL REFERENCES product_categories (id) ON DELETE RESTRICT ON UPDATE CASCADE,
@@ -251,8 +251,8 @@ CREATE TABLE products (
selling_price NUMERIC(15, 3), selling_price NUMERIC(15, 3),
tax NUMERIC(15, 3), tax NUMERIC(15, 3),
expiry_period INT, expiry_period INT,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -268,15 +268,15 @@ WHERE
CREATE TABLE product_suppliers ( CREATE TABLE product_suppliers (
product_id BIGINT NOT NULL REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE, product_id BIGINT NOT NULL REFERENCES products (id) ON DELETE CASCADE ON UPDATE CASCADE,
supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE, supplier_id BIGINT NOT NULL REFERENCES suppliers (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
PRIMARY KEY (product_id, supplier_id) PRIMARY KEY (product_id, supplier_id)
); );
-- PROJECTS -- PROJECTS
CREATE TABLE projects ( CREATE TABLE projects (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ,
created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE created_by BIGINT REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE
); );
@@ -288,8 +288,8 @@ CREATE TABLE product_warehouses (
warehouse_id BIGINT NOT NULL REFERENCES warehouses (id), warehouse_id BIGINT NOT NULL REFERENCES warehouses (id),
quantity INTEGER NOT NULL DEFAULT 0, quantity INTEGER NOT NULL DEFAULT 0,
created_by BIGINT NOT NULL REFERENCES users (id), created_by BIGINT NOT NULL REFERENCES users (id),
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ deleted_at TIMESTAMPTZ
); );
@@ -316,8 +316,8 @@ CREATE TABLE stock_logs (
note TEXT, note TEXT,
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE, product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses (id) ON DELETE CASCADE ON UPDATE CASCADE,
created_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE, created_by BIGINT NOT NULL REFERENCES users (id) ON DELETE SET NULL ON UPDATE CASCADE,
created_at TIMESTAMPTZ DEFAULT NOW (), created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW (), updated_at TIMESTAMPTZ DEFAULT NOW(),
deleted_at TIMESTAMPTZ deleted_at TIMESTAMPTZ
); );
@@ -1,7 +0,0 @@
DROP INDEX IF EXISTS stock_allocations_released_at_idx;
DROP INDEX IF EXISTS stock_allocations_status_idx;
DROP INDEX IF EXISTS stock_allocations_usage_lookup;
DROP INDEX IF EXISTS stock_allocations_lookup;
DROP INDEX IF EXISTS stock_allocations_product_warehouse_id_idx;
DROP TABLE IF EXISTS stock_allocations;
@@ -1,30 +0,0 @@
CREATE TABLE IF NOT EXISTS stock_allocations (
id BIGSERIAL PRIMARY KEY,
product_warehouse_id BIGINT NOT NULL REFERENCES product_warehouses(id),
stockable_type VARCHAR(100) NOT NULL,
stockable_id BIGINT NOT NULL,
usable_type VARCHAR(100) NOT NULL,
usable_id BIGINT NOT NULL,
qty NUMERIC(15,3) NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'ACTIVE',
note TEXT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
released_at TIMESTAMPTZ NULL,
deleted_at TIMESTAMPTZ NULL
);
CREATE INDEX IF NOT EXISTS stock_allocations_product_warehouse_id_idx
ON stock_allocations (product_warehouse_id);
CREATE INDEX IF NOT EXISTS stock_allocations_lookup
ON stock_allocations (stockable_type, stockable_id);
CREATE INDEX IF NOT EXISTS stock_allocations_usage_lookup
ON stock_allocations (usable_type, usable_id);
CREATE INDEX IF NOT EXISTS stock_allocations_status_idx
ON stock_allocations (status);
CREATE INDEX IF NOT EXISTS stock_allocations_released_at_idx
ON stock_allocations (released_at);
@@ -1,7 +1,7 @@
CREATE TABLE expenses ( CREATE TABLE expenses (
id BIGSERIAL PRIMARY KEY, id BIGSERIAL PRIMARY KEY,
reference_number VARCHAR(50) UNIQUE NOT NULL, reference_number VARCHAR(50) UNIQUE NOT NULL,
supplier_id BIGINT NOT NULL, supplier_id BIGINT NULL,
category VARCHAR(50) NOT NULL CHECK ( category VARCHAR(50) NOT NULL CHECK (
category IN ('BOP', 'NON-BOP') category IN ('BOP', 'NON-BOP')
), ),
@@ -1,44 +0,0 @@
-- ============================
-- EXPENSES
-- ============================
ALTER TABLE expenses DROP COLUMN IF EXISTS grand_total;
ALTER TABLE expenses RENAME COLUMN note TO notes;
ALTER TABLE expenses RENAME COLUMN expense_date TO transaction_date;
-- ============================
-- EXPENSE_REALIZATIONS
-- ============================
ALTER TABLE expense_realizations
RENAME COLUMN realization_qty TO qty;
ALTER TABLE expense_realizations
RENAME COLUMN realization_unit_price TO price;
ALTER TABLE expense_realizations RENAME COLUMN note TO notes;
ALTER TABLE expense_realizations
DROP COLUMN IF EXISTS realization_total_price;
ALTER TABLE expense_realizations
DROP COLUMN IF EXISTS realization_date;
ALTER TABLE expense_realizations DROP COLUMN IF EXISTS created_by;
ALTER TABLE expense_realizations
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
-- ============================
-- EXPENSE_NONSTOCKS
-- ============================
ALTER TABLE expense_nonstocks RENAME COLUMN note TO notes;
ALTER TABLE expense_nonstocks DROP COLUMN IF EXISTS total_price;
ALTER TABLE expense_nonstocks RENAME COLUMN unit_price TO price;
ALTER TABLE expense_nonstocks DROP COLUMN IF EXISTS created_by;
ALTER TABLE expense_nonstocks
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW();
@@ -1,2 +0,0 @@
DROP Table IF EXISTS project_budgets;
@@ -1,31 +0,0 @@
CREATE TABLE project_budgets (
id BIGSERIAL PRIMARY KEY,
project_flock_id BIGINT NOT NULL,
nonstock_id BIGINT NOT NULL,
qty NUMERIC(15, 3) NOT NULL,
price NUMERIC(15, 3) NOT NULL,
created_at TIMESTAMPTZ DEFAULT now()
);
-- Tambahkan Foreign Key ke project_flocks
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'project_flocks') THEN
ALTER TABLE project_budgets
ADD CONSTRAINT fk_project_budgets_project_flock_id
FOREIGN KEY (project_flock_id) REFERENCES project_flocks(id);
END IF;
END $$;
-- Tambahkan Foreign Key ke nonstocks
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_tables WHERE tablename = 'nonstocks') THEN
ALTER TABLE project_budgets
ADD CONSTRAINT fk_project_budgets_nonstock_id
FOREIGN KEY (nonstock_id) REFERENCES nonstocks(id);
END IF;
END $$;
-- Index
CREATE INDEX idx_project_budgets_project_flock_id ON project_budgets (project_flock_id);
CREATE INDEX idx_project_budgets_nonstock_id ON project_budgets (nonstock_id);
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Area struct { type Area struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null;uniqueIndex:areas_name_unique,where:deleted_at IS NULL"` Name string `gorm:"not null;uniqueIndex:areas_name_unique,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
+2 -2
View File
@@ -8,9 +8,9 @@ import (
type Bank struct { type Bank struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null;uniqueIndex:banks_name_unique,where:deleted_at IS NULL"` Name string `gorm:"not null;uniqueIndex:banks_name_unique,where:deleted_at IS NULL"`
Alias string `gorm:"not null;size:5"` Alias string `gorm:"not null;size:5"`
Owner *string `gorm:"type:varchar(50)"` Owner *string `gorm:""`
AccountNumber string `gorm:"not null;size:50"` AccountNumber string `gorm:"not null;size:50"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
+2 -2
View File
@@ -8,12 +8,12 @@ import (
type Customer struct { type Customer struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null;uniqueIndex:customers_name_unique,where:deleted_at IS NULL"` Name string `gorm:"not null;uniqueIndex:customers_name_unique,where:deleted_at IS NULL"`
PicId uint `gorm:"not null"` PicId uint `gorm:"not null"`
Type string `gorm:"not null;size:50"` Type string `gorm:"not null;size:50"`
Address string `gorm:"not null"` Address string `gorm:"not null"`
Phone string `gorm:"not null;size:20"` Phone string `gorm:"not null;size:20"`
Email string `gorm:"type:varchar(50);not null"` Email string `gorm:"not null"`
AccountNumber string `gorm:"not null;size:50"` AccountNumber string `gorm:"not null;size:50"`
Balance float64 `gorm:"default:0"` Balance float64 `gorm:"default:0"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
+7 -5
View File
@@ -13,16 +13,18 @@ type Expense struct {
SupplierId uint64 `gorm:""` SupplierId uint64 `gorm:""`
Category string `gorm:"type:varchar(50);not null"` Category string `gorm:"type:varchar(50);not null"`
PoNumber string `gorm:"type:varchar(50)"` PoNumber string `gorm:"type:varchar(50)"`
DocumentPath sql.NullString `gorm:"type:json"` DocumentPath sql.NullString `gorm:"type:json"` // Dokumen pengajuan
RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"` RealizationDocumentPath sql.NullString `gorm:"type:json;column:realization_document_path"` // Dokumen realisasi
RealizationDate time.Time `gorm:"type:date;column:realization_date"` RealizationDate time.Time `gorm:"type:date;column:realization_date"` // Tanggal realisasi
TransactionDate time.Time `gorm:"type:date;not null"` ExpenseDate time.Time `gorm:"type:date;not null"`
Notes string `gorm:"type:text;column:notes"` GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
Note string `gorm:"type:text"`
CreatedBy uint64 `gorm:""` CreatedBy uint64 `gorm:""`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
// Relations
Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"` Supplier *Supplier `gorm:"foreignKey:SupplierId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"` CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"` Nonstocks []ExpenseNonstock `gorm:"foreignKey:ExpenseId;references:Id"`
+11 -14
View File
@@ -1,23 +1,20 @@
package entities package entities
import (
"time"
)
type ExpenseNonstock struct { type ExpenseNonstock struct {
Id uint64 `gorm:"primaryKey;autoIncrement"` Id uint64 `gorm:"primaryKey;autoIncrement"`
ExpenseId *uint64 `gorm:""` ExpenseId *uint64 `gorm:""`
ProjectFlockKandangId *uint64 `gorm:""` ProjectFlockKandangId *uint64 `gorm:""`
KandangId *uint64 `gorm:""` KandangId *uint64 `gorm:""`
NonstockId *uint64 `gorm:""` NonstockId *uint64 `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null"` Qty float64 `gorm:"type:numeric(15,3);not null"`
Price float64 `gorm:"type:numeric(15,3);not null;column:price"` UnitPrice float64 `gorm:"type:numeric(15,3);not null"`
Notes string `gorm:"type:text;column:notes"` TotalPrice float64 `gorm:"type:numeric(15,3);not null"`
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"` Note string `gorm:"type:text"`
// Relations
Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"` Expense *Expense `gorm:"foreignKey:ExpenseId;references:Id"`
ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"` ProjectFlockKandang *ProjectFlockKandang `gorm:"foreignKey:ProjectFlockKandangId;references:Id"`
Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"` Kandang *Kandang `gorm:"foreignKey:KandangId;references:Id"`
Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"` Nonstock *Nonstock `gorm:"foreignKey:NonstockId;references:Id"`
Realization *ExpenseRealization `gorm:"foreignKey:Id;references:ExpenseNonstockId"` Realization *ExpenseRealization `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
} }
+10 -6
View File
@@ -5,12 +5,16 @@ import (
) )
type ExpenseRealization struct { type ExpenseRealization struct {
Id uint64 `gorm:"primaryKey;autoIncrement"` Id uint64 `gorm:"primaryKey;autoIncrement"`
ExpenseNonstockId *uint64 `gorm:""` ExpenseNonstockId *uint64 `gorm:""`
Qty float64 `gorm:"type:numeric(15,3);not null;"` RealizationQty float64 `gorm:"type:numeric(15,3);not null"`
Price float64 `gorm:"type:numeric(15,3);not null;"` RealizationUnitPrice float64 `gorm:"type:numeric(15,3);not null"`
Notes string `gorm:"type:text;"` RealizationTotalPrice float64 `gorm:"type:numeric(15,3);not null"`
CreatedAt time.Time `gorm:"type:timestamptz;default:CURRENT_TIMESTAMP"` RealizationDate time.Time `gorm:"type:date;not null"`
Note *string `gorm:"type:text"`
CreatedBy *uint64 `gorm:""`
// Relations
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"` ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
CreatedUser *User `gorm:"foreignKey:CreatedBy;references:Id"`
} }
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Fcr struct { type Fcr struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"` Name string `gorm:"not null;uniqueIndex:idx_suppliers_name,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
+1 -1
View File
@@ -9,7 +9,7 @@ const (
type Flag struct { type Flag struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);size:50;not null;uniqueIndex:flags_unique_flagable"` Name string `gorm:"not null;uniqueIndex:flags_unique_flagable"`
FlagableID uint `gorm:"not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:2"` FlagableID uint `gorm:"not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:2"`
FlagableType string `gorm:"size:50;not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:1"` FlagableType string `gorm:"size:50;not null;uniqueIndex:flags_unique_flagable;index:flags_flagable_lookup,priority:1"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Kandang struct { type Kandang struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"` Name string `gorm:"not null;uniqueIndex:kandangs_name_unique,where:deleted_at IS NULL"`
Status string `gorm:"type:varchar(50);not null"` Status string `gorm:"type:varchar(50);not null"`
LocationId uint `gorm:"not null"` LocationId uint `gorm:"not null"`
Capacity float64 `gorm:"not null"` Capacity float64 `gorm:"not null"`
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Location struct { type Location struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null;uniqueIndex:locations_name_unique,where:deleted_at IS NULL"` Name string `gorm:"not null;uniqueIndex:locations_name_unique,where:deleted_at IS NULL"`
Address string `gorm:"not null"` Address string `gorm:"not null"`
AreaId uint `gorm:"not null"` AreaId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Nonstock struct { type Nonstock struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null;uniqueIndex:nonstocks_name_unique,where:deleted_at IS NULL"` Name string `gorm:"not null;uniqueIndex:nonstocks_name_unique,where:deleted_at IS NULL"`
UomId uint `gorm:"not null"` UomId uint `gorm:"not null"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type ProductCategory struct { type ProductCategory struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null;uniqueIndex:product_categories_name_unique,where:deleted_at IS NULL"` Name string `gorm:"not null;uniqueIndex:product_categories_name_unique,where:deleted_at IS NULL"`
Code string `gorm:"not null;size:10;uniqueIndex:product_categories_code_unique,where:deleted_at IS NULL"` Code string `gorm:"not null;size:10;uniqueIndex:product_categories_code_unique,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
+2 -2
View File
@@ -8,8 +8,8 @@ import (
type Product struct { type Product struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null;uniqueIndex:products_name_unique,where:deleted_at IS NULL"` Name string `gorm:"not null;uniqueIndex:products_name_unique,where:deleted_at IS NULL"`
Brand string `gorm:"type:varchar(50);not null"` Brand string `gorm:"not null"`
Sku *string `gorm:"size:100;uniqueIndex:products_sku_unique,where:deleted_at IS NULL"` Sku *string `gorm:"size:100;uniqueIndex:products_sku_unique,where:deleted_at IS NULL"`
UomId uint `gorm:"not null"` UomId uint `gorm:"not null"`
ProductCategoryId uint `gorm:"not null"` ProductCategoryId uint `gorm:"not null"`
-15
View File
@@ -1,15 +0,0 @@
package entities
import (
"time"
)
type ProjectBudget struct {
Id uint `gorm:"primaryKey"`
Qty float64 `gorm:"type:numeric(15,3);not null"`
Price float64 `gorm:"type:numeric(15,3);not null"`
CreatedAt time.Time `gorm:"autoCreateTime"`
Nonstock *Nonstock `gorm:"foreignKey:Id;references:Id"`
ProjectFlock *ProjectFlock `gorm:"foreignKey:Id;references:Id"`
}
+3 -3
View File
@@ -5,11 +5,11 @@ import (
) )
type Purchase struct { type Purchase struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint64 `gorm:"primaryKey;autoIncrement"`
PrNumber string `gorm:"not null"` PrNumber string `gorm:"not null"`
PoNumber *string PoNumber *string
PoDate *time.Time PoDate *time.Time
SupplierId uint `gorm:"not null"` SupplierId uint64 `gorm:"not null"`
CreditTerm *int CreditTerm *int
DueDate *time.Time DueDate *time.Time
GrandTotal float64 `gorm:"type:numeric(15,3);default:0"` GrandTotal float64 `gorm:"type:numeric(15,3);default:0"`
@@ -17,7 +17,7 @@ type Purchase struct {
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt *time.Time `gorm:"index"` DeletedAt *time.Time `gorm:"index"`
CreatedBy uint `gorm:"not null"` CreatedBy uint64 `gorm:"not null"`
// Relations // Relations
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"` Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
+5 -5
View File
@@ -5,11 +5,11 @@ import (
) )
type PurchaseItem struct { type PurchaseItem struct {
Id uint `gorm:"primaryKey;autoIncrement"` Id uint64 `gorm:"primaryKey;autoIncrement"`
PurchaseId uint `gorm:"not null"` PurchaseId uint64 `gorm:"not null"`
ProductId uint `gorm:"not null"` ProductId uint64 `gorm:"not null"`
WarehouseId uint `gorm:"not null"` WarehouseId uint64 `gorm:"not null"`
ProductWarehouseId *uint ProductWarehouseId *uint64
ReceivedDate *time.Time ReceivedDate *time.Time
TravelNumber *string TravelNumber *string
TravelNumberDocs *string TravelNumberDocs *string
-33
View File
@@ -1,33 +0,0 @@
package entities
import (
"time"
"gorm.io/gorm"
)
const (
StockAllocationStatusPending = "PENDING"
StockAllocationStatusActive = "ACTIVE"
StockAllocationStatusReleased = "RELEASED"
)
// StockAllocation links a usable record (consumption) with an incoming stock record.
// The combination lets us trace FIFO deductions while keeping each module focused on its own fields.
type StockAllocation struct {
Id uint `gorm:"primaryKey"`
ProductWarehouseId uint `gorm:"not null;index"`
StockableType string `gorm:"size:100;not null;index:stock_allocations_lookup,priority:1"`
StockableId uint `gorm:"not null;index:stock_allocations_lookup,priority:2"`
UsableType string `gorm:"size:100;not null;index:stock_allocations_usage_lookup,priority:1"`
UsableId uint `gorm:"not null;index:stock_allocations_usage_lookup,priority:2"`
Qty float64 `gorm:"type:numeric(15,3);not null"`
Status string `gorm:"size:20;not null;default:ACTIVE"`
Note *string `gorm:"type:text"`
CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"`
ReleasedAt *time.Time `gorm:"index"`
DeletedAt gorm.DeletedAt `gorm:"index"`
ProductWarehouse *ProductWarehouse `gorm:"foreignKey:ProductWarehouseId;references:Id"`
}
+4 -4
View File
@@ -8,14 +8,14 @@ import (
type Supplier struct { type Supplier struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null;uniqueIndex:suppliers_name_unique,where:deleted_at IS NULL"` Name string `gorm:"not null;uniqueIndex:suppliers_name_unique,where:deleted_at IS NULL"`
Alias string `gorm:"not null;size:5"` Alias string `gorm:"not null;size:5"`
Pic string `gorm:"type:varchar(50);not null"` Pic string `gorm:"not null"`
Type string `gorm:"not null;size:50"` Type string `gorm:"not null;size:50"`
Category string `gorm:"not null;size:20"` Category string `gorm:"not null;size:20"`
Hatchery *string `gorm:"type:varchar(50)"` Hatchery *string `gorm:"size:255"`
Phone string `gorm:"not null;size:20"` Phone string `gorm:"not null;size:20"`
Email string `gorm:"type:varchar(50);not null"` Email string `gorm:"not null"`
Address string `gorm:"not null"` Address string `gorm:"not null"`
Npwp *string `gorm:"size:50"` Npwp *string `gorm:"size:50"`
AccountNumber *string `gorm:"size:50"` AccountNumber *string `gorm:"size:50"`
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Uom struct { type Uom struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null;uniqueIndex:uoms_name_unique,where:deleted_at IS NULL"` Name string `gorm:"not null;uniqueIndex:uoms_name_unique,where:deleted_at IS NULL"`
CreatedBy uint `gorm:"not null"` CreatedBy uint `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
+2 -2
View File
@@ -9,8 +9,8 @@ import (
type User struct { type User struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
IdUser int64 `gorm:"uniqueIndex"` IdUser int64 `gorm:"uniqueIndex"`
Email string `gorm:"type:varchar(50);uniqueIndex"` Email string `gorm:"uniqueIndex"`
Name string `gorm:"type:varchar(50);not null"` Name string `gorm:"not null"`
CreatedAt time.Time `gorm:"autoCreateTime"` CreatedAt time.Time `gorm:"autoCreateTime"`
UpdatedAt time.Time `gorm:"autoUpdateTime"` UpdatedAt time.Time `gorm:"autoUpdateTime"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
+1 -1
View File
@@ -8,7 +8,7 @@ import (
type Warehouse struct { type Warehouse struct {
Id uint `gorm:"primaryKey"` Id uint `gorm:"primaryKey"`
Name string `gorm:"type:varchar(50);not null"` Name string `gorm:"not null"`
Type string `gorm:"not null"` Type string `gorm:"not null"`
AreaId uint `gorm:"not null"` AreaId uint `gorm:"not null"`
LocationId *uint LocationId *uint
-8
View File
@@ -105,14 +105,6 @@ func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) {
return nil, false return nil, false
} }
func ActorIDFromContext(c *fiber.Ctx) (uint, error) {
user, ok := AuthenticatedUser(c)
if !ok || user == nil || user.Id == 0 {
return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
}
return user.Id, nil
}
// AuthDetails returns the full authentication context (token, claims, user). // AuthDetails returns the full authentication context (token, claims, user).
func AuthDetails(c *fiber.Ctx) (*AuthContext, bool) { func AuthDetails(c *fiber.Ctx) (*AuthContext, bool) {
value := c.Locals(authContextLocalsKey) value := c.Locals(authContextLocalsKey)
+2 -2
View File
@@ -3,7 +3,7 @@ package approvals
import ( import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware" // m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service" common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/approvals/controllers"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -12,8 +12,8 @@ import (
func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalService) { func ApprovalRoutes(v1 fiber.Router, u user.UserService, s common.ApprovalService) {
_ = u _ = u
ctrl := controller.NewApprovalController(s) ctrl := controller.NewApprovalController(s)
route := v1.Group("/approvals") route := v1.Group("/approvals")
route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll) route.Get("/", ctrl.GetAll)
} }
@@ -1,76 +0,0 @@
package controller
import (
"math"
"strconv"
"gitlab.com/mbugroup/lti-api.git/internal/modules/closings/dto"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/response"
"github.com/gofiber/fiber/v2"
)
type ClosingController struct {
ClosingService service.ClosingService
}
func NewClosingController(closingService service.ClosingService) *ClosingController {
return &ClosingController{
ClosingService: closingService,
}
}
func (u *ClosingController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{
Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10),
Search: c.Query("search", ""),
}
if query.Page < 1 || query.Limit < 1 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0")
}
result, totalResults, err := u.ClosingService.GetAll(c, query)
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.ClosingListDTO]{
Code: fiber.StatusOK,
Status: "success",
Message: "Get all closings successfully",
Meta: response.Meta{
Page: query.Page,
Limit: query.Limit,
TotalPages: int64(math.Ceil(float64(totalResults) / float64(query.Limit))),
TotalResults: totalResults,
},
Data: dto.ToClosingListDTOs(result),
})
}
func (u *ClosingController) GetOne(c *fiber.Ctx) error {
param := c.Params("id")
id, err := strconv.Atoi(param)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
}
result, err := u.ClosingService.GetOne(c, uint(id))
if err != nil {
return err
}
return c.Status(fiber.StatusOK).
JSON(response.Success{
Code: fiber.StatusOK,
Status: "success",
Message: "Get closing successfully",
Data: dto.ToClosingListDTO(*result),
})
}
@@ -1,64 +0,0 @@
package dto
import (
"time"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
)
// === DTO Structs ===
type ClosingRelationDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
}
type ClosingListDTO struct {
Id uint `json:"id"`
Name string `json:"name"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
type ClosingDetailDTO struct {
ClosingListDTO
}
// === Mapper Functions ===
func ToClosingRelationDTO(e entity.ProjectFlock) ClosingRelationDTO {
return ClosingRelationDTO{
Id: e.Id,
}
}
func ToClosingListDTO(e entity.ProjectFlock) ClosingListDTO {
var createdUser *userDTO.UserRelationDTO
if e.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(e.CreatedUser)
createdUser = &mapped
}
return ClosingListDTO{
Id: e.Id,
CreatedAt: e.CreatedAt,
UpdatedAt: e.UpdatedAt,
CreatedUser: createdUser,
}
}
func ToClosingListDTOs(e []entity.ProjectFlock) []ClosingListDTO {
result := make([]ClosingListDTO, len(e))
for i, r := range e {
result[i] = ToClosingListDTO(r)
}
return result
}
func ToClosingDetailDTO(e entity.ProjectFlock) ClosingDetailDTO {
return ClosingDetailDTO{
ClosingListDTO: ToClosingListDTO(e),
}
}
-26
View File
@@ -1,26 +0,0 @@
package closings
import (
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"gorm.io/gorm"
rClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
sClosing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
)
type ClosingModule struct{}
func (ClosingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
closingRepo := rClosing.NewClosingRepository(db)
userRepo := rUser.NewUserRepository(db)
closingService := sClosing.NewClosingService(closingRepo, validate)
userService := sUser.NewUserService(userRepo, validate)
ClosingRoutes(router, userService, closingService)
}
@@ -1,21 +0,0 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ClosingRepository interface {
repository.BaseRepository[entity.ProjectFlock]
}
type ClosingRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProjectFlock]
}
func NewClosingRepository(db *gorm.DB) ClosingRepository {
return &ClosingRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectFlock](db),
}
}
-25
View File
@@ -1,25 +0,0 @@
package closings
import (
// m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/controllers"
closing "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/services"
user "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
"github.com/gofiber/fiber/v2"
)
func ClosingRoutes(v1 fiber.Router, u user.UserService, s closing.ClosingService) {
ctrl := controller.NewClosingController(s)
route := v1.Group("/closings")
// route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne)
// route.Patch("/:id", m.Auth(u), ctrl.UpdateOne)
// route.Delete("/:id", m.Auth(u), ctrl.DeleteOne)
route.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne)
}
@@ -1,72 +0,0 @@
package service
import (
"errors"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/closings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm"
)
type ClosingService interface {
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error)
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, error)
}
type closingService struct {
Log *logrus.Logger
Validate *validator.Validate
Repository repository.ClosingRepository
}
func NewClosingService(repo repository.ClosingRepository, validate *validator.Validate) ClosingService {
return &closingService{
Log: utils.Log,
Validate: validate,
Repository: repo,
}
}
func (s closingService) withRelations(db *gorm.DB) *gorm.DB {
return db.Preload("CreatedUser")
}
func (s closingService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
if err := s.Validate.Struct(params); err != nil {
return nil, 0, err
}
offset := (params.Page - 1) * params.Limit
closings, total, err := s.Repository.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
db = s.withRelations(db)
if params.Search != "" {
return db.Where("name LIKE ?", "%"+params.Search+"%")
}
return db.Order("created_at DESC").Order("updated_at DESC")
})
if err != nil {
s.Log.Errorf("Failed to get closings: %+v", err)
return nil, 0, err
}
return closings, total, nil
}
func (s closingService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock, error) {
closing, err := s.Repository.GetByID(c.Context(), id, s.withRelations)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Closing not found")
}
if err != nil {
s.Log.Errorf("Failed get closing by id: %+v", err)
return nil, err
}
return closing, nil
}
@@ -1,15 +0,0 @@
package validation
type Create struct {
Name string `json:"name" validate:"required_strict,min=3"`
}
type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty"`
}
type Query struct {
Page int `query:"page" validate:"omitempty,number,min=1,gt=0"`
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100,gt=0"`
Search string `query:"search" validate:"omitempty,max=50"`
}
@@ -96,30 +96,30 @@ func (u *ExpenseController) CreateOne(c *fiber.Ctx) error {
} }
req.Documents = form.File["documents"] req.Documents = form.File["documents"]
expenseNonstocksJSON := c.FormValue("expense_nonstocks") costPerKandangJSON := c.FormValue("cost_per_kandangs")
if expenseNonstocksJSON != "" { if costPerKandangJSON != "" {
if err := json.Unmarshal([]byte(expenseNonstocksJSON), &req.ExpenseNonstocks); err != nil { if err := json.Unmarshal([]byte(costPerKandangJSON), &req.CostPerKandangs); err != nil {
var singleExpenseNonstock validation.ExpenseNonstock var singleCostPerKandang validation.CostPerKandang
if err := json.Unmarshal([]byte(expenseNonstocksJSON), &singleExpenseNonstock); err != nil { if err := json.Unmarshal([]byte(costPerKandangJSON), &singleCostPerKandang); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid expense_nonstocks JSON: %v", err)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandangs JSON: %v", err))
} }
if singleExpenseNonstock.KandangID == 0 { if singleCostPerKandang.KandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Field KandangID is required") return fiber.NewError(fiber.StatusBadRequest, "Field KandangID is required")
} }
req.ExpenseNonstocks = []validation.ExpenseNonstock{singleExpenseNonstock} req.CostPerKandangs = []validation.CostPerKandang{singleCostPerKandang}
} else { } else {
for i, expenseNonstock := range req.ExpenseNonstocks { for i, costPerKandang := range req.CostPerKandangs {
if expenseNonstock.KandangID == 0 { if costPerKandang.KandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for expense_nonstocks[%d]", i)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for cost_per_kandangs[%d]", i))
} }
} }
} }
} else { } else {
return fiber.NewError(fiber.StatusBadRequest, "Field expense_nonstocks is required") return fiber.NewError(fiber.StatusBadRequest, "Field cost_per_kandangs is required")
} }
result, err := u.ExpenseService.CreateOne(c, req) result, err := u.ExpenseService.CreateOne(c, req)
@@ -155,32 +155,20 @@ func (u *ExpenseController) UpdateOne(c *fiber.Ctx) error {
req.TransactionDate = &transactionDate req.TransactionDate = &transactionDate
} }
categoryVal := c.FormValue("category") costPerKandangJSON := c.FormValue("cost_per_kandang")
req.Category = &categoryVal if costPerKandangJSON != "" {
var costPerKandang []validation.CostPerKandang
supplierIDVal := c.FormValue("supplier_id") if err := json.Unmarshal([]byte(costPerKandangJSON), &costPerKandang); err != nil {
if supplierIDVal != "" { return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid cost_per_kandang JSON: %v", err))
supplierID, err := strconv.ParseUint(supplierIDVal, 10, 64)
if err != nil {
return fiber.NewError(fiber.StatusBadRequest, "Invalid supplier_id format")
}
req.SupplierID = &supplierID
}
expenseNonstocksJSON := c.FormValue("expense_nonstocks")
if expenseNonstocksJSON != "" {
var expenseNonstocks []validation.ExpenseNonstock
if err := json.Unmarshal([]byte(expenseNonstocksJSON), &expenseNonstocks); err != nil {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid expense_nonstocks JSON: %v", err))
} }
for i, expenseNonstock := range expenseNonstocks { for i, costPerKandang := range costPerKandang {
if expenseNonstock.KandangID == 0 { if costPerKandang.KandangID == 0 {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for expense_nonstocks[%d]", i)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Field KandangID is required for cost_per_kandang[%d]", i))
} }
} }
req.ExpenseNonstocks = &expenseNonstocks req.CostPerKandang = &costPerKandang
} }
result, err := u.ExpenseService.UpdateOne(c, req, uint(id)) result, err := u.ExpenseService.UpdateOne(c, req, uint(id))
+45 -52
View File
@@ -15,9 +15,10 @@ import (
// === DTO Structs === // === DTO Structs ===
type ExpenseRelationDTO struct { type ExpenseRelationDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
PoNumber string `json:"po_number"` PoNumber string `json:"po_number"`
TransactionDate time.Time `json:"transaction_date"` ExpenseDate time.Time `json:"expense_date"`
GrandTotal float64 `json:"grand_total"`
} }
type ExpenseBaseDTO struct { type ExpenseBaseDTO struct {
@@ -27,7 +28,8 @@ type ExpenseBaseDTO struct {
Category string `json:"category"` Category string `json:"category"`
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"` Supplier *supplierDTO.SupplierRelationDTO `json:"supplier,omitempty"`
RealizationDate *time.Time `json:"realization_date,omitempty"` RealizationDate *time.Time `json:"realization_date,omitempty"`
TransactionDate time.Time `json:"transaction_date"` ExpenseDate time.Time `json:"expense_date"`
GrandTotal float64 `json:"grand_total"`
Location *locationDTO.LocationRelationDTO `json:"location,omitempty"` Location *locationDTO.LocationRelationDTO `json:"location,omitempty"`
} }
@@ -53,26 +55,21 @@ type ExpenseDetailDTO struct {
} }
type ExpenseNonstockDTO struct { type ExpenseNonstockDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
ExpenseId *uint64 `json:"expense_id,omitempty"` Qty float64 `json:"qty"`
ProjectFlockKandangId *uint64 `json:"project_flock_kandang_id,omitempty"` UnitPrice float64 `json:"unit_price"`
KandangId *uint64 `json:"kandang_id,omitempty"` TotalPrice float64 `json:"total_price"`
NonstockId *uint64 `json:"nonstock_id,omitempty"` Note *string `json:"note,omitempty"`
Qty float64 `json:"qty"` Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
Price float64 `json:"price"`
Notes string `json:"notes"`
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
CreatedAt time.Time `json:"created_at"`
} }
type ExpenseRealizationDTO struct { type ExpenseRealizationDTO struct {
Id uint64 `json:"id"` Id uint64 `json:"id"`
ExpenseNonstockId *uint64 `json:"expense_nonstock_id,omitempty"` Qty float64 `json:"qty"`
Qty float64 `json:"qty"` UnitPrice float64 `json:"unit_price"`
Price float64 `json:"price"` TotalPrice float64 `json:"total_price"`
Notes string `json:"notes"` Note *string `json:"note,omitempty"`
Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"` Nonstock *nonstockDTO.NonstockRelationDTO `json:"nonstock,omitempty"`
CreatedAt time.Time `json:"created_at"`
} }
type KandangGroupDTO struct { type KandangGroupDTO struct {
@@ -92,9 +89,10 @@ type DocumentDTO struct {
func ToExpenseRelationDTO(e entity.Expense) ExpenseRelationDTO { func ToExpenseRelationDTO(e entity.Expense) ExpenseRelationDTO {
return ExpenseRelationDTO{ return ExpenseRelationDTO{
Id: e.Id, Id: e.Id,
PoNumber: e.PoNumber, PoNumber: e.PoNumber,
TransactionDate: e.TransactionDate, ExpenseDate: e.ExpenseDate,
GrandTotal: e.GrandTotal,
} }
} }
@@ -126,7 +124,8 @@ func ToExpenseBaseDTO(e *entity.Expense) ExpenseBaseDTO {
Category: e.Category, Category: e.Category,
Supplier: supplier, Supplier: supplier,
RealizationDate: realizationDate, RealizationDate: realizationDate,
TransactionDate: e.TransactionDate, ExpenseDate: e.ExpenseDate,
GrandTotal: e.GrandTotal,
Location: location, Location: location,
} }
} }
@@ -193,9 +192,10 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
for _, ns := range e.Nonstocks { for _, ns := range e.Nonstocks {
pengajuanDTO := ToExpenseNonstockDTO(ns) pengajuanDTO := ToExpenseNonstockDTO(ns)
pengajuans = append(pengajuans, pengajuanDTO) pengajuans = append(pengajuans, pengajuanDTO)
if ns.Realization != nil { if ns.Realization != nil && ns.Realization.Id != 0 {
var nonstock *nonstockDTO.NonstockRelationDTO var nonstock *nonstockDTO.NonstockRelationDTO
if ns.Nonstock != nil && ns.Nonstock.Id != 0 { if ns.Nonstock != nil && ns.Nonstock.Id != 0 {
mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock) mapped := nonstockDTO.ToNonstockRelationDTO(*ns.Nonstock)
@@ -203,13 +203,12 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
} }
realisasiDTO := ExpenseRealizationDTO{ realisasiDTO := ExpenseRealizationDTO{
Id: ns.Realization.Id, Id: ns.Realization.Id,
ExpenseNonstockId: ns.Realization.ExpenseNonstockId, Qty: ns.Realization.RealizationQty,
Qty: ns.Realization.Qty, UnitPrice: ns.Realization.RealizationUnitPrice,
Price: ns.Realization.Price, TotalPrice: ns.Realization.RealizationTotalPrice,
Notes: ns.Realization.Notes, Note: ns.Realization.Note,
Nonstock: nonstock, Nonstock: nonstock,
CreatedAt: ns.Realization.CreatedAt,
} }
realisasi = append(realisasi, realisasiDTO) realisasi = append(realisasi, realisasiDTO)
} }
@@ -218,12 +217,12 @@ func ToExpenseDetailDTO(e *entity.Expense) ExpenseDetailDTO {
var totalPengajuan float64 var totalPengajuan float64
for _, p := range pengajuans { for _, p := range pengajuans {
totalPengajuan += p.Qty * p.Price totalPengajuan += p.TotalPrice
} }
var totalRealisasi float64 var totalRealisasi float64
for _, r := range realisasi { for _, r := range realisasi {
totalRealisasi += r.Qty * r.Price totalRealisasi += r.TotalPrice
} }
kandangs := ToKandangGroupDTO(pengajuans, realisasi, e.Nonstocks) kandangs := ToKandangGroupDTO(pengajuans, realisasi, e.Nonstocks)
@@ -249,16 +248,12 @@ func ToExpenseNonstockDTO(ns entity.ExpenseNonstock) ExpenseNonstockDTO {
} }
return ExpenseNonstockDTO{ return ExpenseNonstockDTO{
Id: ns.Id, Id: ns.Id,
ExpenseId: ns.ExpenseId, Qty: ns.Qty,
ProjectFlockKandangId: ns.ProjectFlockKandangId, UnitPrice: ns.UnitPrice,
KandangId: ns.KandangId, TotalPrice: ns.TotalPrice,
NonstockId: ns.NonstockId, Note: &ns.Note,
Qty: ns.Qty, Nonstock: nonstock,
Price: ns.Price,
Notes: ns.Notes,
Nonstock: nonstock,
CreatedAt: ns.CreatedAt,
} }
} }
@@ -269,13 +264,11 @@ func ToKandangGroupDTO(pengajuans []ExpenseNonstockDTO, realisasi []ExpenseReali
var kandangId uint64 var kandangId uint64
var kandangName string var kandangName string
if p.KandangId != nil { for _, ns := range nonstocks {
kandangId = *p.KandangId if ns.Id == p.Id && ns.Kandang != nil {
for _, ns := range nonstocks { kandangId = uint64(ns.Kandang.Id)
if ns.Id == p.Id && ns.Kandang != nil { kandangName = ns.Kandang.Name
kandangName = ns.Kandang.Name break
break
}
} }
} }
@@ -10,7 +10,7 @@ import (
type ExpenseRepository interface { type ExpenseRepository interface {
repository.BaseRepository[entity.Expense] repository.BaseRepository[entity.Expense]
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint64) (bool, error)
GetNextSequence(ctx context.Context) (int, error) GetNextSequence(ctx context.Context) (int, error)
GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error) GetWithSupplier(ctx context.Context, id uint64) (*entity.Expense, error)
} }
@@ -25,8 +25,8 @@ func NewExpenseRepository(db *gorm.DB) ExpenseRepository {
} }
} }
func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint) (bool, error) { func (r *ExpenseRepositoryImpl) IdExists(ctx context.Context, id uint64) (bool, error) {
return repository.Exists[entity.Expense](ctx, r.DB(), id) return repository.Exists[entity.Expense](ctx, r.DB(), uint(id))
} }
func (r *ExpenseRepositoryImpl) GetNextSequence(ctx context.Context) (int, error) { func (r *ExpenseRepositoryImpl) GetNextSequence(ctx context.Context) (int, error) {
+2 -2
View File
@@ -1,7 +1,7 @@
package expenses package expenses
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/expenses/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/controllers"
expense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services" expense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/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 ExpenseRoutes(v1 fiber.Router, u user.UserService, s expense.ExpenseService
ctrl := controller.NewExpenseController(s) ctrl := controller.NewExpenseController(s)
route := v1.Group("/expenses") route := v1.Group("/expenses")
route.Use(m.Auth(u))
// route.Get("/", m.Auth(u), ctrl.GetAll) // route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne) // route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne) // route.Get("/:id", m.Auth(u), ctrl.GetOne)
@@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"mime/multipart" "mime/multipart"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
@@ -147,8 +148,8 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
return nil, err return nil, err
} }
for _, expenseNonstock := range req.ExpenseNonstocks { for _, costPerKandang := range req.CostPerKandangs {
for _, costItem := range expenseNonstock.CostItems { for _, costItem := range costPerKandang.CostItems {
nonstockId := uint(costItem.NonstockID) nonstockId := uint(costItem.NonstockID)
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
@@ -188,13 +189,21 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number") return fiber.NewError(fiber.StatusInternalServerError, "Failed to generate reference number")
} }
var grandTotal float64
for _, costPerKandang := range req.CostPerKandangs {
for _, costItem := range costPerKandang.CostItems {
grandTotal += costItem.TotalCost
}
}
createdBy := uint64(1) //todo get from auth createdBy := uint64(1) //todo get from auth
expense = &entity.Expense{ expense = &entity.Expense{
ReferenceNumber: referenceNumber, ReferenceNumber: referenceNumber,
PoNumber: req.PoNumber, PoNumber: req.PoNumber,
Category: req.Category, Category: req.Category,
SupplierId: req.SupplierID, SupplierId: req.SupplierID,
TransactionDate: expenseDate, ExpenseDate: expenseDate,
GrandTotal: grandTotal,
CreatedBy: createdBy, CreatedBy: createdBy,
} }
@@ -202,15 +211,15 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense") return fiber.NewError(fiber.StatusInternalServerError, "Failed to create expense")
} }
if len(req.ExpenseNonstocks) > 0 { if len(req.CostPerKandangs) > 0 {
for _, expenseNonstock := range req.ExpenseNonstocks { for _, costPerKandang := range req.CostPerKandangs {
var projectFlockKandangId *uint64 var projectFlockKandangId *uint64
if req.Category == "BOP" { if req.Category == "BOP" {
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID)) projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(costPerKandang.KandangID))
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang") return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
@@ -221,16 +230,16 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
projectFlockKandangId = &id projectFlockKandangId = &id
} }
for _, costItem := range expenseNonstock.CostItems { for _, costItem := range costPerKandang.CostItems {
nonstockId := costItem.NonstockID nonstockId := costItem.NonstockID
var kandangId *uint64 var kandangId *uint64
if req.Category == "NON-BOP" { if req.Category == "NON-BOP" {
id := uint64(expenseNonstock.KandangID) id := uint64(costPerKandang.KandangID)
kandangId = &id kandangId = &id
} else if req.Category == "BOP" { } else if req.Category == "BOP" {
if projectFlockKandangId != nil { if projectFlockKandangId != nil {
kandangId = &expenseNonstock.KandangID kandangId = &costPerKandang.KandangID
} }
} }
@@ -240,8 +249,8 @@ func (s *expenseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*expen
KandangId: kandangId, KandangId: kandangId,
NonstockId: &nonstockId, NonstockId: &nonstockId,
Qty: costItem.Quantity, Qty: costItem.Quantity,
Price: costItem.Price, TotalPrice: costItem.TotalCost,
Notes: costItem.Notes, Note: costItem.Notes,
} }
if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil { if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil {
@@ -293,7 +302,9 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
} }
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists}, commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
return s.Repository.IdExists(ctx, uint64(id))
}},
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -317,27 +328,10 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid transaction_date format")
} }
updateBody["transaction_date"] = expenseDate updateBody["expense_date"] = expenseDate
} }
if req.Category != nil { if len(updateBody) == 0 && req.CostPerKandang == nil && len(req.Documents) == 0 {
updateBody["category"] = *req.Category
}
if req.SupplierID != nil {
supplierID := uint(*req.SupplierID)
supplierExistsFunc := func(ctx context.Context, id uint) (bool, error) {
return commonRepo.Exists[entity.Supplier](ctx, s.SupplierRepo.DB(), id)
}
if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Supplier", ID: &supplierID, Exists: supplierExistsFunc},
); err != nil {
return nil, err
}
updateBody["supplier_id"] = *req.SupplierID
}
if len(updateBody) == 0 && req.ExpenseNonstocks == nil && len(req.Documents) == 0 {
responseDTO, err := s.GetOne(c, id) responseDTO, err := s.GetOne(c, id)
if err != nil { if err != nil {
@@ -352,21 +346,6 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx) expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
currentExpense, err := expenseRepoTx.GetByID(c.Context(), id, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
}
categoryChanged := false
var newCategory string
if req.Category != nil && *req.Category != currentExpense.Category {
categoryChanged = true
newCategory = *req.Category
}
if len(updateBody) > 0 { if len(updateBody) > 0 {
if err := expenseRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil { if err := expenseRepoTx.PatchOne(c.Context(), id, updateBody, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
@@ -376,79 +355,41 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
} }
} }
if categoryChanged { if req.CostPerKandang != nil {
if currentExpense.Category == "BOP" && newCategory == "NON-BOP" {
var existingExpenseNonstocks []entity.ExpenseNonstock if err := tx.Where("expense_id = ?", id).Delete(&entity.ExpenseNonstock{}).Error; err != nil {
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil { return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense items")
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks")
}
for _, ens := range existingExpenseNonstocks {
updateData := map[string]interface{}{
"project_flock_kandang_id": nil,
}
if err := expenseNonstockRepoTx.PatchOne(c.Context(), uint(ens.Id), updateData, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id to null")
}
}
} else if currentExpense.Category == "NON-BOP" && newCategory == "BOP" {
var existingExpenseNonstocks []entity.ExpenseNonstock
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks")
}
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx)
for _, ens := range existingExpenseNonstocks {
if ens.KandangId != nil {
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(*ens.KandangId))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to find project flock kandang for this kandang")
}
projectFlockKandangId := uint64(projectFlockKandang.Id)
updateData := map[string]interface{}{
"project_flock_kandang_id": projectFlockKandangId,
}
if err := expenseNonstockRepoTx.PatchOne(c.Context(), uint(ens.Id), updateData, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock kandang id")
}
}
}
}
}
if req.ExpenseNonstocks != nil {
var existingExpenseNonstocks []entity.ExpenseNonstock
if err := tx.Where("expense_id = ?", id).Find(&existingExpenseNonstocks).Error; err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense nonstocks for deletion")
} }
for _, ens := range existingExpenseNonstocks { var grandTotal float64
if err := expenseNonstockRepoTx.DeleteOne(c.Context(), uint(ens.Id)); err != nil { for _, cpk := range *req.CostPerKandang {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete expense nonstock") for _, costItem := range cpk.CostItems {
grandTotal += costItem.TotalCost
} }
} }
updatedExpense, err := expenseRepoTx.GetByID(c.Context(), id, nil) if err := expenseRepoTx.PatchOne(c.Context(), id, map[string]interface{}{
if err != nil { "grand_total": grandTotal,
if errors.Is(err, gorm.ErrRecordNotFound) { }, nil); err != nil {
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
} return fiber.NewError(fiber.StatusInternalServerError, "Failed to update expense grand total")
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get updated expense")
} }
for _, expenseNonstock := range *req.ExpenseNonstocks { for _, cpk := range *req.CostPerKandang {
var projectFlockKandangId *uint64 var projectFlockKandangId *uint64
if updatedExpense.Category == "BOP" { expense, err := expenseRepoTx.GetByID(c.Context(), id, nil)
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
}
if expense.Category == "BOP" {
projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx) projectFlockKandangRepoTx := projectFlockKandangRepo.NewProjectFlockKandangRepository(tx)
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(expenseNonstock.KandangID)) projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(cpk.KandangID))
if err != nil { if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang") return fiber.NewError(fiber.StatusNotFound, "No active project flock kandang found for this kandang")
@@ -459,7 +400,7 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
projectFlockKandangId = &id projectFlockKandangId = &id
} }
for _, costItem := range expenseNonstock.CostItems { for _, costItem := range cpk.CostItems {
nonstockId := uint(costItem.NonstockID) nonstockId := uint(costItem.NonstockID)
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
@@ -469,12 +410,13 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
} }
var kandangId *uint64 var kandangId *uint64
if updatedExpense.Category == "NON-BOP" { if expense.Category == "NON-BOP" {
id := uint64(expenseNonstock.KandangID) id := uint64(cpk.KandangID)
kandangId = &id kandangId = &id
} else if updatedExpense.Category == "BOP" { } else if expense.Category == "BOP" {
if projectFlockKandangId != nil { if projectFlockKandangId != nil {
kandangId = &expenseNonstock.KandangID kandangId = &cpk.KandangID
} }
} }
@@ -485,8 +427,8 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
KandangId: kandangId, KandangId: kandangId,
NonstockId: &costItem.NonstockID, NonstockId: &costItem.NonstockID,
Qty: costItem.Quantity, Qty: costItem.Quantity,
Price: costItem.Price, TotalPrice: costItem.TotalCost,
Notes: costItem.Notes, Note: costItem.Notes,
} }
if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil { if err := expenseNonstockRepoTx.CreateOne(c.Context(), expenseNonstock, nil); err != nil {
@@ -539,7 +481,9 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error { func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error {
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists}, commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
return s.Repository.IdExists(ctx, uint64(id))
}},
); err != nil { ); err != nil {
return err return err
} }
@@ -562,7 +506,9 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
} }
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists}, commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) {
return s.Repository.IdExists(ctx, uint64(id))
}},
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -572,6 +518,8 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format")
} }
createdBy := uint64(1) // TODO: replace with authenticated user id
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx)) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
@@ -595,14 +543,13 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
} }
realization := &entity.ExpenseRealization{ realization := &entity.ExpenseRealization{
ExpenseNonstockId: &expenseNonstockID, ExpenseNonstockId: &expenseNonstockID,
Qty: realizationItem.Qty, RealizationQty: realizationItem.Qty,
Price: realizationItem.Price, RealizationUnitPrice: realizationItem.UnitPrice,
Notes: "", RealizationTotalPrice: realizationItem.TotalPrice,
} RealizationDate: realizationDate,
Note: realizationItem.Notes,
if realizationItem.Notes != nil { CreatedBy: &createdBy,
realization.Notes = *realizationItem.Notes
} }
if err := realizationRepoTx.CreateOne(c.Context(), realization, nil); err != nil { if err := realizationRepoTx.CreateOne(c.Context(), realization, nil); err != nil {
@@ -629,7 +576,7 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
expenseID, expenseID,
utils.ExpenseStepRealisasi, utils.ExpenseStepRealisasi,
&approvalAction, &approvalAction,
uint(1), // TODO: replace with authenticated user id uint(createdBy),
nil); err != nil { nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval") return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval")
@@ -650,7 +597,9 @@ func (s *expenseService) CreateRealization(c *fiber.Ctx, expenseID uint, req *va
func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) { func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (*expenseDto.ExpenseDetailDTO, error) {
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists}, commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
return s.Repository.IdExists(ctx, uint64(id))
}},
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -703,12 +652,14 @@ func (s *expenseService) CompleteExpense(c *fiber.Ctx, id uint, notes *string) (
func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) { func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *validation.UpdateRealization) (*expenseDto.ExpenseDetailDTO, error) {
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
s.Log.Errorf("Validation failed for UpdateRealization: %+v", err)
return nil, err return nil, err
} }
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists}, commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) {
return s.Repository.IdExists(ctx, uint64(id))
}},
); err != nil { ); err != nil {
return nil, err return nil, err
} }
@@ -718,56 +669,66 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
} }
if latestApproval != nil && (latestApproval.StepNumber < uint16(utils.ExpenseStepRealisasi)) { if latestApproval != nil && latestApproval.StepNumber != uint16(utils.ExpenseStepRealisasi) {
currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)] currentStepName := utils.ExpenseApprovalSteps[approvalutils.ApprovalStep(latestApproval.StepNumber)]
return nil, fiber.NewError(fiber.StatusBadRequest, return nil, fiber.NewError(fiber.StatusBadRequest,
fmt.Sprintf("tidak bisa update realisasi pada step %s. Harus pada step Realisasi atau selesai", currentStepName)) fmt.Sprintf("Cannot update realization at %s step. Must be at Realisasi step", currentStepName))
} }
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error { var realizationDate *time.Time
if req.RealizationDate != "" {
parsedDate, err := utils.ParseDateString(req.RealizationDate)
if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid realization_date format")
}
realizationDate = &parsedDate
}
if err := s.Repository.DB().WithContext(c.Context()).Transaction(func(tx *gorm.DB) error {
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
realizationRepoTx := repository.NewExpenseRealizationRepository(tx) realizationRepoTx := repository.NewExpenseRealizationRepository(tx)
expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx) expenseNonstockRepoTx := repository.NewExpenseNonstockRepository(tx)
expenseRepoTx := repository.NewExpenseRepository(tx) expenseRepoTx := repository.NewExpenseRepository(tx)
// Check if only updating documents for _, realizationItem := range req.Realizations {
updateDataOnly := len(req.Realizations) == 0 && len(req.Documents) > 0
if len(req.Realizations) > 0 { expenseNonstockID := realizationItem.ExpenseNonstockID
for _, realizationItem := range req.Realizations {
expenseNonstockID := realizationItem.ExpenseNonstockID if err := s.validateExpenseNonstockRelation(c, expenseNonstockRepoTx, expenseID, expenseNonstockID); err != nil {
return err
if err := s.validateExpenseNonstockRelation(c, expenseNonstockRepoTx, expenseID, expenseNonstockID); err != nil {
return err
}
existingRealization, err := realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Realization not found for this expense nonstock")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get existing realization")
}
updateData := map[string]interface{}{
"qty": realizationItem.Qty,
"price": realizationItem.Price,
}
if realizationItem.Notes != nil {
updateData["notes"] = *realizationItem.Notes
}
if err := realizationRepoTx.PatchOne(c.Context(), uint(existingRealization.Id), updateData, nil); err != nil {
s.Log.Errorf("Failed to update realization: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization")
}
} }
existingRealization, err := realizationRepoTx.GetByExpenseNonstockID(c.Context(), uint64(expenseNonstockID))
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return fiber.NewError(fiber.StatusNotFound, "Realization not found for this expense nonstock")
}
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get existing realization")
}
updateData := map[string]interface{}{
"realization_qty": realizationItem.Qty,
"realization_unit_price": realizationItem.UnitPrice,
"realization_total_price": realizationItem.TotalPrice,
"realization_date": *realizationDate,
}
if realizationItem.Notes != nil {
updateData["note"] = *realizationItem.Notes
}
if err := realizationRepoTx.PatchOne(c.Context(), uint(existingRealization.Id), updateData, nil); err != nil {
s.Log.Errorf("Failed to update realization: %+v", err)
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization")
}
}
if err := expenseRepoTx.PatchOne(c.Context(), expenseID, map[string]interface{}{
"realization_date": *realizationDate,
}, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to update realization date")
} }
if len(req.Documents) > 0 { if len(req.Documents) > 0 {
@@ -776,28 +737,9 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
} }
} }
if !updateDataOnly && *latestApproval.Action == entity.ApprovalActionUpdated {
actorID := uint(1) // TODO: replace with authenticated user id
approvalAction := entity.ApprovalActionUpdated
if _, err := approvalSvcTx.CreateApproval(
c.Context(),
utils.ApprovalWorkflowExpense,
expenseID,
utils.ExpenseStepRealisasi,
&approvalAction,
actorID,
nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create realization approval")
}
}
return nil return nil
}) }); err != nil {
if err != nil { return nil, err
if fiberErr, ok := err.(*fiber.Error); ok {
return nil, fiberErr
}
return nil, fiber.NewError(fiber.StatusInternalServerError, "gagal update realisasi expense")
} }
responseDTO, err := s.GetOne(c, expenseID) responseDTO, err := s.GetOne(c, expenseID)
@@ -883,7 +825,9 @@ func (s *expenseService) processDocuments(ctx *fiber.Ctx, expenseRepoTx reposito
func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error { func (s *expenseService) DeleteDocument(ctx *fiber.Ctx, expenseID uint, documentID uint64, isRealization bool) error {
if err := commonSvc.EnsureRelations(ctx.Context(), if err := commonSvc.EnsureRelations(ctx.Context(),
commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: s.Repository.IdExists}, commonSvc.RelationCheck{Name: "Expense", ID: &expenseID, Exists: func(ctx context.Context, id uint) (bool, error) {
return s.Repository.IdExists(ctx, uint64(id))
}},
); err != nil { ); err != nil {
return err return err
} }
@@ -965,7 +909,9 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
for _, id := range req.ApprovableIds { for _, id := range req.ApprovableIds {
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: s.Repository.IdExists}, commonSvc.RelationCheck{Name: "Expense", ID: &id, Exists: func(ctx context.Context, id uint) (bool, error) {
return s.Repository.IdExists(ctx, uint64(id))
}},
); err != nil { ); err != nil {
return err return err
} }
@@ -5,15 +5,15 @@ import (
) )
type Create struct { type Create struct {
PoNumber string `form:"po_number" json:"po_number" validate:"omitempty,max=50"` PoNumber string `form:"po_number" json:"po_number" validate:"omitempty,max=50"`
TransactionDate string `form:"transaction_date" json:"transaction_date" validate:"required,datetime=2006-01-02"` TransactionDate string `form:"transaction_date" json:"transaction_date" validate:"required,datetime=2006-01-02"`
Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"` Category string `form:"category" json:"category" validate:"required,oneof=BOP NON-BOP"`
SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"` SupplierID uint64 `form:"supplier_id" json:"supplier_id" validate:"required,gt=0"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"` Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
ExpenseNonstocks []ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"required,min=1,dive"` CostPerKandangs []CostPerKandang `form:"cost_per_kandangs" json:"cost_per_kandangs" validate:"required,min=1,dive"`
} }
type ExpenseNonstock struct { type CostPerKandang struct {
KandangID uint64 `form:"kandang_id" json:"kandang_id" validate:"required,gt=0"` KandangID uint64 `form:"kandang_id" json:"kandang_id" validate:"required,gt=0"`
CostItems []CostItem `form:"cost_items" json:"cost_items" validate:"required,min=1,dive"` CostItems []CostItem `form:"cost_items" json:"cost_items" validate:"required,min=1,dive"`
} }
@@ -21,16 +21,14 @@ type ExpenseNonstock struct {
type CostItem struct { type CostItem struct {
NonstockID uint64 `form:"nonstock_id" json:"nonstock_id" validate:"required,gt=0"` NonstockID uint64 `form:"nonstock_id" json:"nonstock_id" validate:"required,gt=0"`
Quantity float64 `form:"quantity" json:"quantity" validate:"required,gt=0"` Quantity float64 `form:"quantity" json:"quantity" validate:"required,gt=0"`
Price float64 `form:"price" json:"price" validate:"required,gt=0"` TotalCost float64 `form:"total_cost" json:"total_cost" validate:"required,gt=0"`
Notes string `form:"notes" json:"notes" validate:"required,max=500"` Notes string `form:"notes" json:"notes" validate:"required,max=500"`
} }
type Update struct { type Update struct {
TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"` TransactionDate *string `form:"transaction_date" json:"transaction_date" validate:"omitempty,datetime=2006-01-02"`
Category *string `form:"category" json:"category" validate:"omitempty,oneof=BOP NON-BOP"` CostPerKandang *[]CostPerKandang `form:"cost_per_kandang" json:"cost_per_kandang" validate:"omitempty,min=1,dive"`
SupplierID *uint64 `form:"supplier_id" json:"supplier_id" validate:"omitempty,gt=0"` Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
ExpenseNonstocks *[]ExpenseNonstock `form:"expense_nonstocks" json:"expense_nonstocks" validate:"omitempty,min=1,dive"`
Documents []*multipart.FileHeader `form:"documents" json:"documents" validate:"omitempty,dive"`
} }
type Query struct { type Query struct {
@@ -54,7 +52,8 @@ type UpdateRealization struct {
type RealizationItem struct { type RealizationItem struct {
ExpenseNonstockID uint64 `form:"expense_nonstock_id" json:"expense_nonstock_id" validate:"required,gt=0"` ExpenseNonstockID uint64 `form:"expense_nonstock_id" json:"expense_nonstock_id" validate:"required,gt=0"`
Qty float64 `form:"qty" json:"qty" validate:"required,gt=0"` Qty float64 `form:"qty" json:"qty" validate:"required,gt=0"`
Price float64 `form:"price" json:"price" validate:"required,gt=0"` UnitPrice float64 `form:"unit_price" json:"unit_price" validate:"required,gt=0"`
TotalPrice float64 `form:"total_price" json:"total_price" validate:"required,gt=0"`
Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"` Notes *string `form:"notes" json:"notes" validate:"omitempty,max=500"`
} }
@@ -5,8 +5,8 @@ import (
"strings" "strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service" common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/adjustments/validations"
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories" productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
@@ -78,10 +78,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
return nil, err return nil, err
} }
ctx := c.Context() ctx := c.Context()
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
if err := common.EnsureRelations(c.Context(), if err := common.EnsureRelations(c.Context(),
common.RelationCheck{Name: "Product", ID: &req.ProductID, Exists: s.ProductRepo.IdExists}, common.RelationCheck{Name: "Product", ID: &req.ProductID, Exists: s.ProductRepo.IdExists},
common.RelationCheck{Name: "Warehouse", ID: &req.WarehouseID, Exists: s.WarehouseRepo.IdExists}, common.RelationCheck{Name: "Warehouse", ID: &req.WarehouseID, Exists: s.WarehouseRepo.IdExists},
@@ -110,7 +107,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
ProductId: uint(req.ProductID), ProductId: uint(req.ProductID),
WarehouseId: uint(req.WarehouseID), WarehouseId: uint(req.WarehouseID),
Quantity: 0, Quantity: 0,
CreatedBy: actorID, CreatedBy: 1, // TODO: should Get from auth middleware
} }
if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil { if err := s.ProductWarehouseRepo.CreateOne(ctx, newPW, nil); err != nil {
@@ -146,7 +143,7 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
LogId: 0, LogId: 0,
Note: req.Note, Note: req.Note,
ProductWarehouseId: productWarehouse.Id, ProductWarehouseId: productWarehouse.Id,
CreatedBy: actorID, CreatedBy: 1, // TODO: should Get from auth middleware
} }
if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil { if err := s.StockLogsRepository.WithTx(tx).CreateOne(ctx, newLog, nil); err != nil {
@@ -27,7 +27,7 @@ type ProductWarehouseRepository interface {
GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error) GetDetailByID(ctx context.Context, id uint) (*entity.ProductWarehouse, error)
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error CleanupEmpty(ctx context.Context, affected map[uint]struct{}) error
EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, createdBy uint) (uint, error) EnsureProductWarehouse(ctx context.Context, productID, warehouseID uint, createdBy uint64) (uint, error)
} }
type ProductWarehouseRepositoryImpl struct { type ProductWarehouseRepositoryImpl struct {
@@ -199,7 +199,7 @@ func (r *ProductWarehouseRepositoryImpl) EnsureProductWarehouse(
ctx context.Context, ctx context.Context,
productID uint, productID uint,
warehouseID uint, warehouseID uint,
createdBy uint, createdBy uint64,
) (uint, error) { ) (uint, error) {
record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID) record, err := r.GetProductWarehouseByProductAndWarehouseID(ctx, productID, warehouseID)
if err == nil { if err == nil {
@@ -3,15 +3,15 @@ package service
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
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"
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories" rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories" rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"strings"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -127,10 +127,6 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID)) return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok produk %d di gudang asal tidak cukup", product.ProductID))
} }
} }
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
// validasi total qty harus lebih besar dari atau sama dengan total qty di delivery compare berdasarkan productid // validasi total qty harus lebih besar dari atau sama dengan total qty di delivery compare berdasarkan productid
deliveryQtyMap := make(map[uint]float64) deliveryQtyMap := make(map[uint]float64)
@@ -178,7 +174,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
Reason: req.TransferReason, Reason: req.TransferReason,
TransferDate: transferDate, TransferDate: transferDate,
MovementNumber: movementNumber, MovementNumber: movementNumber,
CreatedBy: uint64(actorID), CreatedBy: 1, //todo: get from token
} }
// Save the transfer entity to the database // Save the transfer entity to the database
@@ -281,7 +277,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
LogId: uint(entityTransfer.Id), LogId: uint(entityTransfer.Id),
Note: "", Note: "",
ProductWarehouseId: sourcePW.Id, ProductWarehouseId: sourcePW.Id,
CreatedBy: actorID, CreatedBy: 1,
} }
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), decreaseLog, nil); err != nil { if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), decreaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log decrease: %+v", err) s.Log.Errorf("Failed to create stock log decrease: %+v", err)
@@ -302,7 +298,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
ProductId: uint(product.ProductID), ProductId: uint(product.ProductID),
WarehouseId: uint(req.DestinationWarehouseID), WarehouseId: uint(req.DestinationWarehouseID),
Quantity: 0, Quantity: 0,
CreatedBy: actorID, CreatedBy: 1, // TODO: should Get from auth middleware
} }
if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil { if err := s.ProductWarehouseRepo.WithTx(tx).CreateOne(c.Context(), destPW, nil); err != nil {
s.Log.Errorf("Failed to create destination product warehouse: %+v", err) s.Log.Errorf("Failed to create destination product warehouse: %+v", err)
@@ -329,7 +325,7 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
LogId: uint(entityTransfer.Id), LogId: uint(entityTransfer.Id),
Note: "", Note: "",
ProductWarehouseId: destPW.Id, ProductWarehouseId: destPW.Id,
CreatedBy: actorID, CreatedBy: 1,
} }
if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), increaseLog, nil); err != nil { if err := s.StockLogsRepository.WithTx(tx).CreateOne(c.Context(), increaseLog, nil); err != nil {
s.Log.Errorf("Failed to create stock log increase: %+v", err) s.Log.Errorf("Failed to create stock log increase: %+v", err)
@@ -8,7 +8,6 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
marketingDeliveryProductRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" marketingDeliveryProductRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
@@ -176,11 +175,6 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB()))
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil) latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowMarketing, req.MarketingId, nil)
@@ -196,6 +190,9 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
if latestApproval.StepNumber >= uint16(utils.MarketingDeliveryOrder) { if latestApproval.StepNumber >= uint16(utils.MarketingDeliveryOrder) {
return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery order already exists for this marketing") return nil, fiber.NewError(fiber.StatusBadRequest, "Delivery order already exists for this marketing")
} }
if latestApproval.Action == nil || *latestApproval.Action != entity.ApprovalActionApproved {
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Marketing is not approved - current status: %v", *latestApproval.Action))
}
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
@@ -259,7 +256,7 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
} }
actorID := uint(1) // TODO: ambil dari auth context
approvalAction := entity.ApprovalActionApproved approvalAction := entity.ApprovalActionApproved
if _, err := approvalSvcTx.CreateApproval( if _, err := approvalSvcTx.CreateApproval(
c.Context(), c.Context(),
@@ -2,22 +2,16 @@ package repository
import ( import (
"context" "context"
"fmt"
"strconv"
"strings"
"gitlab.com/mbugroup/lti-api.git/internal/common/repository" "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gitlab.com/mbugroup/lti-api.git/internal/utils"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause"
) )
type MarketingRepository interface { type MarketingRepository interface {
repository.BaseRepository[entity.Marketing] repository.BaseRepository[entity.Marketing]
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
GetNextSequence(ctx context.Context) (uint, error) GetNextSequence(ctx context.Context) (uint, error)
NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error)
} }
type MarketingRepositoryImpl struct { type MarketingRepositoryImpl struct {
@@ -41,82 +35,3 @@ func (r *MarketingRepositoryImpl) GetNextSequence(ctx context.Context) (uint, er
} }
return maxID + 1, nil return maxID + 1, nil
} }
func (r *MarketingRepositoryImpl) NextSoNumber(ctx context.Context, tx *gorm.DB) (string, error) {
return r.generateSequentialNumber(ctx, tx, "so_number", utils.MarketingSoNumberPrefix, utils.MarketingNumberPadding)
}
func parseNumericSuffix(value, prefix string) (int, bool) {
if !strings.HasPrefix(value, prefix) {
return 0, false
}
suffix := strings.TrimPrefix(value, prefix)
if suffix == "" {
return 0, false
}
trimmed := strings.TrimLeft(suffix, "0")
if trimmed == "" {
trimmed = "0"
}
number, err := strconv.Atoi(trimmed)
if err != nil {
return 0, false
}
return number, true
}
func (r *MarketingRepositoryImpl) numberExists(ctx context.Context, db *gorm.DB, column, value string) (bool, error) {
var count int64
if err := db.WithContext(ctx).
Model(&entity.Marketing{}).
Where(fmt.Sprintf("%s = ?", column), value).
Count(&count).Error; err != nil {
return false, err
}
return count > 0, nil
}
func (r *MarketingRepositoryImpl) generateSequentialNumber(ctx context.Context, tx *gorm.DB, column, prefix string, padding int) (string, error) {
db := tx
if db == nil {
db = r.DB()
}
var values []string
err := db.WithContext(ctx).
Model(&entity.Marketing{}).
Where(fmt.Sprintf("%s LIKE ?", column), prefix+"%").
Select(column).
Order(fmt.Sprintf("%s DESC", column)).
Limit(20).
Clauses(clause.Locking{Strength: "UPDATE"}).
Pluck(column, &values).Error
if err != nil {
return "", err
}
next := 1
for _, value := range values {
if number, ok := parseNumericSuffix(value, prefix); ok {
next = number + 1
break
}
}
const maxAttempts = 20
for attempt := 0; attempt < maxAttempts; attempt++ {
candidate := fmt.Sprintf("%s%0*d", prefix, padding, next)
exists, err := r.numberExists(ctx, db, column, candidate)
if err != nil {
return "", err
}
if !exists {
return candidate, nil
}
next++
}
return "", fmt.Errorf("unable to generate unique %s", column)
}
@@ -9,7 +9,6 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
rInvMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories" rInvMarketingDeliveryProduct "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/marketing-delivery-products/repositories"
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
@@ -91,11 +90,6 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists}, commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists},
); err != nil { ); err != nil {
@@ -115,10 +109,11 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format") return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid date format")
} }
soNumber, err := s.MarketingRepo.NextSoNumber(context.Background(), nil) nextSeq, err := s.MarketingRepo.GetNextSequence(c.Context())
if err != nil { if err != nil {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate SO number") return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate SO number")
} }
soNumber := fmt.Sprintf("SO-%05d", nextSeq)
var marketing *entity.Marketing var marketing *entity.Marketing
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
@@ -134,7 +129,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
SoDate: soDate, SoDate: soDate,
SalesPersonId: req.SalesPersonId, SalesPersonId: req.SalesPersonId,
Notes: req.Notes, Notes: req.Notes,
CreatedBy: actorID, CreatedBy: 1,
} }
if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil { if err := marketingRepoTx.CreateOne(c.Context(), marketing, nil); err != nil {
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders") return fiber.NewError(fiber.StatusInternalServerError, "Failed to create salesOrders")
@@ -148,6 +143,7 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
} }
} }
actorID := uint(1) // TODO: ambil dari auth context
approvalAction := entity.ApprovalActionCreated approvalAction := entity.ApprovalActionCreated
if _, err := approvalSvcTx.CreateApproval( if _, err := approvalSvcTx.CreateApproval(
c.Context(), c.Context(),
@@ -184,11 +180,6 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
if err := commonSvc.EnsureRelations(c.Context(), if err := commonSvc.EnsureRelations(c.Context(),
commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists}, commonSvc.RelationCheck{Name: "Marketing", ID: &id, Exists: s.MarketingRepo.IdExists},
commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists}, commonSvc.RelationCheck{Name: "Customer", ID: &req.CustomerId, Exists: s.CustomerRepo.IdExists},
@@ -330,6 +321,7 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
} }
} }
if latestApproval != nil { if latestApproval != nil {
actorID := uint(1) // todo: ambil dari auth context
action := entity.ApprovalActionUpdated action := entity.ApprovalActionUpdated
_, err := approvalSvcTx.CreateApproval( _, err := approvalSvcTx.CreateApproval(
c.Context(), c.Context(),
@@ -413,11 +405,6 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB())) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.MarketingRepo.DB()))
var action entity.ApprovalAction var action entity.ApprovalAction
@@ -461,7 +448,7 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
} }
} }
err = s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err := s.MarketingRepo.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
@@ -492,6 +479,7 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
nextStep = approvalutils.ApprovalStep(currentStep) nextStep = approvalutils.ApprovalStep(currentStep)
} }
actorID := uint(1) // todo ambil dari auth context
if _, err := approvalSvc.CreateApproval( if _, err := approvalSvc.CreateApproval(
c.Context(), c.Context(),
utils.ApprovalWorkflowMarketing, utils.ApprovalWorkflowMarketing,
@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/areas/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -88,14 +87,10 @@ func (s *areaService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.A
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Area with name %s already exists", req.Name)) return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Area with name %s already exists", req.Name))
} }
actorID, err := m.ActorIDFromContext(c) //TODO: created by dummy
if err != nil {
return nil, err
}
createBody := &entity.Area{ createBody := &entity.Area{
Name: req.Name, Name: req.Name,
CreatedBy: actorID, CreatedBy: 1,
} }
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -1,11 +1,11 @@
package validation package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3"`
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
} }
type Query struct { type Query struct {
@@ -1,16 +1,16 @@
package validation package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3"`
Alias string `json:"alias" validate:"required_strict,max=5"` Alias string `json:"alias" validate:"required_strict"`
Owner *string `json:"owner,omitempty" validate:"omitempty,max=50"` Owner *string `json:"owner,omitempty" validate:"omitempty"`
AccountNumber string `json:"account_number" validate:"required_strict,max=50"` AccountNumber string `json:"account_number" validate:"required_strict,max=50"`
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
Alias *string `json:"alias,omitempty" validate:"omitempty,max=5"` Alias *string `json:"alias,omitempty" validate:"omitempty"`
Owner *string `json:"owner,omitempty" validate:"omitempty,max=50"` Owner *string `json:"owner,omitempty" validate:"omitempty"`
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"` AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"`
} }
@@ -3,13 +3,13 @@ package service
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service" common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"strings"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -81,10 +81,6 @@ func (s *customerService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
if err := s.Validate.Struct(req); err != nil { if err := s.Validate.Struct(req); err != nil {
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil { if exists, err := s.Repository.NameExists(c.Context(), req.Name, nil); err != nil {
s.Log.Errorf("Failed to check customer name: %+v", err) s.Log.Errorf("Failed to check customer name: %+v", err)
@@ -104,6 +100,7 @@ func (s *customerService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
return nil, err return nil, err
} }
//TODO: created by dummy
createBody := &entity.Customer{ createBody := &entity.Customer{
Name: req.Name, Name: req.Name,
PicId: req.PicId, PicId: req.PicId,
@@ -112,7 +109,7 @@ func (s *customerService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
Phone: req.Phone, Phone: req.Phone,
Email: req.Email, Email: req.Email,
AccountNumber: req.AccountNumber, AccountNumber: req.AccountNumber,
CreatedBy: actorID, CreatedBy: 1,
} }
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -1,23 +1,23 @@
package validation package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"` PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
Type string `json:"type" validate:"required_strict,max=50"` Type string `json:"type" validate:"required_strict"`
Address string `json:"address" validate:"required_strict"` Address string `json:"address" validate:"required_strict"`
Phone string `json:"phone" validate:"required_strict,max=20"` Phone string `json:"phone" validate:"required_strict,max=20"`
Email string `json:"email" validate:"required_strict,email,max=50"` Email string `json:"email" validate:"required_strict,email"`
AccountNumber string `json:"account_number" validate:"required_strict,max=50"` AccountNumber string `json:"account_number" validate:"required_strict"`
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"` PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
Type *string `json:"type,omitempty" validate:"omitempty,max=50"` Type *string `json:"type,omitempty" validate:"omitempty"`
Address *string `json:"address,omitempty" validate:"omitempty"` Address *string `json:"address,omitempty" validate:"omitempty"`
Phone *string `json:"phone,omitempty" validate:"omitempty,max=20"` Phone *string `json:"phone,omitempty" validate:"omitempty"`
Email *string `json:"email,omitempty" validate:"omitempty,max=50"` Email *string `json:"email,omitempty" validate:"omitempty"`
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"` AccountNumber *string `json:"account_number,omitempty" validate:"omitempty"`
} }
type Query struct { type Query struct {
+2 -2
View File
@@ -1,7 +1,7 @@
package kandangs package kandangs
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/master/kandangs/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/controllers"
kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/services" kandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/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 KandangRoutes(v1 fiber.Router, u user.UserService, s kandang.KandangService
ctrl := controller.NewKandangController(s) ctrl := controller.NewKandangController(s)
route := v1.Group("/kandangs") route := v1.Group("/kandangs")
route.Use(m.Auth(u)) // route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll) route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -3,13 +3,13 @@ package service
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service" common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"strings"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -130,18 +130,14 @@ func (s *kandangService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entit
} }
actorID, err := m.ActorIDFromContext(c) //TODO: created by dummy
if err != nil {
return nil, err
}
createBody := &entity.Kandang{ createBody := &entity.Kandang{
Name: req.Name, Name: req.Name,
LocationId: req.LocationId, LocationId: req.LocationId,
Capacity: req.Capacity, Capacity: req.Capacity,
Status: status, Status: status,
PicId: req.PicId, PicId: req.PicId,
CreatedBy: actorID, CreatedBy: 1,
} }
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -1,8 +1,8 @@
package validation package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3"`
Status string `json:"status,omitempty" validate:"omitempty,min=3,max=50"` Status string `json:"status,omitempty" validate:"omitempty,min=3"`
Capacity float64 `json:"capacity" validate:"required_strict,gt=0"` Capacity float64 `json:"capacity" validate:"required_strict,gt=0"`
LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"` LocationId uint `json:"location_id" validate:"required_strict,number,gt=0"`
PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"` PicId uint `json:"pic_id" validate:"required_strict,number,gt=0"`
@@ -10,8 +10,8 @@ type Create struct {
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
Status *string `json:"status,omitempty" validate:"omitempty,min=3,max=50"` Status *string `json:"status,omitempty" validate:"omitempty,min=3"`
Capacity *float64 `json:"capacity" validate:"omitempty,gt=0"` Capacity *float64 `json:"capacity" validate:"omitempty,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"` LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"` PicId *uint `json:"pic_id,omitempty" validate:"omitempty,number,gt=0"`
@@ -4,15 +4,15 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service" common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/locations/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -97,16 +97,12 @@ func (s *locationService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c) //TODO: created by dummy
if err != nil {
return nil, err
}
createBody := &entity.Location{ createBody := &entity.Location{
Name: req.Name, Name: req.Name,
Address: req.Address, Address: req.Address,
AreaId: req.AreaId, AreaId: req.AreaId,
CreatedBy: actorID, CreatedBy: 1,
} }
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -1,13 +1,13 @@
package validation package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3"`
Address string `json:"address" validate:"required_strict"` Address string `json:"address" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"` AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
Address *string `json:"address,omitempty" validate:"omitempty"` Address *string `json:"address,omitempty" validate:"omitempty"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"` AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
} }
@@ -1,14 +1,14 @@
package validation package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3"`
UomID uint `json:"uom_id" validate:"required,gt=0"` UomID uint `json:"uom_id" validate:"required,gt=0"`
SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"` SupplierIDs []uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags []string `json:"flags,omitempty" validate:"omitempty,dive,max=50"` Flags []string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=50"` Name *string `json:"name,omitempty" validate:"omitempty,min=3"`
UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"` UomID *uint `json:"uom_id,omitempty" validate:"omitempty,gt=0"`
SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"` SupplierIDs *[]uint `json:"supplier_ids,omitempty" validate:"omitempty,dive,gt=0"`
Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,max=50"` Flags *[]string `json:"flags,omitempty" validate:"omitempty,dive,max=50"`
@@ -1,12 +1,12 @@
package validation package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3"`
Code string `json:"code" validate:"required_strict,max=10"` Code string `json:"code" validate:"required_strict,max=10"`
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
Code *string `json:"code,omitempty" validate:"omitempty,max=10"` Code *string `json:"code,omitempty" validate:"omitempty,max=10"`
} }
@@ -12,13 +12,12 @@ import (
// === DTO Structs === // === DTO Structs ===
type ProductRelationDTO struct { type ProductRelationDTO struct {
Id uint `json:"id"` Id uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
ProductPrice float64 `gorm:"type:numeric(15,3);not null"` ProductPrice float64 `gorm:"type:numeric(15,3);not null"`
SellingPrice *float64 `gorm:"type:numeric(15,3)"` SellingPrice *float64 `gorm:"type:numeric(15,3)"`
Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"` Uom *uomDTO.UomRelationDTO `json:"uom,omitempty"`
Flags *[]string `json:"flags,omitempty"` Flags *[]string `json:"flags,omitempty"`
ProductCategory *productCategoryDTO.ProductCategoryRelationDTO `json:"product_category,omitempty"`
} }
type ProductListDTO struct { type ProductListDTO struct {
@@ -56,20 +55,13 @@ func ToProductRelationDTO(e entity.Product) ProductRelationDTO {
uomRef = &mapped uomRef = &mapped
} }
var categoryRef *productCategoryDTO.ProductCategoryRelationDTO
if e.ProductCategory.Id != 0 {
mapped := productCategoryDTO.ToProductCategoryRelationDTO(e.ProductCategory)
categoryRef = &mapped
}
return ProductRelationDTO{ return ProductRelationDTO{
Id: e.Id, Id: e.Id,
Name: e.Name, Name: e.Name,
ProductPrice: e.ProductPrice, ProductPrice: e.ProductPrice,
SellingPrice: e.SellingPrice, SellingPrice: e.SellingPrice,
Flags: &flags, Flags: &flags,
Uom: uomRef, Uom: uomRef,
ProductCategory: categoryRef,
} }
} }
@@ -1,9 +1,9 @@
package validation package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3"`
Brand string `json:"brand" validate:"required_strict,min=2,max=50"` Brand string `json:"brand" validate:"required_strict,min=2"`
Sku *string `json:"sku,omitempty" validate:"omitempty,max=100"` Sku *string `json:"sku,omitempty" validate:"omitempty"`
UomID uint `json:"uom_id" validate:"required,gt=0"` UomID uint `json:"uom_id" validate:"required,gt=0"`
ProductCategoryID uint `json:"product_category_id" validate:"required,gt=0"` ProductCategoryID uint `json:"product_category_id" validate:"required,gt=0"`
ProductPrice float64 `json:"product_price" validate:"required"` ProductPrice float64 `json:"product_price" validate:"required"`
+2 -2
View File
@@ -1,7 +1,7 @@
package suppliers package suppliers
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/master/suppliers/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/controllers"
supplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/services" supplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/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 SupplierRoutes(v1 fiber.Router, u user.UserService, s supplier.SupplierServ
ctrl := controller.NewSupplierController(s) ctrl := controller.NewSupplierController(s)
route := v1.Group("/suppliers") route := v1.Group("/suppliers")
route.Use(m.Auth(u)) // route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll) route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -5,14 +5,14 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -124,10 +124,8 @@ func (s *supplierService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
} }
alias := strings.TrimSpace(strings.ToUpper(req.Alias)) alias := strings.TrimSpace(strings.ToUpper(req.Alias))
actorID, err := m.ActorIDFromContext(c)
if err != nil { //TODO: created by dummy
return nil, err
}
createBody := &entity.Supplier{ createBody := &entity.Supplier{
Name: req.Name, Name: req.Name,
Alias: alias, Alias: alias,
@@ -141,7 +139,7 @@ func (s *supplierService) CreateOne(c *fiber.Ctx, req *validation.Create) (*enti
Npwp: req.Npwp, Npwp: req.Npwp,
AccountNumber: req.AccountNumber, AccountNumber: req.AccountNumber,
DueDate: req.DueDate, DueDate: req.DueDate,
CreatedBy: actorID, CreatedBy: 1,
} }
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -1,14 +1,14 @@
package validation package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3"`
Alias string `json:"alias" validate:"required_strict,max=5"` Alias string `json:"alias" validate:"required_strict,max=5"`
Pic string `json:"pic" validate:"required_strict,max=50"` Pic string `json:"pic" validate:"required_strict"`
Type string `json:"type" validate:"required_strict,max=50"` Type string `json:"type" validate:"required_strict"`
Category string `json:"category" validate:"required_strict,max=20"` Category string `json:"category" validate:"required_strict"`
Hatchery *string `json:"hatchery,omitempty" validate:"omitempty,max=50"` Hatchery *string `json:"hatchery,omitempty" validate:"omitempty"`
Phone string `json:"phone" validate:"required_strict,max=20"` Phone string `json:"phone" validate:"required_strict,max=20"`
Email string `json:"email" validate:"required_strict,email,max=50"` Email string `json:"email" validate:"required_strict,email"`
Address string `json:"address" validate:"required_strict"` Address string `json:"address" validate:"required_strict"`
Npwp *string `json:"npwp,omitempty" validate:"omitempty,max=50"` Npwp *string `json:"npwp,omitempty" validate:"omitempty,max=50"`
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"` AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"`
@@ -16,14 +16,14 @@ type Create struct {
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,min=3,max=50"` Name *string `json:"name,omitempty" validate:"omitempty,min=3"`
Alias *string `json:"alias,omitempty" validate:"omitempty,max=5"` Alias *string `json:"alias,omitempty" validate:"omitempty,max=5"`
Pic *string `json:"pic,omitempty" validate:"omitempty,max=50"` Pic *string `json:"pic,omitempty" validate:"omitempty"`
Type *string `json:"type,omitempty" validate:"omitempty,max=50"` Type *string `json:"type,omitempty" validate:"omitempty"`
Category *string `json:"category,omitempty" validate:"omitempty,max=20"` Category *string `json:"category,omitempty" validate:"omitempty"`
Hatchery *string `json:"hatchery,omitempty" validate:"omitempty,max=50"` Hatchery *string `json:"hatchery,omitempty" validate:"omitempty"`
Phone *string `json:"phone,omitempty" validate:"omitempty,max=20"` Phone *string `json:"phone,omitempty" validate:"omitempty,max=20"`
Email *string `json:"email,omitempty" validate:"omitempty,email,max=50"` Email *string `json:"email,omitempty" validate:"omitempty,email"`
Address *string `json:"address,omitempty" validate:"omitempty"` Address *string `json:"address,omitempty" validate:"omitempty"`
Npwp *string `json:"npwp,omitempty" validate:"omitempty,max=50"` Npwp *string `json:"npwp,omitempty" validate:"omitempty,max=50"`
AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"` AccountNumber *string `json:"account_number,omitempty" validate:"omitempty,max=50"`
@@ -4,14 +4,14 @@ import (
"errors" "errors"
"fmt" "fmt"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -87,13 +87,10 @@ func (s *uomService) CreateOne(c *fiber.Ctx, req *validation.Create) (*entity.Uo
return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Uom with name %s already exists", req.Name)) return nil, fiber.NewError(fiber.StatusConflict, fmt.Sprintf("Uom with name %s already exists", req.Name))
} }
actorID, err := m.ActorIDFromContext(c) //TODO: created by dummy
if err != nil {
return nil, err
}
createBody := &entity.Uom{ createBody := &entity.Uom{
Name: req.Name, Name: req.Name,
CreatedBy: actorID, CreatedBy: 1,
} }
if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil { if err := s.Repository.CreateOne(c.Context(), createBody, nil); err != nil {
@@ -1,11 +1,11 @@
package validation package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3"`
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
} }
type Query struct { type Query struct {
@@ -17,6 +17,7 @@ type WarehouseRepository interface {
IdExists(ctx context.Context, id uint) (bool, error) IdExists(ctx context.Context, id uint) (bool, error)
GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) GetByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error)
GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error)
} }
type WarehouseRepositoryImpl struct { type WarehouseRepositoryImpl struct {
@@ -62,6 +63,18 @@ func (r *WarehouseRepositoryImpl) GetByKandangID(ctx context.Context, kandangId
return &warehouse, nil return &warehouse, nil
} }
func (r *WarehouseRepositoryImpl) GetDetailByID(ctx context.Context, id uint) (*entity.Warehouse, error) {
var warehouse entity.Warehouse
err := r.db.WithContext(ctx).
Preload("Area").
Preload("Location").
First(&warehouse, id).Error
if err != nil {
return nil, err
}
return &warehouse, nil
}
func (r *WarehouseRepositoryImpl) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) { func (r *WarehouseRepositoryImpl) GetLatestByKandangID(ctx context.Context, kandangId uint) (*entity.Warehouse, error) {
var warehouse entity.Warehouse var warehouse entity.Warehouse
err := r.db.WithContext(ctx). err := r.db.WithContext(ctx).
@@ -3,13 +3,13 @@ package service
import ( import (
"errors" "errors"
"fmt" "fmt"
"strings"
common "gitlab.com/mbugroup/lti-api.git/internal/common/service" common "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"strings"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -105,15 +105,13 @@ func (s *warehouseService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
); err != nil { ); err != nil {
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c)
if err != nil { //TODO: created by dummy
return nil, err
}
createBody := &entity.Warehouse{ createBody := &entity.Warehouse{
Name: req.Name, Name: req.Name,
Type: typ, Type: typ,
AreaId: req.AreaId, AreaId: req.AreaId,
CreatedBy: actorID, CreatedBy: 1,
} }
if req.LocationId != nil { if req.LocationId != nil {
createBody.LocationId = req.LocationId createBody.LocationId = req.LocationId
@@ -1,16 +1,16 @@
package validation package validation
type Create struct { type Create struct {
Name string `json:"name" validate:"required_strict,min=3,max=50"` Name string `json:"name" validate:"required_strict,min=3"`
Type string `json:"type" validate:"required_strict,max=50"` Type string `json:"type" validate:"required_strict"`
AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"` AreaId uint `json:"area_id" validate:"required_strict,number,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"` LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty,number,gt=0"` KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty,number,gt=0"`
} }
type Update struct { type Update struct {
Name *string `json:"name,omitempty" validate:"omitempty,max=50"` Name *string `json:"name,omitempty" validate:"omitempty"`
Type *string `json:"type,omitempty" validate:"omitempty,max=50"` Type *string `json:"type,omitempty" validate:"omitempty"`
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"` AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"` LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty,number,gt=0"` KandangId *uint `json:"kandang_id,omitempty" validate:"omitempty,number,gt=0"`
@@ -8,7 +8,6 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
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"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
@@ -126,10 +125,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category)) category := strings.ToUpper(strings.TrimSpace(projectFlockKandang.ProjectFlock.Category))
actorID, err := m.ActorIDFromContext(c) actorID := uint(1) // todo nanti ambil dari auth context
if err != nil {
return nil, err
}
newChikins := make([]*entity.ProjectChickin, 0) newChikins := make([]*entity.ProjectChickin, 0)
for _, chickinReq := range req.ChickinRequests { for _, chickinReq := range req.ChickinRequests {
@@ -360,11 +356,6 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB())) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(s.Repository.DB()))
var action entity.ApprovalAction var action entity.ApprovalAction
@@ -406,13 +397,14 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
step = utils.ProjectFlockKandangStepDisetujui step = utils.ProjectFlockKandangStepDisetujui
} }
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
chickinRepoTx := repository.NewChickinRepository(dbTransaction) chickinRepoTx := repository.NewChickinRepository(dbTransaction)
productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction) productWarehouseTx := s.ProductWarehouseRepo.WithTx(dbTransaction)
for _, approvableID := range approvableIDs { for _, approvableID := range approvableIDs {
actorID := uint(1) // todo nanti ambil dari auth context
if _, err := approvalSvc.CreateApproval( if _, err := approvalSvc.CreateApproval(
c.Context(), c.Context(),
utils.ApprovalWorkflowProjectFlockKandang, utils.ApprovalWorkflowProjectFlockKandang,
@@ -1,7 +1,7 @@
package project_flock_kandangs package project_flock_kandangs
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/production/project-flock-kandangs/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/controllers"
projectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services" projectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/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 ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlo
ctrl := controller.NewProjectFlockKandangController(s) ctrl := controller.NewProjectFlockKandangController(s)
route := v1.Group("/project-flock-kandangs") route := v1.Group("/project-flock-kandangs")
route.Use(m.Auth(u))
// route.Get("/", m.Auth(u), ctrl.GetAll) // route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne) // route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne) // route.Get("/:id", m.Auth(u), ctrl.GetOne)
@@ -1,23 +0,0 @@
package repository
import (
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
"gorm.io/gorm"
)
type ProjectBudgetRepository interface {
repository.BaseRepository[entity.ProjectBudget]
}
type ProjectBudgetRepositoryImpl struct {
*repository.BaseRepositoryImpl[entity.ProjectBudget]
db *gorm.DB
}
func NewProjectBudgetRepository(db *gorm.DB) ProjectBudgetRepository {
return &ProjectBudgetRepositoryImpl{
BaseRepositoryImpl: repository.NewBaseRepository[entity.ProjectBudget](db),
db: db,
}
}
@@ -11,6 +11,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
type ProjectflockRepository interface { type ProjectflockRepository interface {
repository.BaseRepository[entity.ProjectFlock] repository.BaseRepository[entity.ProjectFlock]
GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error)
@@ -41,23 +42,24 @@ func NewProjectflockRepository(db *gorm.DB) ProjectflockRepository {
func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) { func (r *ProjectflockRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, params *validation.Query) ([]entity.ProjectFlock, int64, error) {
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB { return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
return r.applyQueryFilters(r.WithDefaultRelations()(db), params) return r.applyQueryFilters(db, params)
}) })
} }
func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB { func (r *ProjectflockRepositoryImpl) WithDefaultRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB {
return db. return db.
Preload("CreatedUser"). Preload("CreatedUser").
Preload("Area"). Preload("Area").
Preload("Fcr"). Preload("Fcr").
Preload("Location"). Preload("Location").
Preload("Kandangs"). Preload("Kandangs").
Preload("KandangHistory"). Preload("KandangHistory").
Preload("KandangHistory.Kandang") Preload("KandangHistory.Kandang")
} }
} }
func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *validation.Query) *gorm.DB { func (r *ProjectflockRepositoryImpl) applyQueryFilters(db *gorm.DB, params *validation.Query) *gorm.DB {
if params == nil { if params == nil {
return db return db
@@ -1,7 +1,7 @@
package project_flocks package project_flocks
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/production/project_flocks/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/controllers"
projectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/services" projectflock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/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 ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
ctrl := controller.NewProjectflockController(s) ctrl := controller.NewProjectflockController(s)
route := v1.Group("/project-flocks") route := v1.Group("/project-flocks")
route.Use(m.Auth(u)) // route.Use(m.Auth(u))
route.Get("/", ctrl.GetAll) route.Get("/", ctrl.GetAll)
route.Post("/", ctrl.CreateOne) route.Post("/", ctrl.CreateOne)
@@ -10,7 +10,8 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
// authmiddleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" productWarehouseRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto" flockDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/dto"
flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories" flockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/master/flocks/repositories"
@@ -84,17 +85,18 @@ func NewProjectflockService(
} }
} }
func (s projectflockService) approvalQueryModifier() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return db.Preload("ActionUser")
}
}
func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error) { func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error) {
if err := s.Validate.Struct(params); err != nil { if err := s.Validate.Struct(params); err != nil {
return nil, 0, nil, err return nil, 0, nil, err
} }
if params.Page <= 0 {
params.Page = 1
}
if params.Limit <= 0 {
params.Limit = 10
}
offset := (params.Page - 1) * params.Limit offset := (params.Page - 1) * params.Limit
projectflocks, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params) projectflocks, total, err := s.Repository.GetAllWithFilters(c.Context(), offset, params.Limit, params)
@@ -110,7 +112,7 @@ func (s projectflockService) GetAll(c *fiber.Ctx, params *validation.Query) ([]e
ids[i] = item.Id ids[i] = item.Id
} }
latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, s.approvalQueryModifier()) latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), s.approvalWorkflow, ids, s.Repository.WithDefaultRelations())
if err != nil { if err != nil {
s.Log.Warnf("Unable to load latest approvals for projectflocks: %+v", err) s.Log.Warnf("Unable to load latest approvals for projectflocks: %+v", err)
} else if len(latestMap) > 0 { } else if len(latestMap) > 0 {
@@ -154,7 +156,7 @@ func (s projectflockService) getOneEntityOnly(c *fiber.Ctx, id uint) (*entity.Pr
} }
if s.ApprovalSvc != nil { if s.ApprovalSvc != nil {
approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier()) approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.Repository.WithDefaultRelations())
if err != nil { if err != nil {
s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err) s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err)
} else if len(approvals) > 0 { } else if len(approvals) > 0 {
@@ -181,7 +183,7 @@ func (s projectflockService) GetOne(c *fiber.Ctx, id uint) (*entity.ProjectFlock
} }
if s.ApprovalSvc != nil { if s.ApprovalSvc != nil {
approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.approvalQueryModifier()) approvals, err := s.ApprovalSvc.ListByTarget(c.Context(), s.approvalWorkflow, id, s.Repository.WithDefaultRelations())
if err != nil { if err != nil {
s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err) s.Log.Warnf("Unable to load approvals for projectflock %d: %+v", id, err)
} else if len(approvals) > 0 { } else if len(approvals) > 0 {
@@ -219,7 +221,7 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c) actorID, err := actorIDFromContext(c)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -342,7 +344,7 @@ func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c) actorID, err := actorIDFromContext(c)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -600,7 +602,7 @@ func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c) actorID, err := actorIDFromContext(c)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -845,7 +847,7 @@ func (s projectflockService) GetPeriodSummary(c *fiber.Ctx, locationID uint) ([]
summaries := make([]KandangPeriodSummary, 0, len(rows)) summaries := make([]KandangPeriodSummary, 0, len(rows))
for _, row := range rows { for _, row := range rows {
nextPeriod := 1 nextPeriod := 0
if row.LatestPeriod > 0 { if row.LatestPeriod > 0 {
nextPeriod = row.LatestPeriod + 1 nextPeriod = row.LatestPeriod + 1
} }
@@ -1044,3 +1046,12 @@ func (s projectflockService) kandangRepoWithTx(tx *gorm.DB) kandangRepository.Ka
} }
return kandangRepository.NewKandangRepository(s.Repository.DB()) return kandangRepository.NewKandangRepository(s.Repository.DB())
} }
func actorIDFromContext(_ *fiber.Ctx) (uint, error) {
// user, ok := authmiddleware.AuthenticatedUser(c)
// if !ok || user == nil || user.Id == 0 {
// return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
// }
// return user.Id, nil
return 1, nil
}
@@ -2,7 +2,6 @@ package recordings
import ( import (
"fmt" "fmt"
"strings"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -15,7 +14,6 @@ import (
rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" rRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services" sRecording "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/services"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories" rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services" sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
@@ -28,25 +26,6 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db) projectFlockKandangRepo := rProjectFlock.NewProjectFlockKandangRepository(db)
projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db) projectFlockPopulationRepo := rProjectFlock.NewProjectFlockPopulationRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
stockAllocationRepo := commonRepo.NewStockAllocationRepository(db)
fifoService := commonSvc.NewFifoService(db, stockAllocationRepo, productWarehouseRepo, utils.Log)
if err := fifoService.RegisterUsable(fifo.UsableConfig{
Key: fifo.UsableKeyRecordingStock,
Table: "recording_stocks",
Columns: fifo.UsableColumns{
ID: "id",
ProductWarehouseID: "product_warehouse_id",
UsageQuantity: "usage_qty",
PendingQuantity: "pending_qty",
CreatedAt: "created_at",
},
}); err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "already registered") {
panic(fmt.Sprintf("failed to register recording usable workflow: %v", err))
}
}
approvalRepo := commonRepo.NewApprovalRepository(db) approvalRepo := commonRepo.NewApprovalRepository(db)
approvalService := commonSvc.NewApprovalService(approvalRepo) approvalService := commonSvc.NewApprovalService(approvalRepo)
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowRecording, utils.RecordingApprovalSteps); err != nil { if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowRecording, utils.RecordingApprovalSteps); err != nil {
@@ -62,7 +41,6 @@ func (RecordingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
projectFlockPopulationRepo, projectFlockPopulationRepo,
approvalRepo, approvalRepo,
approvalService, approvalService,
fifoService,
validate, validate,
) )
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
@@ -25,7 +25,6 @@ type RecordingRepository interface {
CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error CreateStocks(tx *gorm.DB, stocks []entity.RecordingStock) error
DeleteStocks(tx *gorm.DB, recordingID uint) error DeleteStocks(tx *gorm.DB, recordingID uint) error
ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error) ListStocks(tx *gorm.DB, recordingID uint) ([]entity.RecordingStock, error)
UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error
CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error
DeleteDepletions(tx *gorm.DB, recordingID uint) error DeleteDepletions(tx *gorm.DB, recordingID uint) error
@@ -121,15 +120,6 @@ func (r *RecordingRepositoryImpl) ListStocks(tx *gorm.DB, recordingID uint) ([]e
return items, nil return items, nil
} }
func (r *RecordingRepositoryImpl) UpdateStockUsage(tx *gorm.DB, stockID uint, usageQty, pendingQty float64) error {
return tx.Model(&entity.RecordingStock{}).
Where("id = ?", stockID).
Updates(map[string]any{
"usage_qty": usageQty,
"pending_qty": pendingQty,
}).Error
}
func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error { func (r *RecordingRepositoryImpl) CreateDepletions(tx *gorm.DB, depletions []entity.RecordingDepletion) error {
if len(depletions) == 0 { if len(depletions) == 0 {
return nil return nil
@@ -4,21 +4,20 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math"
"strings"
"time"
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
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"
rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" rProjectFlock "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings/validations"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals" approvalutils "gitlab.com/mbugroup/lti-api.git/internal/utils/approvals"
"gitlab.com/mbugroup/lti-api.git/internal/utils/fifo"
recordingutil "gitlab.com/mbugroup/lti-api.git/internal/utils/recording" recordingutil "gitlab.com/mbugroup/lti-api.git/internal/utils/recording"
"math"
"strings"
"time"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
@@ -37,13 +36,6 @@ type RecordingService interface {
Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error) Approval(ctx *fiber.Ctx, req *validation.Approve) ([]entity.Recording, error)
} }
type RecordingFIFOIntegrationService interface {
ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error
ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error
}
var recordingStockUsableKey = fifo.UsableKeyRecordingStock
type recordingService struct { type recordingService struct {
Log *logrus.Logger Log *logrus.Logger
Validate *validator.Validate Validate *validator.Validate
@@ -53,7 +45,6 @@ type recordingService struct {
ProjectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository ProjectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository
ApprovalRepo commonRepo.ApprovalRepository ApprovalRepo commonRepo.ApprovalRepository
ApprovalSvc commonSvc.ApprovalService ApprovalSvc commonSvc.ApprovalService
FifoSvc commonSvc.FifoService
} }
func NewRecordingService( func NewRecordingService(
@@ -63,7 +54,6 @@ func NewRecordingService(
projectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository, projectFlockPopulationRepo rProjectFlock.ProjectFlockPopulationRepository,
approvalRepo commonRepo.ApprovalRepository, approvalRepo commonRepo.ApprovalRepository,
approvalSvc commonSvc.ApprovalService, approvalSvc commonSvc.ApprovalService,
fifoSvc commonSvc.FifoService,
validate *validator.Validate, validate *validator.Validate,
) RecordingService { ) RecordingService {
return &recordingService{ return &recordingService{
@@ -75,20 +65,6 @@ func NewRecordingService(
ProjectFlockPopulationRepo: projectFlockPopulationRepo, ProjectFlockPopulationRepo: projectFlockPopulationRepo,
ApprovalRepo: approvalRepo, ApprovalRepo: approvalRepo,
ApprovalSvc: approvalSvc, ApprovalSvc: approvalSvc,
FifoSvc: fifoSvc,
}
}
func NewRecordingFIFOIntegrationService(
repo repository.RecordingRepository,
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
fifoSvc commonSvc.FifoService,
) RecordingFIFOIntegrationService {
return &recordingService{
Log: utils.Log,
Repository: repo,
ProductWarehouseRepo: productWarehouseRepo,
FifoSvc: fifoSvc,
} }
} }
@@ -193,10 +169,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions, req.Eggs); err != nil { if err := s.ensureProductWarehousesExist(c, req.Stocks, req.Depletions, req.Eggs); err != nil {
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
var createdRecording entity.Recording var createdRecording entity.Recording
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
nextDay, err := s.Repository.GenerateNextDay(tx, req.ProjectFlockKandangId) nextDay, err := s.Repository.GenerateNextDay(tx, req.ProjectFlockKandangId)
@@ -220,7 +193,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
ProjectFlockKandangId: req.ProjectFlockKandangId, ProjectFlockKandangId: req.ProjectFlockKandangId,
RecordDatetime: recordTime, RecordDatetime: recordTime,
Day: &day, Day: &day,
CreatedBy: actorID, CreatedBy: 1, // TODO: replace with authenticated user
} }
if err := s.Repository.CreateOne(ctx, &createdRecording, func(*gorm.DB) *gorm.DB { return tx }); err != nil { if err := s.Repository.CreateOne(ctx, &createdRecording, func(*gorm.DB) *gorm.DB { return tx }); err != nil {
@@ -246,10 +219,6 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
return err return err
} }
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil {
return err
}
mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions) mappedDepletions := recordingutil.MapDepletions(createdRecording.Id, req.Depletions)
if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil { if err := s.Repository.CreateDepletions(tx, mappedDepletions); err != nil {
s.Log.Errorf("Failed to persist depletions: %+v", err) s.Log.Errorf("Failed to persist depletions: %+v", err)
@@ -262,7 +231,7 @@ func (s *recordingService) CreateOne(c *fiber.Ctx, req *validation.Create) (*ent
return err return err
} }
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, nil, nil, mappedEggs)); err != nil { if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, mappedDepletions, nil, mappedStocks, nil, mappedEggs)); err != nil {
s.Log.Errorf("Failed to adjust product warehouses: %+v", err) s.Log.Errorf("Failed to adjust product warehouses: %+v", err)
return err return err
} }
@@ -375,10 +344,6 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
return err return err
} }
if err := s.releaseRecordingStocks(ctx, tx, existingStocks); err != nil {
return err
}
if err := s.Repository.DeleteStocks(tx, recordingEntity.Id); err != nil { if err := s.Repository.DeleteStocks(tx, recordingEntity.Id); err != nil {
s.Log.Errorf("Failed to clear stocks: %+v", err) s.Log.Errorf("Failed to clear stocks: %+v", err)
return err return err
@@ -390,7 +355,8 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
return err return err
} }
if err := s.consumeRecordingStocks(ctx, tx, mappedStocks); err != nil { if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(nil, nil, existingStocks, mappedStocks, nil, nil)); err != nil {
s.Log.Errorf("Failed to adjust product warehouses for stocks: %+v", err)
return err return err
} }
} }
@@ -456,7 +422,7 @@ func (s recordingService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uin
action := entity.ApprovalActionUpdated action := entity.ApprovalActionUpdated
actorID := recordingEntity.CreatedBy actorID := recordingEntity.CreatedBy
if actorID == 0 { if actorID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Actor Id tidak valid untuk approval") actorID = 1
} }
var step approvalutils.ApprovalStep var step approvalutils.ApprovalStep
@@ -647,10 +613,7 @@ func (s recordingService) Approval(c *fiber.Ctx, req *validation.Approve) ([]ent
} }
ctx := c.Context() ctx := c.Context()
actorID, err := m.ActorIDFromContext(c) actorID := uint(1) // TODO: replace with authenticated user once auth is integrated
if err != nil {
return nil, err
}
transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error { transactionErr := s.Repository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
repoTx := s.Repository.WithTx(tx) repoTx := s.Repository.WithTx(tx)
@@ -722,11 +685,7 @@ func (s recordingService) DeleteOne(c *fiber.Ctx, id uint) error {
return err return err
} }
if err := s.releaseRecordingStocks(ctx, tx, oldStocks); err != nil { if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, oldStocks, nil, oldEggs, nil)); err != nil {
return err
}
if err := s.adjustProductWarehouseQuantities(ctx, tx, buildWarehouseDeltas(oldDepletions, nil, nil, nil, oldEggs, nil)); err != nil {
return err return err
} }
@@ -781,77 +740,6 @@ func (s *recordingService) ensureProductWarehousesExist(c *fiber.Ctx, stocks []v
return nil return nil
} }
func (s *recordingService) consumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
if len(stocks) == 0 || s.FifoSvc == nil {
return nil
}
for _, stock := range stocks {
if stock.Id == 0 {
continue
}
var desired float64
if stock.UsageQty != nil {
desired = *stock.UsageQty
}
result, err := s.FifoSvc.Consume(ctx, commonSvc.StockConsumeRequest{
UsableKey: recordingStockUsableKey,
UsableID: stock.Id,
ProductWarehouseID: stock.ProductWarehouseId,
Quantity: desired,
AllowPending: true,
Tx: tx,
})
if err != nil {
s.Log.Errorf("Failed to consume FIFO stock for recording stock %d: %+v", stock.Id, err)
return err
}
if err := s.Repository.UpdateStockUsage(tx, stock.Id, result.UsageQuantity, result.PendingQuantity); err != nil {
return err
}
}
return nil
}
func (s *recordingService) ConsumeRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
return s.consumeRecordingStocks(ctx, tx, stocks)
}
func (s *recordingService) releaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
if len(stocks) == 0 || s.FifoSvc == nil {
return nil
}
for _, stock := range stocks {
if stock.Id == 0 {
continue
}
if err := s.FifoSvc.ReleaseUsage(ctx, commonSvc.StockReleaseRequest{
UsableKey: recordingStockUsableKey,
UsableID: stock.Id,
Tx: tx,
}); err != nil {
s.Log.Errorf("Failed to release FIFO stock for recording stock %d: %+v", stock.Id, err)
return err
}
if err := s.Repository.UpdateStockUsage(tx, stock.Id, 0, 0); err != nil {
return err
}
}
return nil
}
func (s *recordingService) ReleaseRecordingStocks(ctx context.Context, tx *gorm.DB, stocks []entity.RecordingStock) error {
return s.releaseRecordingStocks(ctx, tx, stocks)
}
func buildWarehouseDeltas( func buildWarehouseDeltas(
oldDepletions, newDepletions []entity.RecordingDepletion, oldDepletions, newDepletions []entity.RecordingDepletion,
oldStocks, newStocks []entity.RecordingStock, oldStocks, newStocks []entity.RecordingStock,
@@ -864,6 +752,12 @@ func buildWarehouseDeltas(
for _, item := range newDepletions { for _, item := range newDepletions {
accumulateWarehouseDelta(deltas, item.ProductWarehouseId, item.Qty) accumulateWarehouseDelta(deltas, item.ProductWarehouseId, item.Qty)
} }
for _, item := range oldStocks {
accumulateWarehouseDelta(deltas, item.ProductWarehouseId, usageQtyValue(item.UsageQty))
}
for _, item := range newStocks {
accumulateWarehouseDelta(deltas, item.ProductWarehouseId, -usageQtyValue(item.UsageQty))
}
for _, item := range oldEggs { for _, item := range oldEggs {
accumulateWarehouseDelta(deltas, item.ProductWarehouseId, -float64(item.Qty)) accumulateWarehouseDelta(deltas, item.ProductWarehouseId, -float64(item.Qty))
} }
@@ -873,6 +767,13 @@ func buildWarehouseDeltas(
return deltas return deltas
} }
func usageQtyValue(val *float64) float64 {
if val == nil {
return 0
}
return *val
}
func accumulateWarehouseDelta(deltas map[uint]float64, id uint, value float64) { func accumulateWarehouseDelta(deltas map[uint]float64, id uint, value float64) {
if id == 0 || value == 0 { if id == 0 || value == 0 {
return return
@@ -1050,7 +951,7 @@ func (s *recordingService) createRecordingApproval(
return fiber.NewError(fiber.StatusBadRequest, "Recording tidak valid untuk approval") return fiber.NewError(fiber.StatusBadRequest, "Recording tidak valid untuk approval")
} }
if actorID == 0 { if actorID == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Actor Id tidak valid untuk approval") actorID = 1
} }
var svc commonSvc.ApprovalService var svc commonSvc.ApprovalService
@@ -1,7 +1,7 @@
package transfer_layings package transfer_layings
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/production/transfer_layings/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/controllers"
transferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/services" transferLaying "gitlab.com/mbugroup/lti-api.git/internal/modules/production/transfer_layings/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 TransferLayingRoutes(v1 fiber.Router, u user.UserService, s transferLaying.
ctrl := controller.NewTransferLayingController(s) ctrl := controller.NewTransferLayingController(s)
route := v1.Group("/transfer_layings") route := v1.Group("/transfer_layings")
route.Use(m.Auth(u))
// route.Get("/", m.Auth(u), ctrl.GetAll) // route.Get("/", m.Auth(u), ctrl.GetAll)
// route.Post("/", m.Auth(u), ctrl.CreateOne) // route.Post("/", m.Auth(u), ctrl.CreateOne)
// route.Get("/:id", m.Auth(u), ctrl.GetOne) // route.Get("/:id", m.Auth(u), ctrl.GetOne)
@@ -10,7 +10,6 @@ import (
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository" commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service" commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
entity "gitlab.com/mbugroup/lti-api.git/internal/entities" entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rInventory "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/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"
ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories" ProjectFlockRepository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
@@ -155,11 +154,6 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c)
if err != nil {
return nil, err
}
if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.SourceProjectFlockId, nil); err != nil { if _, err := s.ProjectFlockRepo.GetByID(c.Context(), req.SourceProjectFlockId, nil); err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) { if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fiber.NewError(fiber.StatusNotFound, "Source Project Flock not found") return nil, fiber.NewError(fiber.StatusNotFound, "Source Project Flock not found")
@@ -265,7 +259,7 @@ func (s *transferLayingService) CreateOne(c *fiber.Ctx, req *validation.Create)
ToProjectFlockId: req.TargetProjectFlockId, ToProjectFlockId: req.TargetProjectFlockId,
TransferDate: transferDate, TransferDate: transferDate,
PendingUsageQty: &totalSourceQty, PendingUsageQty: &totalSourceQty,
CreatedBy: actorID, CreatedBy: 1, //todo : harus diambil dari auth
} }
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
@@ -598,11 +592,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
return nil, err return nil, err
} }
actorID, err := m.ActorIDFromContext(c) actorID := uint(1) // TODO: change from auth context
if err != nil {
return nil, err
}
var action entity.ApprovalAction var action entity.ApprovalAction
switch strings.ToUpper(strings.TrimSpace(req.Action)) { switch strings.ToUpper(strings.TrimSpace(req.Action)) {
case string(entity.ApprovalActionRejected): case string(entity.ApprovalActionRejected):
@@ -623,7 +613,7 @@ func (s transferLayingService) Approval(c *fiber.Ctx, req *validation.Approve) (
step = utils.TransferToLayingStepDisetujui step = utils.TransferToLayingStepDisetujui
} }
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error { err := s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction)) approvalSvcTx := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction) sourceRepoTx := repository.NewLayingTransferSourceRepository(dbTransaction)
@@ -23,19 +23,21 @@ func NewPurchaseController(s service.PurchaseService) *PurchaseController {
} }
func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error { func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error {
query := &validation.Query{ query := &validation.PurchaseQuery{
Page: c.QueryInt("page", 1), Page: c.QueryInt("page", 1),
Limit: c.QueryInt("limit", 10), Limit: c.QueryInt("limit", 10),
Search: strings.TrimSpace(c.Query("search")),
PrNumber: strings.TrimSpace(c.Query("pr_number")),
CreatedFrom: strings.TrimSpace(c.Query("created_from")), CreatedFrom: strings.TrimSpace(c.Query("created_from")),
CreatedTo: strings.TrimSpace(c.Query("created_to")), CreatedTo: strings.TrimSpace(c.Query("created_to")),
SupplierID: uint(c.QueryInt("supplier_id", 0)),
AreaID: uint(c.QueryInt("area_id", 0)),
LocationID: uint(c.QueryInt("location_id", 0)),
ProductCategoryID: uint(c.QueryInt("product_category_id", 0)),
} }
if query.Page < 1 || query.Limit < 1 { if supplierID := c.QueryInt("supplier_id", 0); supplierID > 0 {
return fiber.NewError(fiber.StatusBadRequest, "page and limit must be greater than 0") query.SupplierID = uint(supplierID)
}
if status := strings.TrimSpace(c.Query("status")); status != "" {
query.Status = strings.ToUpper(status)
} }
results, total, err := ctrl.service.GetAll(c, query) results, total, err := ctrl.service.GetAll(c, query)
@@ -43,15 +45,24 @@ func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error {
return err return err
} }
limit := query.Limit
if limit <= 0 {
limit = 10
}
page := query.Page
if page <= 0 {
page = 1
}
return c.Status(fiber.StatusOK). return c.Status(fiber.StatusOK).
JSON(response.SuccessWithPaginate[dto.PurchaseListDTO]{ JSON(response.SuccessWithPaginate[dto.PurchaseListItemDTO]{
Code: fiber.StatusOK, Code: fiber.StatusOK,
Status: "success", Status: "success",
Message: "Purchase fetched successfully", Message: "Purchase fetched successfully",
Meta: response.Meta{ Meta: response.Meta{
Page: query.Page, Page: page,
Limit: query.Limit, Limit: limit,
TotalPages: int64(math.Ceil(float64(total) / float64(query.Limit))), TotalPages: int64(math.Ceil(float64(total) / float64(limit))),
TotalResults: total, TotalResults: total,
}, },
Data: dto.ToPurchaseListDTOs(results), Data: dto.ToPurchaseListDTOs(results),
@@ -60,13 +71,12 @@ func (ctrl *PurchaseController) GetAll(c *fiber.Ctx) error {
func (ctrl *PurchaseController) GetOne(c *fiber.Ctx) error { func (ctrl *PurchaseController) GetOne(c *fiber.Ctx) error {
param := c.Params("id") param := c.Params("id")
id, err := strconv.ParseUint(param, 10, 64)
id, err := strconv.Atoi(param)
if err != nil || id == 0 { if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id") return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
} }
result, err := ctrl.service.GetOne(c, uint(id)) result, err := ctrl.service.GetOne(c, id)
if err != nil { if err != nil {
return err return err
} }
@@ -103,7 +113,7 @@ func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error {
func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error { func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error {
param := c.Params("id") param := c.Params("id")
id, err := strconv.Atoi(param) id, err := strconv.ParseUint(param, 10, 64)
if err != nil || id == 0 { if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id") return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
} }
@@ -113,7 +123,7 @@ func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid request body: %v", err)) return fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid request body: %v", err))
} }
result, err := ctrl.service.ApproveStaffPurchase(c, uint(id), req) result, err := ctrl.service.ApproveStaffPurchase(c, id, req)
if err != nil { if err != nil {
return err return err
} }
@@ -127,9 +137,10 @@ func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error {
}) })
} }
func (ctrl *PurchaseController) ApproveManagerPurchase(c *fiber.Ctx) error { func (ctrl *PurchaseController) ApproveManagerPurchase(c *fiber.Ctx) error {
param := c.Params("id") param := c.Params("id")
id, err := strconv.Atoi(param) id, err := strconv.ParseUint(param, 10, 64)
if err != nil || id == 0 { if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id") return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
} }
@@ -139,7 +150,7 @@ func (ctrl *PurchaseController) ApproveManagerPurchase(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
} }
result, err := ctrl.service.ApproveManagerPurchase(c, uint(id), req) result, err := ctrl.service.ApproveManagerPurchase(c, id, req)
if err != nil { if err != nil {
return err return err
} }
@@ -155,7 +166,7 @@ func (ctrl *PurchaseController) ApproveManagerPurchase(c *fiber.Ctx) error {
func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error { func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
param := c.Params("id") param := c.Params("id")
id, err := strconv.Atoi(param) id, err := strconv.ParseUint(param, 10, 64)
if err != nil || id == 0 { if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id") return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
} }
@@ -165,7 +176,7 @@ func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
} }
result, err := ctrl.service.ReceiveProducts(c, uint(id), req) result, err := ctrl.service.ReceiveProducts(c, id, req)
if err != nil { if err != nil {
return err return err
} }
@@ -181,7 +192,7 @@ func (ctrl *PurchaseController) ReceiveProducts(c *fiber.Ctx) error {
func (ctrl *PurchaseController) DeleteItems(c *fiber.Ctx) error { func (ctrl *PurchaseController) DeleteItems(c *fiber.Ctx) error {
param := c.Params("id") param := c.Params("id")
id, err := strconv.Atoi(param) id, err := strconv.ParseUint(param, 10, 64)
if err != nil || id == 0 { if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id") return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
} }
@@ -191,7 +202,7 @@ func (ctrl *PurchaseController) DeleteItems(c *fiber.Ctx) error {
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body") return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
} }
result, err := ctrl.service.DeleteItems(c, uint(id), req) result, err := ctrl.service.DeleteItems(c, id, req)
if err != nil { if err != nil {
return err return err
} }
@@ -207,12 +218,12 @@ func (ctrl *PurchaseController) DeleteItems(c *fiber.Ctx) error {
func (ctrl *PurchaseController) DeletePurchase(c *fiber.Ctx) error { func (ctrl *PurchaseController) DeletePurchase(c *fiber.Ctx) error {
param := c.Params("id") param := c.Params("id")
id, err := strconv.Atoi(param) id, err := strconv.ParseUint(param, 10, 64)
if err != nil || id == 0 { if err != nil || id == 0 {
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id") return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
} }
if err := ctrl.service.DeletePurchase(c, uint(id)); err != nil { if err := ctrl.service.DeletePurchase(c, id); err != nil {
return err return err
} }
+84 -111
View File
@@ -10,51 +10,46 @@ import (
productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto" productDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/dto"
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto" supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto" warehouseDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/dto"
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
) )
type PurchaseRelationDTO struct { type PurchaseListItemDTO struct {
Id uint `json:"id"` Id uint64 `json:"id"`
PrNumber string `json:"pr_number"` PrNumber string `json:"pr_number"`
PoNumber *string `json:"po_number"` PoNumber *string `json:"po_number"`
PoDate *time.Time `json:"po_date"` Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
Notes *string `json:"notes"` CreditTerm *int `json:"credit_term"`
} DueDate *time.Time `json:"due_date"`
PoDate *time.Time `json:"po_date"`
GrandTotal float64 `json:"grand_total"`
type PurchaseListDTO struct { Notes *string `json:"notes"`
PurchaseRelationDTO CreatedAt time.Time `json:"created_at"`
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"` UpdatedAt time.Time `json:"updated_at"`
CreditTerm *int `json:"credit_term"` Approval *approvalDTO.ApprovalRelationDTO `json:"approval"`
DueDate *time.Time `json:"due_date"`
GrandTotal float64 `json:"grand_total"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
} }
type PurchaseDetailDTO struct { type PurchaseDetailDTO struct {
PurchaseRelationDTO Id uint64 `json:"id"`
Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"` PrNumber string `json:"pr_number"`
CreditTerm *int `json:"credit_term"` PoNumber *string `json:"po_number"`
DueDate *time.Time `json:"due_date"` Supplier *supplierDTO.SupplierRelationDTO `json:"supplier"`
GrandTotal float64 `json:"grand_total"` CreditTerm *int `json:"credit_term"`
Items []PurchaseItemDTO `json:"items"` DueDate *time.Time `json:"due_date"`
CreatedUser *userDTO.UserRelationDTO `json:"created_user"` PoDate *time.Time `json:"po_date"`
CreatedAt time.Time `json:"created_at"` GrandTotal float64 `json:"grand_total"`
UpdatedAt time.Time `json:"updated_at"` Notes *string `json:"notes"`
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"` Items []PurchaseItemDTO `json:"items"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
Approval *approvalDTO.ApprovalRelationDTO `json:"approval"`
} }
type PurchaseItemDTO struct { type PurchaseItemDTO struct {
Id uint `json:"id"` Id uint64 `json:"id"`
ProductID uint `json:"product_id"` ProductID uint64 `json:"product_id"`
Product *productDTO.ProductRelationDTO `json:"product"` Product *productDTO.ProductRelationDTO `json:"product"`
WarehouseID uint `json:"warehouse_id"` WarehouseID uint64 `json:"warehouse_id"`
Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse"` Warehouse *warehouseDTO.WarehouseRelationDTO `json:"warehouse"`
ProductWarehouseID *uint `json:"product_warehouse_id"` ProductWarehouseID *uint64 `json:"product_warehouse_id"`
SubQty float64 `json:"sub_qty"` SubQty float64 `json:"sub_qty"`
TotalQty float64 `json:"total_qty"` TotalQty float64 `json:"total_qty"`
TotalUsed float64 `json:"total_used"` TotalUsed float64 `json:"total_used"`
@@ -66,17 +61,6 @@ type PurchaseItemDTO struct {
VehicleNumber *string `json:"vehicle_number"` VehicleNumber *string `json:"vehicle_number"`
} }
func ToPurchaseRelationDTO(p *entity.Purchase) PurchaseRelationDTO {
return PurchaseRelationDTO{
Id: p.Id,
PrNumber: p.PrNumber,
PoNumber: p.PoNumber,
PoDate: p.PoDate,
Notes: p.Notes,
}
}
func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO { func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
dto := PurchaseItemDTO{ dto := PurchaseItemDTO{
Id: item.Id, Id: item.Id,
@@ -93,12 +77,10 @@ func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
TravelDocumentPath: item.TravelNumberDocs, TravelDocumentPath: item.TravelNumberDocs,
VehicleNumber: item.VehicleNumber, VehicleNumber: item.VehicleNumber,
} }
if item.Product != nil && item.Product.Id != 0 { if item.Product != nil && item.Product.Id != 0 {
summary := productDTO.ToProductRelationDTO(*item.Product) summary := productDTO.ToProductRelationDTO(*item.Product)
dto.Product = &summary dto.Product = &summary
} }
if item.Warehouse != nil && item.Warehouse.Id != 0 { if item.Warehouse != nil && item.Warehouse.Id != 0 {
summary := warehouseDTO.ToWarehouseRelationDTO(*item.Warehouse) summary := warehouseDTO.ToWarehouseRelationDTO(*item.Warehouse)
if item.Warehouse.Area.Id != 0 { if item.Warehouse.Area.Id != 0 {
@@ -111,7 +93,6 @@ func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
} }
dto.Warehouse = &summary dto.Warehouse = &summary
} }
return dto return dto
} }
@@ -123,78 +104,70 @@ func ToPurchaseItemDTOs(items []entity.PurchaseItem) []PurchaseItemDTO {
return result return result
} }
func ToPurchaseListDTO(p entity.Purchase) PurchaseListDTO { func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO {
var supplier *supplierDTO.SupplierRelationDTO dto := PurchaseDetailDTO{
if p.Supplier.Id != 0 { Id: p.Id,
mapped := supplierDTO.ToSupplierRelationDTO(p.Supplier) PrNumber: p.PrNumber,
supplier = &mapped PoNumber: p.PoNumber,
Supplier: mapSupplier(p.Supplier),
CreditTerm: p.CreditTerm,
DueDate: p.DueDate,
PoDate: p.PoDate,
GrandTotal: p.GrandTotal,
Notes: p.Notes,
Items: ToPurchaseItemDTOs(p.Items),
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
} }
if approval := toPurchaseApprovalDTO(p); approval != nil {
var createdUser *userDTO.UserRelationDTO dto.Approval = approval
if p.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(p.CreatedUser)
createdUser = &mapped
}
var latestApproval *approvalDTO.ApprovalRelationDTO
if p.LatestApproval != nil && p.LatestApproval.Id != 0 {
mapped := approvalDTO.ToApprovalDTO(*p.LatestApproval)
latestApproval = &mapped
}
return PurchaseListDTO{
PurchaseRelationDTO: ToPurchaseRelationDTO(&p),
Supplier: supplier,
CreditTerm: p.CreditTerm,
DueDate: p.DueDate,
GrandTotal: p.GrandTotal,
CreatedUser: createdUser,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
LatestApproval: latestApproval,
} }
return dto
} }
func ToPurchaseListDTOs(items []entity.Purchase) []PurchaseListDTO { func ToPurchaseListDTO(p entity.Purchase) PurchaseListItemDTO {
if len(items) == 0 { dto := PurchaseListItemDTO{
return make([]PurchaseListDTO, 0) Id: p.Id,
PrNumber: p.PrNumber,
PoNumber: p.PoNumber,
Supplier: mapSupplier(p.Supplier),
CreditTerm: p.CreditTerm,
DueDate: p.DueDate,
PoDate: p.PoDate,
GrandTotal: p.GrandTotal,
Notes: p.Notes,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
} }
result := make([]PurchaseListDTO, len(items)) if approval := toPurchaseApprovalDTO(p); approval != nil {
dto.Approval = approval
}
return dto
}
func mapSupplier(s entity.Supplier) *supplierDTO.SupplierRelationDTO {
if s.Id == 0 {
return nil
}
summary := supplierDTO.ToSupplierRelationDTO(s)
return &summary
}
func ToPurchaseListDTOs(items []entity.Purchase) []PurchaseListItemDTO {
if len(items) == 0 {
return nil
}
result := make([]PurchaseListItemDTO, len(items))
for i, item := range items { for i, item := range items {
result[i] = ToPurchaseListDTO(item) result[i] = ToPurchaseListDTO(item)
} }
return result return result
} }
func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO { func toPurchaseApprovalDTO(p entity.Purchase) *approvalDTO.ApprovalRelationDTO {
var supplier *supplierDTO.SupplierRelationDTO if p.LatestApproval == nil || p.LatestApproval.Id == 0 {
if p.Supplier.Id != 0 { return nil
mapped := supplierDTO.ToSupplierRelationDTO(p.Supplier)
supplier = &mapped
}
var createdUser *userDTO.UserRelationDTO
if p.CreatedUser.Id != 0 {
mapped := userDTO.ToUserRelationDTO(p.CreatedUser)
createdUser = &mapped
}
var latestApproval *approvalDTO.ApprovalRelationDTO
if p.LatestApproval != nil && p.LatestApproval.Id != 0 {
mapped := approvalDTO.ToApprovalDTO(*p.LatestApproval)
latestApproval = &mapped
}
return PurchaseDetailDTO{
PurchaseRelationDTO: ToPurchaseRelationDTO(&p),
Supplier: supplier,
CreditTerm: p.CreditTerm,
DueDate: p.DueDate,
GrandTotal: p.GrandTotal,
Items: ToPurchaseItemDTOs(p.Items),
CreatedUser: createdUser,
CreatedAt: p.CreatedAt,
UpdatedAt: p.UpdatedAt,
LatestApproval: latestApproval,
} }
mapped := approvalDTO.ToApprovalDTO(*p.LatestApproval)
return &mapped
} }
+1
View File
@@ -43,6 +43,7 @@ func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
warehouseRepo, warehouseRepo,
supplierRepo, supplierRepo,
productWarehouseRepo, productWarehouseRepo,
approvalRepo,
approvalService, approvalService,
expenseBridge, expenseBridge,
) )
@@ -18,11 +18,14 @@ import (
type PurchaseRepository interface { type PurchaseRepository interface {
repository.BaseRepository[entity.Purchase] repository.BaseRepository[entity.Purchase]
CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error
UpdatePricing(ctx context.Context, purchaseID uint, updates []PurchasePricingUpdate, grandTotal float64) error GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error)
UpdateReceivingDetails(ctx context.Context, purchaseID uint, updates []PurchaseReceivingUpdate) error GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error)
DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error UpdatePricing(ctx context.Context, purchaseID uint64, updates []PurchasePricingUpdate, grandTotal float64) error
UpdateGrandTotal(ctx context.Context, purchaseID uint, grandTotal float64) error UpdateReceivingDetails(ctx context.Context, purchaseID uint64, updates []PurchaseReceivingUpdate) error
DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error
WithListRelations() func(*gorm.DB) *gorm.DB
UpdateGrandTotal(ctx context.Context, purchaseID uint64, grandTotal float64) error
NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error) NextPrNumber(ctx context.Context, tx *gorm.DB) (string, error)
NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error) NextPoNumber(ctx context.Context, tx *gorm.DB) (string, error)
} }
@@ -37,10 +40,19 @@ func NewPurchaseRepository(db *gorm.DB) PurchaseRepository {
} }
} }
type PurchaseListFilter struct {
SupplierID uint
Search string
PrNumber string
CreatedFrom *time.Time
CreatedTo *time.Time
Status *entity.ApprovalAction
CompletedOnly bool
}
func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error { func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error {
db := r.DB().WithContext(ctx) db := r.DB().WithContext(ctx)
//ambil dari base repository
if err := db.Create(purchase).Error; err != nil { if err := db.Create(purchase).Error; err != nil {
return err return err
} }
@@ -59,7 +71,7 @@ func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *
return nil return nil
} }
func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint, items []*entity.PurchaseItem) error { func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uint64, items []*entity.PurchaseItem) error {
if len(items) == 0 { if len(items) == 0 {
return nil return nil
} }
@@ -74,9 +86,52 @@ func (r *PurchaseRepositoryImpl) CreateItems(ctx context.Context, purchaseID uin
return r.DB().WithContext(ctx).Create(&items).Error return r.DB().WithContext(ctx).Create(&items).Error
} }
func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) {
var purchase entity.Purchase
err := r.DB().WithContext(ctx).
Scopes(r.withDetailRelations).
First(&purchase, id).Error
if err != nil {
return nil, err
}
return &purchase, nil
}
func (r *PurchaseRepositoryImpl) GetAllWithFilters(ctx context.Context, offset, limit int, filter *PurchaseListFilter) ([]entity.Purchase, int64, error) {
return r.GetAll(ctx, offset, limit, func(db *gorm.DB) *gorm.DB {
db = r.withListRelations(db)
return r.applyListFilters(db, filter)
})
}
func (r *PurchaseRepositoryImpl) WithListRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return r.withListRelations(db)
}
}
func (r *PurchaseRepositoryImpl) withDetailRelations(db *gorm.DB) *gorm.DB {
return db.
Preload("Supplier").
Preload("Items", func(db *gorm.DB) *gorm.DB {
return db.Order("id ASC")
}).
Preload("Items.Product").
Preload("Items.Warehouse").
Preload("Items.Warehouse.Area").
Preload("Items.Warehouse.Location").
Preload("Items.ProductWarehouse")
}
func (r *PurchaseRepositoryImpl) WithDetailRelations() func(*gorm.DB) *gorm.DB {
return func(db *gorm.DB) *gorm.DB {
return r.withDetailRelations(db)
}
}
type PurchasePricingUpdate struct { type PurchasePricingUpdate struct {
ItemID uint ItemID uint64
ProductID *uint ProductID *uint64
Price float64 Price float64
TotalPrice float64 TotalPrice float64
Quantity *float64 Quantity *float64
@@ -84,7 +139,7 @@ type PurchasePricingUpdate struct {
} }
type PurchaseReceivingUpdate struct { type PurchaseReceivingUpdate struct {
ItemID uint ItemID uint64
ReceivedDate *time.Time ReceivedDate *time.Time
TravelNumber *string TravelNumber *string
TravelDocumentPath *string TravelDocumentPath *string
@@ -97,7 +152,7 @@ type PurchaseReceivingUpdate struct {
func (r *PurchaseRepositoryImpl) UpdatePricing( func (r *PurchaseRepositoryImpl) UpdatePricing(
ctx context.Context, ctx context.Context,
purchaseID uint, purchaseID uint64,
updates []PurchasePricingUpdate, updates []PurchasePricingUpdate,
grandTotal float64, grandTotal float64,
) error { ) error {
@@ -137,6 +192,7 @@ func (r *PurchaseRepositoryImpl) UpdatePricing(
Where("id = ?", purchaseID). Where("id = ?", purchaseID).
Updates(map[string]interface{}{ Updates(map[string]interface{}{
"grand_total": grandTotal, "grand_total": grandTotal,
"updated_at": gorm.Expr("NOW()"),
}).Error; err != nil { }).Error; err != nil {
return err return err
} }
@@ -146,7 +202,7 @@ func (r *PurchaseRepositoryImpl) UpdatePricing(
func (r *PurchaseRepositoryImpl) UpdateReceivingDetails( func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
ctx context.Context, ctx context.Context,
purchaseID uint, purchaseID uint64,
updates []PurchaseReceivingUpdate, updates []PurchaseReceivingUpdate,
) error { ) error {
if len(updates) == 0 { if len(updates) == 0 {
@@ -203,7 +259,7 @@ func (r *PurchaseRepositoryImpl) UpdateReceivingDetails(
func (r *PurchaseRepositoryImpl) UpdateGrandTotal( func (r *PurchaseRepositoryImpl) UpdateGrandTotal(
ctx context.Context, ctx context.Context,
purchaseID uint, purchaseID uint64,
grandTotal float64, grandTotal float64,
) error { ) error {
return r.DB().WithContext(ctx). return r.DB().WithContext(ctx).
@@ -215,7 +271,7 @@ func (r *PurchaseRepositoryImpl) UpdateGrandTotal(
}).Error }).Error
} }
func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint, itemIDs []uint) error { func (r *PurchaseRepositoryImpl) DeleteItems(ctx context.Context, purchaseID uint64, itemIDs []uint64) error {
if len(itemIDs) == 0 { if len(itemIDs) == 0 {
return errors.New("itemIDs cannot be empty") return errors.New("itemIDs cannot be empty")
} }
@@ -305,3 +361,63 @@ func parseNumericSuffix(value, prefix string) (int, bool) {
} }
return number, true return number, true
} }
func (r *PurchaseRepositoryImpl) withListRelations(db *gorm.DB) *gorm.DB {
return db.Preload("Supplier")
}
func (r *PurchaseRepositoryImpl) applyListFilters(db *gorm.DB, filter *PurchaseListFilter) *gorm.DB {
if filter == nil {
return db
}
if filter.SupplierID > 0 {
db = db.Where("purchases.supplier_id = ?", filter.SupplierID)
}
if search := strings.ToLower(strings.TrimSpace(filter.Search)); search != "" {
like := "%" + search + "%"
db = db.Where("(LOWER(purchases.pr_number) LIKE ? OR LOWER(COALESCE(purchases.notes, '')) LIKE ?)", like, like)
}
if pr := strings.TrimSpace(filter.PrNumber); pr != "" {
db = db.Where("purchases.pr_number ILIKE ?", "%"+pr+"%")
}
if filter.CreatedFrom != nil {
db = db.Where("purchases.created_at >= ?", *filter.CreatedFrom)
}
if filter.CreatedTo != nil {
db = db.Where("purchases.created_at < ?", *filter.CreatedTo)
}
if filter.CompletedOnly {
step := uint16(utils.PurchaseStepCompleted)
db = r.applyLatestApprovalFilter(db, entity.ApprovalActionApproved, &step)
} else if filter.Status != nil {
db = r.applyLatestApprovalFilter(db, *filter.Status, nil)
}
return db.Order("purchases.created_at DESC").Order("purchases.id DESC")
}
func (r *PurchaseRepositoryImpl) applyLatestApprovalFilter(db *gorm.DB, action entity.ApprovalAction, minStep *uint16) *gorm.DB {
latestSub := r.DB().
Model(&entity.Approval{}).
Select("approvable_id, MAX(action_at) AS latest_action_at").
Where("approvable_type = ?", utils.ApprovalWorkflowPurchase.String()).
Group("approvable_id")
db = db.
Joins("LEFT JOIN (?) AS latest_purchase_approvals ON latest_purchase_approvals.approvable_id = purchases.id", latestSub).
Joins(
"LEFT JOIN approvals ON approvals.approvable_id = purchases.id AND approvals.approvable_type = ? AND approvals.action_at = latest_purchase_approvals.latest_action_at",
utils.ApprovalWorkflowPurchase.String(),
).
Where("approvals.action = ?", string(action))
if minStep != nil {
db = db.Where("approvals.step_number >= ?", *minStep)
}
return db
}
+2 -2
View File
@@ -3,7 +3,7 @@ package purchases
import ( import (
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
m "gitlab.com/mbugroup/lti-api.git/internal/middleware" middleware "gitlab.com/mbugroup/lti-api.git/internal/middleware"
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/controllers" controller "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/controllers"
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services" service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/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 Routes(router fiber.Router, purchaseService service.PurchaseService, userSe
ctrl := controller.NewPurchaseController(purchaseService) ctrl := controller.NewPurchaseController(purchaseService)
route := router.Group("/purchases") route := router.Group("/purchases")
route.Use(m.Auth(userService)) route.Use(middleware.Auth(userService))
route.Get("/", ctrl.GetAll) route.Get("/", ctrl.GetAll)
route.Get("/:id", ctrl.GetOne) route.Get("/:id", ctrl.GetOne)
@@ -9,16 +9,16 @@ import (
// PurchaseExpenseBridge defines hooks that allow purchase flows to stay in sync with expense data once it exists. // PurchaseExpenseBridge defines hooks that allow purchase flows to stay in sync with expense data once it exists.
type PurchaseExpenseBridge interface { type PurchaseExpenseBridge interface {
OnItemsCreated(ctx context.Context, purchaseID uint, items []entity.PurchaseItem) error OnItemsCreated(ctx context.Context, purchaseID uint64, items []entity.PurchaseItem) error
OnItemsDeleted(ctx context.Context, purchaseID uint, itemIDs []uint) error OnItemsDeleted(ctx context.Context, purchaseID uint64, itemIDs []uint64) error
OnItemsReceived(ctx context.Context, purchaseID uint, updates []ExpenseReceivingPayload) error OnItemsReceived(ctx context.Context, purchaseID uint64, updates []ExpenseReceivingPayload) error
} }
// ExpenseReceivingPayload captures the minimum data expense integration will need once available. // ExpenseReceivingPayload captures the minimum data expense integration will need once available.
type ExpenseReceivingPayload struct { type ExpenseReceivingPayload struct {
PurchaseItemID uint PurchaseItemID uint64
ProductID uint ProductID uint64
WarehouseID uint WarehouseID uint64
ReceivedQty float64 ReceivedQty float64
ReceivedDate *time.Time ReceivedDate *time.Time
} }
@@ -30,14 +30,14 @@ func NewNoopPurchaseExpenseBridge() PurchaseExpenseBridge {
return &noopPurchaseExpenseBridge{} return &noopPurchaseExpenseBridge{}
} }
func (n *noopPurchaseExpenseBridge) OnItemsCreated(_ context.Context, _ uint, _ []entity.PurchaseItem) error { func (n *noopPurchaseExpenseBridge) OnItemsCreated(_ context.Context, _ uint64, _ []entity.PurchaseItem) error {
return nil return nil
} }
func (n *noopPurchaseExpenseBridge) OnItemsDeleted(_ context.Context, _ uint, _ []uint) error { func (n *noopPurchaseExpenseBridge) OnItemsDeleted(_ context.Context, _ uint64, _ []uint64) error {
return nil return nil
} }
func (n *noopPurchaseExpenseBridge) OnItemsReceived(_ context.Context, _ uint, _ []ExpenseReceivingPayload) error { func (n *noopPurchaseExpenseBridge) OnItemsReceived(_ context.Context, _ uint64, _ []ExpenseReceivingPayload) error {
return nil return nil
} }
File diff suppressed because it is too large Load Diff
@@ -14,28 +14,26 @@ type CreatePurchaseRequest struct {
} }
type StaffPurchaseApprovalItem struct { type StaffPurchaseApprovalItem struct {
PurchaseItemID uint `json:"purchase_item_id,omitempty" validate:"omitempty,gt=0"` PurchaseItemID uint64 `json:"purchase_item_id,omitempty" validate:"omitempty,gt=0"`
// For new items (no purchase_item_id), product_id is required. // For new items (no purchase_item_id), product_id is required.
ProductID uint `json:"product_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"` ProductID uint64 `json:"product_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
WarehouseID uint `json:"warehouse_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"` WarehouseID uint64 `json:"warehouse_id,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
Qty *float64 `json:"qty,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"` Qty *float64 `json:"qty,omitempty" validate:"required_without=PurchaseItemID,omitempty,gt=0"`
Price float64 `json:"price" validate:"required,gt=0"` Price float64 `json:"price" validate:"required,gt=0"`
TotalPrice float64 `json:"total_price" validate:"required,gt=0"` TotalPrice float64 `json:"total_price" validate:"required,gt=0"`
} }
type ApproveStaffPurchaseRequest struct { type ApproveStaffPurchaseRequest struct {
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"` Items []StaffPurchaseApprovalItem `json:"items" validate:"required,min=1,dive"`
Items []StaffPurchaseApprovalItem `json:"items,omitempty" validate:"omitempty,min=1,dive"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
} }
type ApproveManagerPurchaseRequest struct { type ApproveManagerPurchaseRequest struct {
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
} }
type ReceivePurchaseItemRequest struct { type ReceivePurchaseItemRequest struct {
PurchaseItemID uint `json:"purchase_item_id" validate:"required,gt=0"` PurchaseItemID uint64 `json:"purchase_item_id" validate:"required,gt=0"`
WarehouseID *uint `json:"warehouse_id" validate:"omitempty,gt=0"` WarehouseID *uint `json:"warehouse_id" validate:"omitempty,gt=0"`
ReceivedDate string `json:"received_date" validate:"required,datetime=2006-01-02"` ReceivedDate string `json:"received_date" validate:"required,datetime=2006-01-02"`
TravelNumber *string `json:"travel_number" validate:"omitempty,max=100"` TravelNumber *string `json:"travel_number" validate:"omitempty,max=100"`
@@ -45,23 +43,21 @@ type ReceivePurchaseItemRequest struct {
} }
type ReceivePurchaseRequest struct { type ReceivePurchaseRequest struct {
Action string `json:"action" validate:"required,oneof=APPROVED REJECTED"` Items []ReceivePurchaseItemRequest `json:"items" validate:"required,min=1,dive"`
Items []ReceivePurchaseItemRequest `json:"items,omitempty" validate:"omitempty,min=1,dive"` Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
} }
type DeletePurchaseItemsRequest struct { type DeletePurchaseItemsRequest struct {
ItemIDs []uint `json:"item_ids" validate:"required,min=1,dive,gt=0"` ItemIDs []uint64 `json:"item_ids" validate:"required,min=1,dive,gt=0"`
} }
type Query struct { type PurchaseQuery 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=100"`
SupplierID uint `query:"supplier_id" validate:"omitempty,gt=0"` SupplierID uint `query:"supplier_id" validate:"omitempty,gt=0"`
AreaID uint `query:"area_id" validate:"omitempty,gt=0"` Search string `query:"search" validate:"omitempty,max=100"`
LocationID uint `query:"location_id" validate:"omitempty,gt=0"` PrNumber string `query:"pr_number" validate:"omitempty,max=50"`
ProductCategoryID uint `query:"product_category_id" validate:"omitempty,gt=0"` CreatedFrom string `query:"created_from" validate:"omitempty,datetime=2006-01-02"`
Search string `query:"search" validate:"omitempty,max=100"` CreatedTo string `query:"created_to" validate:"omitempty,datetime=2006-01-02"`
CreatedFrom string `query:"created_from" validate:"omitempty,datetime=2006-01-02"` Status string `query:"status" validate:"omitempty,oneof=CREATED UPDATED APPROVED REJECTED COMPLETED"`
CreatedTo string `query:"created_to" validate:"omitempty,datetime=2006-01-02"`
} }

Some files were not shown because too many files have changed in this diff Show More