mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
Merge branch 'feat/BE/Sprint-6' into 'development'
add approval projectflockkandang closed,expense must be done,stock must empty by flag unfinished:need info approval fix See merge request mbugroup/lti-api!91
This commit is contained in:
@@ -3,7 +3,7 @@ package capabilities
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
recordings "gitlab.com/mbugroup/lti-api.git/internal/modules/production/recordings"
|
permission "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FromPermissions returns a filtered map of capabilities that the frontend can use
|
// FromPermissions returns a filtered map of capabilities that the frontend can use
|
||||||
@@ -37,8 +37,8 @@ func normalizeAndAllow(perm string) (string, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allowed = map[string]struct{}{
|
var allowed = map[string]struct{}{
|
||||||
recordings.PermissionRecordingRead: {},
|
permission.PermissionRecordingRead: {},
|
||||||
recordings.PermissionRecordingCreate: {},
|
permission.PermissionRecordingCreate: {},
|
||||||
recordings.PermissionRecordingUpdate: {},
|
permission.PermissionRecordingUpdate: {},
|
||||||
recordings.PermissionRecordingDelete: {},
|
permission.PermissionRecordingDelete: {},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
productWarehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
||||||
|
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Dipakai untuk semua module yang butuh cek:
|
||||||
|
// "PW ini → warehouse → kandang → project_flock_kandang sudah closing atau belum"
|
||||||
|
func EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
ctx context.Context,
|
||||||
|
db *gorm.DB,
|
||||||
|
productWarehouseIDs []uint,
|
||||||
|
) error {
|
||||||
|
if len(productWarehouseIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
pwRepo := productWarehouseRepo.NewProductWarehouseRepository(db)
|
||||||
|
wRepo := warehouseRepo.NewWarehouseRepository(db)
|
||||||
|
pfkRepo := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
||||||
|
|
||||||
|
seenPW := make(map[uint]struct{})
|
||||||
|
seenKandang := make(map[uint]struct{})
|
||||||
|
|
||||||
|
for _, pwID := range productWarehouseIDs {
|
||||||
|
if pwID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seenPW[pwID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenPW[pwID] = struct{}{}
|
||||||
|
|
||||||
|
pw, err := pwRepo.GetByID(ctx, pwID, nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Product warehouse %d tidak ditemukan", pwID))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
wh, err := wRepo.GetByID(ctx, uint(pw.WarehouseId), nil)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Warehouse %d tidak ditemukan", pw.WarehouseId))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warehouse tanpa kandang → bukan kandang produksi → skip
|
||||||
|
if wh.KandangId == nil || *wh.KandangId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
kandangID := uint(*wh.KandangId)
|
||||||
|
if _, ok := seenKandang[kandangID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seenKandang[kandangID] = struct{}{}
|
||||||
|
|
||||||
|
pfk, err := pfkRepo.GetActiveByKandangID(ctx, kandangID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
// nggak ada project aktif untuk kandang ini → aman
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||||
|
}
|
||||||
|
// INTI RULE: kalau aktif tapi sudah punya ClosedAt → anggap "project sudah closing"
|
||||||
|
if pfk != nil && pfk.ClosedAt != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func EnsureProjectFlockNotClosedByProjectFlockKandangID(
|
||||||
|
ctx context.Context,
|
||||||
|
db *gorm.DB,
|
||||||
|
pfkIDs []uint,
|
||||||
|
) error {
|
||||||
|
pfkRepo := projectFlockKandangRepo.NewProjectFlockKandangRepository(db)
|
||||||
|
|
||||||
|
seen := make(map[uint]struct{})
|
||||||
|
for _, id := range pfkIDs {
|
||||||
|
if id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[id]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
|
||||||
|
pfk, err := pfkRepo.GetByID(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Project flock kandang %d tidak ditemukan", id))
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||||
|
}
|
||||||
|
|
||||||
|
if pfk.ClosedAt != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
DROP INDEX IF EXISTS idx_project_flock_kandangs_closed_at;
|
||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
DROP COLUMN IF EXISTS closed_at;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE project_flock_kandangs
|
||||||
|
ADD COLUMN IF NOT EXISTS closed_at TIMESTAMPTZ;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_project_flock_kandangs_closed_at
|
||||||
|
ON project_flock_kandangs (closed_at);
|
||||||
+1
-1
@@ -20,7 +20,7 @@ ALTER TABLE product_warehouses
|
|||||||
|
|
||||||
-- Restore audit/soft-delete columns
|
-- Restore audit/soft-delete columns
|
||||||
ALTER TABLE product_warehouses
|
ALTER TABLE product_warehouses
|
||||||
ADD COLUMN IF NOT EXISTS created_by BIGINT NOT NULL REFERENCES users (id),
|
ADD COLUMN IF NOT EXISTS created_by BIGINT REFERENCES users (id),
|
||||||
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
|
ADD COLUMN IF NOT EXISTS created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
|
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ DEFAULT NOW(),
|
||||||
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ;
|
||||||
|
|||||||
+2
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE purchases
|
||||||
|
DROP COLUMN IF EXISTS credit_term;
|
||||||
+5
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE purchases
|
||||||
|
ADD COLUMN IF NOT EXISTS credit_term INT NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE purchases
|
||||||
|
ALTER COLUMN credit_term DROP DEFAULT;
|
||||||
@@ -3,11 +3,12 @@ package entities
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type ProjectFlockKandang struct {
|
type ProjectFlockKandang struct {
|
||||||
Id uint `gorm:"primaryKey"`
|
Id uint `gorm:"primaryKey"`
|
||||||
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
ProjectFlockId uint `gorm:"not null;index:idx_project_flock_kandangs_project;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||||
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
KandangId uint `gorm:"not null;index:idx_project_flock_kandangs_kandang;uniqueIndex:idx_project_flock_kandangs_unique"`
|
||||||
Period int `gorm:"not null"`
|
Period int `gorm:"not null"`
|
||||||
CreatedAt time.Time `gorm:"autoCreateTime"`
|
ClosedAt *time.Time `gorm:"index"`
|
||||||
|
CreatedAt time.Time `gorm:"autoCreateTime"`
|
||||||
|
|
||||||
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
ProjectFlock ProjectFlock `gorm:"foreignKey:ProjectFlockId;references:Id"`
|
||||||
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
Kandang Kandang `gorm:"foreignKey:KandangId;references:Id"`
|
||||||
|
|||||||
@@ -5,17 +5,18 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type Purchase struct {
|
type Purchase struct {
|
||||||
Id uint `gorm:"primaryKey;autoIncrement"`
|
Id uint `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 uint `gorm:"not null"`
|
||||||
|
CreditTerm int `gorm:"column:credit_term;not null;default:0"`
|
||||||
DueDate *time.Time
|
DueDate *time.Time
|
||||||
Notes *string
|
Notes *string
|
||||||
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 uint `gorm:"not null"`
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
Supplier Supplier `gorm:"foreignKey:SupplierId;references:Id"`
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ type PurchaseItem struct {
|
|||||||
ExpenseNonstockId *uint64
|
ExpenseNonstockId *uint64
|
||||||
|
|
||||||
// Relations
|
// Relations
|
||||||
|
ExpenseNonstock *ExpenseNonstock `gorm:"foreignKey:ExpenseNonstockId;references:Id"`
|
||||||
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
Purchase *Purchase `gorm:"foreignKey:PurchaseId;references:Id"`
|
||||||
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
Product *Product `gorm:"foreignKey:ProductId;references:Id"`
|
||||||
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
Warehouse *Warehouse `gorm:"foreignKey:WarehouseId;references:Id"`
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
"gitlab.com/mbugroup/lti-api.git/internal/config"
|
||||||
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/modules/sso/session"
|
"gitlab.com/mbugroup/lti-api.git/internal/modules/sso/session"
|
||||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
service "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
"gitlab.com/mbugroup/lti-api.git/internal/sso"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -91,7 +90,6 @@ func Auth(userService service.UserService, requiredScopes ...string) fiber.Handl
|
|||||||
|
|
||||||
c.Locals(authContextLocalsKey, ctx)
|
c.Locals(authContextLocalsKey, ctx)
|
||||||
c.Locals(authUserLocalsKey, user)
|
c.Locals(authUserLocalsKey, user)
|
||||||
|
|
||||||
return c.Next()
|
return c.Next()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +105,7 @@ func AuthenticatedUser(c *fiber.Ctx) (*entity.User, bool) {
|
|||||||
|
|
||||||
func ActorIDFromContext(c *fiber.Ctx) (uint, error) {
|
func ActorIDFromContext(c *fiber.Ctx) (uint, error) {
|
||||||
user, ok := AuthenticatedUser(c)
|
user, ok := AuthenticatedUser(c)
|
||||||
if !ok || user == nil || user.Id == 0 {
|
if !ok || user == nil || user.Id == 0 {
|
||||||
return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
}
|
}
|
||||||
return user.Id, nil
|
return user.Id, nil
|
||||||
@@ -199,3 +197,71 @@ func hasAllScopes(have, required []string) bool {
|
|||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RequirePermissions ensures the authenticated user possesses all specified permissions.
|
||||||
|
func RequirePermissions(perms ...string) fiber.Handler {
|
||||||
|
required := canonicalPermissions(perms)
|
||||||
|
return func(c *fiber.Ctx) error {
|
||||||
|
if len(required) == 0 {
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, ok := AuthDetails(c)
|
||||||
|
if !ok || ctx == nil {
|
||||||
|
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
||||||
|
}
|
||||||
|
|
||||||
|
userPerms := ctx.permissionSet()
|
||||||
|
if len(userPerms) == 0 {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, perm := range required {
|
||||||
|
if _, has := userPerms[perm]; !has {
|
||||||
|
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasPermission reports whether the current request context includes the given permission.
|
||||||
|
func HasPermission(c *fiber.Ctx, perm string) bool {
|
||||||
|
ctx, ok := AuthDetails(c)
|
||||||
|
if !ok || ctx == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
perm = canonicalPermission(perm)
|
||||||
|
if perm == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, has := ctx.permissionSet()[perm]
|
||||||
|
return has
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AuthContext) permissionSet() map[string]struct{} {
|
||||||
|
if a == nil || a.Permissions == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return a.Permissions
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalPermissions(perms []string) []string {
|
||||||
|
out := make([]string, 0, len(perms))
|
||||||
|
seen := make(map[string]struct{}, len(perms))
|
||||||
|
for _, perm := range perms {
|
||||||
|
if canonical := canonicalPermission(perm); canonical != "" {
|
||||||
|
if _, ok := seen[canonical]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[canonical] = struct{}{}
|
||||||
|
out = append(out, canonical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func canonicalPermission(perm string) string {
|
||||||
|
return strings.ToLower(strings.TrimSpace(perm))
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,75 +1,14 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
//project-flock
|
||||||
"strings"
|
const (
|
||||||
|
PermissionProjectFlockClosing = "lti:project-flock:closing"
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RequirePermissions ensures the authenticated user possesses all specified permissions.
|
//recording
|
||||||
func RequirePermissions(perms ...string) fiber.Handler {
|
const (
|
||||||
required := canonicalPermissions(perms)
|
PermissionRecordingRead = "recording.index"
|
||||||
return func(c *fiber.Ctx) error {
|
PermissionRecordingCreate = "recording.create"
|
||||||
if len(required) == 0 {
|
PermissionRecordingUpdate = "recording.update"
|
||||||
return c.Next()
|
PermissionRecordingDelete = "recording.delete"
|
||||||
}
|
)
|
||||||
|
|
||||||
ctx, ok := AuthDetails(c)
|
|
||||||
if !ok || ctx == nil {
|
|
||||||
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
|
||||||
}
|
|
||||||
|
|
||||||
userPerms := ctx.permissionSet()
|
|
||||||
if len(userPerms) == 0 {
|
|
||||||
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, perm := range required {
|
|
||||||
if _, has := userPerms[perm]; !has {
|
|
||||||
return fiber.NewError(fiber.StatusForbidden, "Insufficient permission")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Next()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasPermission reports whether the current request context includes the given permission.
|
|
||||||
func HasPermission(c *fiber.Ctx, perm string) bool {
|
|
||||||
ctx, ok := AuthDetails(c)
|
|
||||||
if !ok || ctx == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
perm = canonicalPermission(perm)
|
|
||||||
if perm == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
_, has := ctx.permissionSet()[perm]
|
|
||||||
return has
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *AuthContext) permissionSet() map[string]struct{} {
|
|
||||||
if a == nil || a.Permissions == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return a.Permissions
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalPermissions(perms []string) []string {
|
|
||||||
out := make([]string, 0, len(perms))
|
|
||||||
seen := make(map[string]struct{}, len(perms))
|
|
||||||
for _, perm := range perms {
|
|
||||||
if canonical := canonicalPermission(perm); canonical != "" {
|
|
||||||
if _, ok := seen[canonical]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[canonical] = struct{}{}
|
|
||||||
out = append(out, canonical)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func canonicalPermission(perm string) string {
|
|
||||||
return strings.ToLower(strings.TrimSpace(perm))
|
|
||||||
}
|
|
||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
sExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/services"
|
||||||
|
|
||||||
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"
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,11 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
|
|
||||||
"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"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -13,6 +15,8 @@ type ExpenseRepository interface {
|
|||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (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)
|
||||||
|
WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB
|
||||||
|
CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExpenseRepositoryImpl struct {
|
type ExpenseRepositoryImpl struct {
|
||||||
@@ -49,3 +53,57 @@ func (r *ExpenseRepositoryImpl) GetWithSupplier(ctx context.Context, id uint64)
|
|||||||
}
|
}
|
||||||
return &expense, nil
|
return &expense, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRepositoryImpl) WithProjectFlockKandangFilter(pfkID, kandangID uint) func(*gorm.DB) *gorm.DB {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
if pfkID == 0 && kandangID == 0 {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
q := db.Joins("JOIN expense_nonstocks ON expense_nonstocks.expense_id = expenses.id")
|
||||||
|
if pfkID > 0 && kandangID > 0 {
|
||||||
|
return q.Where("expense_nonstocks.project_flock_kandang_id = ? OR expense_nonstocks.kandang_id = ?", pfkID, kandangID)
|
||||||
|
}
|
||||||
|
if pfkID > 0 {
|
||||||
|
return q.Where("expense_nonstocks.project_flock_kandang_id = ?", pfkID)
|
||||||
|
}
|
||||||
|
return q.Where("expense_nonstocks.kandang_id = ?", kandangID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ExpenseRepositoryImpl) CountUnfinishedByProjectFlockKandang(ctx context.Context, pfkID, kandangID uint, isFinished func(*entity.Approval) bool) (int64, error) {
|
||||||
|
if pfkID == 0 && kandangID == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var ids []uint64
|
||||||
|
if err := r.DB().WithContext(ctx).
|
||||||
|
Table("expenses").
|
||||||
|
Scopes(r.WithProjectFlockKandangFilter(pfkID, kandangID)).
|
||||||
|
Group("expenses.id").Where("expenses.deleted_at IS NULL").
|
||||||
|
Pluck("expenses.id", &ids).Error; err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var unfinished int64
|
||||||
|
for _, id := range ids {
|
||||||
|
var latest entity.Approval
|
||||||
|
err := r.DB().WithContext(ctx).
|
||||||
|
Table("approvals").
|
||||||
|
Where("approvable_type = ? AND approvable_id = ?", utils.ApprovalWorkflowExpense.String(), id).
|
||||||
|
Order("action_at DESC").
|
||||||
|
Limit(1).
|
||||||
|
First(&latest).Error
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if isFinished != nil {
|
||||||
|
if !isFinished(&latest) {
|
||||||
|
unfinished++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return unfinished, nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -365,6 +365,9 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), currentExpense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
categoryChanged := false
|
categoryChanged := false
|
||||||
var newCategory string
|
var newCategory string
|
||||||
if req.Category != nil && *req.Category != currentExpense.Category {
|
if req.Category != nil && *req.Category != currentExpense.Category {
|
||||||
@@ -409,6 +412,9 @@ func (s expenseService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint)
|
|||||||
if ens.KandangId != nil {
|
if ens.KandangId != nil {
|
||||||
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(*ens.KandangId))
|
projectFlockKandang, err := projectFlockKandangRepoTx.GetActiveByKandangID(c.Context(), uint(*ens.KandangId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), currentExpense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
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")
|
||||||
}
|
}
|
||||||
@@ -551,7 +557,21 @@ func (s expenseService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
expense, err := s.Repository.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get expense for ID %d: %+v", id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
if err := s.Repository.DeleteOne(c.Context(), id); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
|
s.Log.Errorf("Expense not found for ID %d: %+v", id, err)
|
||||||
@@ -580,6 +600,20 @@ 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")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expense, err := s.Repository.GetByID(c.Context(), expenseID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
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))
|
||||||
@@ -723,7 +757,19 @@ func (s *expenseService) UpdateRealization(c *fiber.Ctx, expenseID uint, req *va
|
|||||||
); err != nil {
|
); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
expense, err := s.Repository.GetByID(c.Context(), expenseID, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseID, nil)
|
latestApproval, err := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowExpense, expenseID, nil)
|
||||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate workflow")
|
||||||
@@ -1024,6 +1070,21 @@ func (s *expenseService) Approval(c *fiber.Ctx, req *validation.ApprovalRequest,
|
|||||||
} else {
|
} else {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval action")
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid approval action")
|
||||||
}
|
}
|
||||||
|
if approvalAction == entity.ApprovalActionApproved {
|
||||||
|
expense, err := expenseRepoTx.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Nonstocks")
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, "Expense not found")
|
||||||
|
}
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to load expense")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.ensureProjectFlockNotClosedForExpense(c.Context(), expense); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := approvalSvcTx.CreateApproval(
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
@@ -1093,13 +1154,45 @@ func (s *expenseService) validateExpenseNonstockRelation(ctx *fiber.Ctx, expense
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// func actorIDFromContext(c *fiber.Ctx) (uint, error) {
|
func (s *expenseService) ensureProjectFlockNotClosedForExpense(
|
||||||
// user, ok := authmiddleware.AuthenticatedUser(c)
|
ctx context.Context,
|
||||||
// if !ok || user == nil || user.Id == 0 {
|
expense *entity.Expense,
|
||||||
// return 0, fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
|
) error {
|
||||||
// }
|
// Kalau repo belum di-wire atau expense kosong → gak usah ngecek apa-apa
|
||||||
// return user.Id, nil
|
if s.ProjectFlockKandangRepo == nil || expense == nil {
|
||||||
// }
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// return user.Id, nil
|
seen := make(map[uint]struct{})
|
||||||
// }
|
|
||||||
|
for _, ens := range expense.Nonstocks {
|
||||||
|
// Field ini pointer, bisa nil
|
||||||
|
if ens.ProjectFlockKandangId == nil || *ens.ProjectFlockKandangId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
pfkID := uint(*ens.ProjectFlockKandangId)
|
||||||
|
if _, ok := seen[pfkID]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[pfkID] = struct{}{}
|
||||||
|
|
||||||
|
pfk, err := s.ProjectFlockKandangRepo.GetByID(ctx, pfkID)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(
|
||||||
|
fiber.StatusBadRequest,
|
||||||
|
fmt.Sprintf("Project flock %d tidak ditemukan", pfkID),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to validate project flock %d for expense %d: %+v", pfkID, expense.Id, err)
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
||||||
|
}
|
||||||
|
// ❗ RULE: kalau ClosedAt tidak nil → project sudah closing
|
||||||
|
if pfk.ClosedAt != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Project sudah closing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import (
|
|||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
rStockLogs "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
||||||
|
|
||||||
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"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -6,6 +6,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"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"
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
@@ -17,10 +20,6 @@ import (
|
|||||||
stockLogsRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/shared/repositories"
|
stockLogsRepo "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"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AdjustmentService interface {
|
type AdjustmentService interface {
|
||||||
@@ -129,6 +128,23 @@ func (s *adjustmentService) Adjustment(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
|
s.Log.Infof("Product warehouse created: %+v", newPW.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pw, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(
|
||||||
|
ctx,
|
||||||
|
uint(req.ProductID),
|
||||||
|
uint(req.WarehouseID),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
s.Log.Errorf("Failed to get product warehouse for project flock check: %+v", err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product warehouse")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := common.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
ctx,
|
||||||
|
s.StockLogsRepository.DB(),
|
||||||
|
[]uint{pw.Id},
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
err = s.StockLogsRepository.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
|
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(ctx, uint(req.ProductID), uint(req.WarehouseID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
+4
-1
@@ -258,7 +258,10 @@ func (r *ProductWarehouseRepositoryImpl) GetByFlagAndWarehouseID(ctx context.Con
|
|||||||
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
Joins("JOIN flags ON flags.flagable_id = products.id AND flags.flagable_type = 'products'").
|
||||||
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
|
Where("flags.name = ? AND product_warehouses.warehouse_id = ?", flagName, warehouseId).
|
||||||
Order("product_warehouses.id DESC").
|
Order("product_warehouses.id DESC").
|
||||||
Preload("Product").Preload("Warehouse").
|
Preload("Product").
|
||||||
|
Preload("Product.ProductCategory").
|
||||||
|
Preload("Product.Uom").
|
||||||
|
Preload("Warehouse").
|
||||||
Find(&productWarehouses).Error
|
Find(&productWarehouses).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -6,6 +6,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/gofiber/fiber/v2"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
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"
|
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"
|
||||||
@@ -17,9 +21,6 @@ import (
|
|||||||
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"
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
|
||||||
"github.com/gofiber/fiber/v2"
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -119,6 +120,8 @@ func (s transferService) GetOne(c *fiber.Ctx, id uint) (*entity.StockTransfer, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferRequest) (*entity.StockTransfer, error) {
|
||||||
|
// Validasi stok di gudang asal harus exist dan mencukupi
|
||||||
|
pwIDs := make([]uint, 0, len(req.Products))
|
||||||
|
|
||||||
// Validasi stok di gudang asal harus exist dan mencukupi
|
// Validasi stok di gudang asal harus exist dan mencukupi
|
||||||
for _, product := range req.Products {
|
for _, product := range req.Products {
|
||||||
@@ -134,7 +137,16 @@ func (s *transferService) CreateOne(c *fiber.Ctx, req *validation.TransferReques
|
|||||||
if sourcePW.Quantity < product.ProductQty {
|
if sourcePW.Quantity < product.ProductQty {
|
||||||
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))
|
||||||
}
|
}
|
||||||
|
pwIDs = append(pwIDs, sourcePW.Id)
|
||||||
}
|
}
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
c.Context(),
|
||||||
|
s.StockTransferRepo.DB(),
|
||||||
|
pwIDs,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ import (
|
|||||||
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
||||||
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"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,8 +40,10 @@ func (MarketingModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
|
|||||||
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
|
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
|
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
|
||||||
// Initialize services
|
// Initialize services
|
||||||
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, validate)
|
salesOrdersService := service.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc,warehouseRepo,projectFlockKandangRepo, validate)
|
||||||
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
|
deliveryOrdersService := service.NewDeliveryOrdersService(marketingRepo, marketingProductRepo, marketingDeliveryProductRepo, approvalSvc, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
|
|||||||
@@ -222,6 +222,14 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Delivery
|
|||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
c.Context(),
|
||||||
|
dbTransaction,
|
||||||
|
[]uint{foundMarketingProduct.ProductWarehouseId},
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -319,6 +327,13 @@ func (s deliveryOrdersService) UpdateOne(c *fiber.Ctx, req *validation.DeliveryO
|
|||||||
if foundMarketingProduct == nil {
|
if foundMarketingProduct == nil {
|
||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Marketing product %d not found for this marketing", requestedProduct.MarketingProductId))
|
||||||
}
|
}
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(
|
||||||
|
c.Context(),
|
||||||
|
dbTransaction,
|
||||||
|
[]uint{foundMarketingProduct.ProductWarehouseId},
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
deliveryProduct, err := marketingDeliveryProductRepositoryTx.GetByMarketingProductID(c.Context(), foundMarketingProduct.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ import (
|
|||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/repositories"
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/validations"
|
||||||
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
customerRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories"
|
||||||
|
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
projectFlockKandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
userRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||||
"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"
|
||||||
@@ -32,24 +34,29 @@ type SalesOrdersService interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type salesOrdersService struct {
|
type salesOrdersService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
MarketingRepo repository.MarketingRepository
|
MarketingRepo repository.MarketingRepository
|
||||||
CustomerRepo customerRepo.CustomerRepository
|
CustomerRepo customerRepo.CustomerRepository
|
||||||
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
ProductWarehouseRepo productWarehouseRepo.ProductWarehouseRepository
|
||||||
UserRepo userRepo.UserRepository
|
UserRepo userRepo.UserRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
WarehouseRepo warehouseRepo.WarehouseRepository
|
||||||
|
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, validate *validator.Validate) SalesOrdersService {
|
func NewSalesOrdersService(marketingRepo repository.MarketingRepository, customerRepo customerRepo.CustomerRepository, productWarehouseRepo productWarehouseRepo.ProductWarehouseRepository, userRepo userRepo.UserRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo warehouseRepo.WarehouseRepository,
|
||||||
|
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository, validate *validator.Validate) SalesOrdersService {
|
||||||
return &salesOrdersService{
|
return &salesOrdersService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
MarketingRepo: marketingRepo,
|
MarketingRepo: marketingRepo,
|
||||||
CustomerRepo: customerRepo,
|
CustomerRepo: customerRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
UserRepo: userRepo,
|
UserRepo: userRepo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
|
WarehouseRepo: warehouseRepo,
|
||||||
|
ProjectFlockKandangRepo: projectFlockKandangRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,10 +147,18 @@ func (s *salesOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(req.MarketingProducts) > 0 {
|
if len(req.MarketingProducts) > 0 {
|
||||||
|
pwIDs := make([]uint, 0, len(req.MarketingProducts))
|
||||||
for _, product := range req.MarketingProducts {
|
for _, product := range req.MarketingProducts {
|
||||||
|
if product.ProductWarehouseId != 0 {
|
||||||
|
pwIDs = append(pwIDs, product.ProductWarehouseId)
|
||||||
|
}
|
||||||
if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
|
if err := s.createMarketingProductWithDelivery(c.Context(), marketing.Id, product, marketingProductRepoTx, invDeliveryRepoTx); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create marketing product")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -213,6 +228,18 @@ func (s salesOrdersService) UpdateOne(c *fiber.Ctx, req *validation.Update, id u
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if len(req.MarketingProducts) > 0 {
|
||||||
|
pwIDs := make([]uint, 0, len(req.MarketingProducts))
|
||||||
|
for _, item := range req.MarketingProducts {
|
||||||
|
if item.ProductWarehouseId != 0 {
|
||||||
|
pwIDs = append(pwIDs, item.ProductWarehouseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|
||||||
@@ -367,7 +394,18 @@ func (s salesOrdersService) DeleteOne(c *fiber.Ctx, id uint) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order")
|
||||||
}
|
}
|
||||||
|
if len(marketing.Products) > 0 {
|
||||||
|
pwIDs := make([]uint, 0, len(marketing.Products))
|
||||||
|
for _, p := range marketing.Products {
|
||||||
|
if p.ProductWarehouseId != 0 {
|
||||||
|
pwIDs = append(pwIDs, p.ProductWarehouseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
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 {
|
||||||
|
|
||||||
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
marketingProductRepoTx := repository.NewMarketingProductRepository(dbTransaction)
|
||||||
@@ -458,6 +496,27 @@ func (s salesOrdersService) Approval(c *fiber.Ctx, req *validation.Approve) ([]e
|
|||||||
fmt.Sprintf("Marketing %d cannot be approved - current step is %d", id, latestApproval.StepNumber))
|
fmt.Sprintf("Marketing %d cannot be approved - current step is %d", id, latestApproval.StepNumber))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
marketing, mErr := s.MarketingRepo.GetByID(c.Context(), id, func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Preload("Products")
|
||||||
|
})
|
||||||
|
if mErr != nil {
|
||||||
|
if errors.Is(mErr, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("SalesOrders %d not found", id))
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch sales order for project validation")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(marketing.Products) > 0 {
|
||||||
|
pwIDs := make([]uint, 0, len(marketing.Products))
|
||||||
|
for _, p := range marketing.Products {
|
||||||
|
if p.ProductWarehouseId != 0 {
|
||||||
|
pwIDs = append(pwIDs, p.ProductWarehouseId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(c.Context(), s.MarketingRepo.DB(), pwIDs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"time"
|
|
||||||
|
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
supplierDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/dto"
|
||||||
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
uomDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/master/uoms/dto"
|
||||||
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
userDTO "gitlab.com/mbugroup/lti-api.git/internal/modules/users/dto"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// === DTO Structs ===
|
// === DTO Structs ===
|
||||||
@@ -18,13 +18,14 @@ type NonstockRelationDTO struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NonstockListDTO struct {
|
type NonstockListDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Flags []string `json:"flags"`
|
Flags []string `json:"flags"`
|
||||||
Uom *uomDTO.UomRelationDTO `json:"uom"`
|
Uom *uomDTO.UomRelationDTO `json:"uom"`
|
||||||
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
Suppliers []supplierDTO.SupplierRelationDTO `json:"suppliers,omitempty"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedUser *userDTO.UserRelationDTO `json:"created_user"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NonstockDetailDTO struct {
|
type NonstockDetailDTO struct {
|
||||||
@@ -76,6 +77,7 @@ func ToNonstockListDTO(e entity.Nonstock) NonstockListDTO {
|
|||||||
Name: e.Name,
|
Name: e.Name,
|
||||||
Flags: flags,
|
Flags: flags,
|
||||||
Uom: uomRef,
|
Uom: uomRef,
|
||||||
|
Suppliers: toNonstockSupplierDTOs(e.NonstockSuppliers),
|
||||||
CreatedAt: e.CreatedAt,
|
CreatedAt: e.CreatedAt,
|
||||||
UpdatedAt: e.UpdatedAt,
|
UpdatedAt: e.UpdatedAt,
|
||||||
CreatedUser: createdUser,
|
CreatedUser: createdUser,
|
||||||
@@ -95,3 +97,23 @@ func ToNonstockDetailDTO(e entity.Nonstock) NonstockDetailDTO {
|
|||||||
NonstockListDTO: ToNonstockListDTO(e),
|
NonstockListDTO: ToNonstockListDTO(e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func toNonstockSupplierDTOs(relations []entity.NonstockSupplier) []supplierDTO.SupplierRelationDTO {
|
||||||
|
if len(relations) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make([]supplierDTO.SupplierRelationDTO, 0, len(relations))
|
||||||
|
for _, relation := range relations {
|
||||||
|
if relation.Supplier.Id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result = append(result, supplierDTO.ToSupplierRelationDTO(relation.Supplier))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ func (ChickinModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *
|
|||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowChickin, utils.ChickinApprovalSteps); err != nil {
|
||||||
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
|
panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
|
chickinService := sChickin.NewChickinService(chickinRepo, kandangRepo, warehouseRepo, productWarehouseRepo, projectFlockRepo, projectflockkandangrepo, projectflockpopulationrepo, chickinDetailRepo, validate)
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create chickins")
|
||||||
}
|
}
|
||||||
|
|
||||||
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, projectFlockKandang.Id, nil)
|
latest, err := approvalSvcTx.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, projectFlockKandang.Id, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get latest approval")
|
||||||
}
|
}
|
||||||
@@ -218,9 +218,9 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
if latest == nil {
|
if latest == nil {
|
||||||
if _, err := approvalSvcTx.CreateApproval(
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
utils.ApprovalWorkflowProjectFlockKandang,
|
utils.ApprovalWorkflowChickin,
|
||||||
projectFlockKandang.Id,
|
projectFlockKandang.Id,
|
||||||
utils.ProjectFlockKandangStepPengajuan,
|
utils.ChickinStepPengajuan,
|
||||||
&approvalAction,
|
&approvalAction,
|
||||||
actorID,
|
actorID,
|
||||||
nil); err != nil {
|
nil); err != nil {
|
||||||
@@ -228,12 +228,12 @@ func (s *chickinService) CreateOne(c *fiber.Ctx, req *validation.Create) ([]enti
|
|||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
return fiber.NewError(fiber.StatusInternalServerError, "Failed to create approval")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if latest.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
|
} else if latest.StepNumber != uint16(utils.ChickinStepPengajuan) {
|
||||||
if _, err := approvalSvcTx.CreateApproval(
|
if _, err := approvalSvcTx.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
utils.ApprovalWorkflowProjectFlockKandang,
|
utils.ApprovalWorkflowChickin,
|
||||||
projectFlockKandang.Id,
|
projectFlockKandang.Id,
|
||||||
utils.ProjectFlockKandangStepPengajuan,
|
utils.ChickinStepPengajuan,
|
||||||
&approvalAction,
|
&approvalAction,
|
||||||
actorID,
|
actorID,
|
||||||
nil); err != nil {
|
nil); err != nil {
|
||||||
@@ -388,7 +388,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, id, nil)
|
latestApproval, err := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowChickin, id, nil)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to check approval status")
|
||||||
@@ -396,14 +396,14 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
if latestApproval == nil {
|
if latestApproval == nil {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for ProjectFlockKandang %d - chickins must be created first", id))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("No approval found for ProjectFlockKandang %d - chickins must be created first", id))
|
||||||
}
|
}
|
||||||
if latestApproval.StepNumber != uint16(utils.ProjectFlockKandangStepPengajuan) {
|
if latestApproval.StepNumber != uint16(utils.ChickinStepPengajuan) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ProjectFlockKandang %d cannot be approved - current status is not in PENGAJUAN stage", id))
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("ProjectFlockKandang %d cannot be approved - current status is not in PENGAJUAN stage", id))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
step := utils.ProjectFlockKandangStepPengajuan
|
step := utils.ChickinStepPengajuan
|
||||||
if action == entity.ApprovalActionApproved {
|
if action == entity.ApprovalActionApproved {
|
||||||
step = utils.ProjectFlockKandangStepDisetujui
|
step = utils.ChickinStepDisetujui
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
@@ -415,7 +415,7 @@ func (s chickinService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entit
|
|||||||
for _, approvableID := range approvableIDs {
|
for _, approvableID := range approvableIDs {
|
||||||
if _, err := approvalSvc.CreateApproval(
|
if _, err := approvalSvc.CreateApproval(
|
||||||
c.Context(),
|
c.Context(),
|
||||||
utils.ApprovalWorkflowProjectFlockKandang,
|
utils.ApprovalWorkflowChickin,
|
||||||
approvableID,
|
approvableID,
|
||||||
step,
|
step,
|
||||||
&action,
|
&action,
|
||||||
|
|||||||
+45
@@ -84,3 +84,48 @@ func (u *ProjectFlockKandangController) GetOne(c *fiber.Ctx) error {
|
|||||||
Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys, productWarehouses),
|
Data: dto.ToProjectFlockKandangDetailDTOWithAvailableQty(*result, availableQtys, productWarehouses),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (u *ProjectFlockKandangController) Closing(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
req := new(validation.Closing)
|
||||||
|
if err := c.BodyParser(req); err != nil {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ProjectFlockKandangService.Closing(c, uint(id), req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).
|
||||||
|
JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Status closing kandang diperbarui",
|
||||||
|
// Data: dto.ProjectFlockKandangDetailDTO(*result),
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *ProjectFlockKandangController) CheckClosing(c *fiber.Ctx) error {
|
||||||
|
id, err := strconv.Atoi(c.Params("id"))
|
||||||
|
if err != nil || id <= 0 {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := u.ProjectFlockKandangService.CheckClosing(c, uint(id))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Status(fiber.StatusOK).JSON(response.Success{
|
||||||
|
Code: fiber.StatusOK,
|
||||||
|
Status: "success",
|
||||||
|
Message: "Cek persyaratan closing kandang",
|
||||||
|
Data: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,9 +9,10 @@ import (
|
|||||||
|
|
||||||
sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
|
sProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/services"
|
||||||
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
rProjectFlockKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
rKandang "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
|
||||||
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"
|
||||||
|
rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
|
||||||
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"
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
@@ -28,15 +29,17 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB
|
|||||||
userRepo := rUser.NewUserRepository(db)
|
userRepo := rUser.NewUserRepository(db)
|
||||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||||
|
kandangRepo := rKandang.NewKandangRepository(db)
|
||||||
|
|
||||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||||
// register workflow steps for project flock kandang approvals
|
// register workflow steps for chickin approvals
|
||||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
|
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowProjectFlockKandang, utils.ProjectFlockKandangApprovalSteps); err != nil {
|
||||||
panic(fmt.Sprintf("failed to register project flock kandang approval workflow: %v", err))
|
panic(fmt.Sprintf("failed to register chickin approval workflow: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, validate)
|
expenseRepo := rExpense.NewExpenseRepository(db)
|
||||||
|
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, expenseRepo, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo,kandangRepo, validate)
|
||||||
userService := sUser.NewUserService(userRepo, validate)
|
userService := sUser.NewUserService(userRepo, validate)
|
||||||
|
|
||||||
ProjectFlockKandangRoutes(router, userService, projectFlockKandangService)
|
ProjectFlockKandangRoutes(router, userService, projectFlockKandangService)
|
||||||
|
|||||||
@@ -22,5 +22,8 @@ func ProjectFlockKandangRoutes(v1 fiber.Router, u user.UserService, s projectFlo
|
|||||||
|
|
||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/:id", ctrl.GetOne)
|
||||||
|
// route.Post("/:id/closing", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.Closing)
|
||||||
|
// route.Get("/:id/closing/check", m.RequirePermissions(m.PermissionProjectFlockClosing), ctrl.CheckClosing)
|
||||||
|
route.Post("/:id/closing", ctrl.Closing)
|
||||||
|
route.Get("/:id/closing/check", ctrl.CheckClosing)
|
||||||
}
|
}
|
||||||
|
|||||||
+351
-11
@@ -2,47 +2,84 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
"strings"
|
||||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
"time"
|
||||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
|
|
||||||
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
|
||||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
|
|
||||||
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
|
||||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
|
||||||
|
|
||||||
"github.com/go-playground/validator/v10"
|
"github.com/go-playground/validator/v10"
|
||||||
"github.com/gofiber/fiber/v2"
|
"github.com/gofiber/fiber/v2"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
|
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||||
|
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||||
|
m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
|
||||||
|
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/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"
|
||||||
|
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
|
||||||
|
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations"
|
||||||
|
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
|
||||||
|
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type ProjectFlockKandangService interface {
|
type ProjectFlockKandangService interface {
|
||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlockKandang, int64, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlockKandang, map[uint]float64, []entity.ProductWarehouse, error)
|
||||||
|
CheckClosing(ctx *fiber.Ctx, id uint) (*ClosingCheckResult, error)
|
||||||
|
Closing(ctx *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error)
|
||||||
|
GetProjectFlockKandangClosingDate(c *fiber.Ctx, id uint) (*time.Time, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: map[uint]float64 adalah mapping dari ProductWarehouse ID ke calculated available quantity
|
|
||||||
|
|
||||||
type projectFlockKandangService struct {
|
type projectFlockKandangService struct {
|
||||||
Log *logrus.Logger
|
Log *logrus.Logger
|
||||||
Validate *validator.Validate
|
Validate *validator.Validate
|
||||||
Repository repository.ProjectFlockKandangRepository
|
Repository repository.ProjectFlockKandangRepository
|
||||||
ApprovalSvc commonSvc.ApprovalService
|
ApprovalSvc commonSvc.ApprovalService
|
||||||
|
ExpenseRepo expenseRepo.ExpenseRepository
|
||||||
WarehouseRepo rWarehouse.WarehouseRepository
|
WarehouseRepo rWarehouse.WarehouseRepository
|
||||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||||
PopulationRepo repository.ProjectFlockPopulationRepository
|
PopulationRepo repository.ProjectFlockPopulationRepository
|
||||||
|
KandangRepo kandangRepo.KandangRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, validate *validator.Validate) ProjectFlockKandangService {
|
type ClosingCheckResult struct {
|
||||||
|
UnfinishedExpenses int64 `json:"unfinished_expenses"`
|
||||||
|
StockRemaining []StockRemainingDetail `json:"stock_remaining"`
|
||||||
|
Expenses []ExpenseSummary `json:"expenses"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type StockRemainingDetail struct {
|
||||||
|
FlagName string `json:"flag_name"`
|
||||||
|
ProductWarehouseId uint `json:"product_warehouse_id"`
|
||||||
|
ProductId uint `json:"product_id"`
|
||||||
|
ProductName string `json:"product_name"`
|
||||||
|
ProductCategory string `json:"product_category"`
|
||||||
|
Uom string `json:"uom"`
|
||||||
|
Quantity float64 `json:"quantity"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExpenseSummary struct {
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
PoNumber string `json:"po_number"`
|
||||||
|
Category string `json:"category"`
|
||||||
|
Total float64 `json:"total"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
StepName string `json:"step_name"`
|
||||||
|
Step uint16 `json:"step"`
|
||||||
|
Reference string `json:"reference_number"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository, approvalSvc commonSvc.ApprovalService, expenseRepo expenseRepo.ExpenseRepository, warehouseRepo rWarehouse.WarehouseRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, populationRepo repository.ProjectFlockPopulationRepository, kandangRepo kandangRepo.KandangRepository, validate *validator.Validate) ProjectFlockKandangService {
|
||||||
return &projectFlockKandangService{
|
return &projectFlockKandangService{
|
||||||
Log: utils.Log,
|
Log: utils.Log,
|
||||||
Validate: validate,
|
Validate: validate,
|
||||||
Repository: repo,
|
Repository: repo,
|
||||||
ApprovalSvc: approvalSvc,
|
ApprovalSvc: approvalSvc,
|
||||||
|
ExpenseRepo: expenseRepo,
|
||||||
WarehouseRepo: warehouseRepo,
|
WarehouseRepo: warehouseRepo,
|
||||||
ProductWarehouseRepo: productWarehouseRepo,
|
ProductWarehouseRepo: productWarehouseRepo,
|
||||||
PopulationRepo: populationRepo,
|
PopulationRepo: populationRepo,
|
||||||
|
KandangRepo: kandangRepo,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +203,309 @@ func (s projectFlockKandangService) getAvailableQuantities(c *fiber.Ctx, project
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s projectFlockKandangService) CheckClosing(c *fiber.Ctx, id uint) (*ClosingCheckResult, error) {
|
||||||
|
pfk, err := s.Repository.GetByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var unfinished int64
|
||||||
|
if s.ExpenseRepo != nil && s.ApprovalSvc != nil {
|
||||||
|
count, err := s.ExpenseRepo.CountUnfinishedByProjectFlockKandang(c.Context(), pfk.Id, pfk.KandangId, func(appr *entity.Approval) bool {
|
||||||
|
return appr != nil && appr.StepNumber == uint16(utils.ExpenseStepSelesai) && appr.Action != nil && *appr.Action == entity.ApprovalActionApproved
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
unfinished = count
|
||||||
|
}
|
||||||
|
|
||||||
|
stockRemain := make([]StockRemainingDetail, 0)
|
||||||
|
if s.WarehouseRepo != nil && s.ProductWarehouseRepo != nil {
|
||||||
|
warehouse, werr := s.WarehouseRepo.GetByKandangID(c.Context(), pfk.KandangId)
|
||||||
|
if werr != nil {
|
||||||
|
return nil, werr
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, flagName := range []utils.FlagType{utils.FlagPakan, utils.FlagOVK} {
|
||||||
|
productWarehouses, pwErr := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), string(flagName), warehouse.Id)
|
||||||
|
if pwErr != nil {
|
||||||
|
return nil, pwErr
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pw := range productWarehouses {
|
||||||
|
if pw.Quantity > 0 {
|
||||||
|
category := ""
|
||||||
|
if pw.Product.ProductCategory.Id != 0 {
|
||||||
|
category = pw.Product.ProductCategory.Name
|
||||||
|
}
|
||||||
|
uomName := ""
|
||||||
|
if pw.Product.Uom.Id != 0 {
|
||||||
|
uomName = pw.Product.Uom.Name
|
||||||
|
}
|
||||||
|
stockRemain = append(stockRemain, StockRemainingDetail{
|
||||||
|
FlagName: string(flagName),
|
||||||
|
ProductWarehouseId: pw.Id,
|
||||||
|
ProductId: pw.ProductId,
|
||||||
|
ProductName: pw.Product.Name,
|
||||||
|
ProductCategory: category,
|
||||||
|
Uom: uomName,
|
||||||
|
Quantity: pw.Quantity,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseSummaries := make([]ExpenseSummary, 0)
|
||||||
|
if s.ExpenseRepo != nil {
|
||||||
|
var expenses []entity.Expense
|
||||||
|
if err := s.ExpenseRepo.DB().WithContext(c.Context()).
|
||||||
|
Scopes(s.ExpenseRepo.WithProjectFlockKandangFilter(pfk.Id, pfk.KandangId)).
|
||||||
|
Preload("Nonstocks").
|
||||||
|
Find(&expenses).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(expenses) > 0 && s.ApprovalSvc != nil {
|
||||||
|
ids := make([]uint, 0, len(expenses))
|
||||||
|
for _, e := range expenses {
|
||||||
|
ids = append(ids, uint(e.Id))
|
||||||
|
}
|
||||||
|
latestMap, err := s.ApprovalSvc.LatestByTargets(c.Context(), utils.ApprovalWorkflowExpense, ids, nil)
|
||||||
|
if err == nil {
|
||||||
|
for i := range expenses {
|
||||||
|
if latest, ok := latestMap[uint(expenses[i].Id)]; ok {
|
||||||
|
expenses[i].LatestApproval = latest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, exp := range expenses {
|
||||||
|
total := 0.0
|
||||||
|
for _, ns := range exp.Nonstocks {
|
||||||
|
total += ns.Qty * ns.Price
|
||||||
|
}
|
||||||
|
|
||||||
|
status := "Pending"
|
||||||
|
stepName := ""
|
||||||
|
stepNum := uint16(0)
|
||||||
|
if exp.LatestApproval != nil {
|
||||||
|
stepName = exp.LatestApproval.StepName
|
||||||
|
stepNum = exp.LatestApproval.StepNumber
|
||||||
|
if exp.LatestApproval.Action != nil {
|
||||||
|
status = string(*exp.LatestApproval.Action)
|
||||||
|
} else if stepName != "" {
|
||||||
|
status = stepName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expenseSummaries = append(expenseSummaries, ExpenseSummary{
|
||||||
|
Id: exp.Id,
|
||||||
|
PoNumber: exp.PoNumber,
|
||||||
|
Category: exp.Category,
|
||||||
|
Total: total,
|
||||||
|
Status: status,
|
||||||
|
StepName: stepName,
|
||||||
|
Step: stepNum,
|
||||||
|
Reference: exp.ReferenceNumber,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ClosingCheckResult{
|
||||||
|
UnfinishedExpenses: unfinished,
|
||||||
|
StockRemaining: stockRemain,
|
||||||
|
Expenses: expenseSummaries,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getProjectFlockKandangClosingDate mengembalikan tanggal closing PFK jika sudah di-close.
|
||||||
|
func (s projectFlockKandangService) GetProjectFlockKandangClosingDate(c *fiber.Ctx, id uint) (*time.Time, error) {
|
||||||
|
if id == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
pfk, err := s.Repository.GetByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return pfk.ClosedAt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pfk, err := s.Repository.GetByID(c.Context(), id)
|
||||||
|
if err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "ProjectFlockKandang not found")
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.ApprovalSvc != nil {
|
||||||
|
latest, aerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlock, pfk.ProjectFlockId, nil)
|
||||||
|
if aerr != nil {
|
||||||
|
return nil, aerr
|
||||||
|
}
|
||||||
|
if latest == nil || latest.Action == nil || *latest.Action != entity.ApprovalActionApproved {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock belum berstatus aktif")
|
||||||
|
}
|
||||||
|
if latest.StepNumber != uint16(utils.ProjectFlockStepAktif) && latest.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Project flock belum berstatus aktif")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
action := strings.ToLower(strings.TrimSpace(req.Action))
|
||||||
|
now := time.Now()
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "close":
|
||||||
|
if pfk.ClosedAt != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusConflict, "Kandang sudah closed")
|
||||||
|
}
|
||||||
|
if s.ExpenseRepo != nil && s.ApprovalSvc != nil {
|
||||||
|
unfinished, err := s.ExpenseRepo.CountUnfinishedByProjectFlockKandang(c.Context(), pfk.Id, pfk.KandangId, func(appr *entity.Approval) bool {
|
||||||
|
return appr != nil && appr.StepNumber == uint16(utils.ExpenseStepSelesai) && appr.Action != nil && *appr.Action == entity.ApprovalActionApproved
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if unfinished > 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Masih ada expense belum selesai untuk kandang ini")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if s.WarehouseRepo != nil && s.ProductWarehouseRepo != nil {
|
||||||
|
warehouse, werr := s.WarehouseRepo.GetByKandangID(c.Context(), pfk.KandangId)
|
||||||
|
if werr != nil {
|
||||||
|
return nil, werr
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, flagName := range []utils.FlagType{utils.FlagPakan, utils.FlagOVK} {
|
||||||
|
productWarehouses, pwErr := s.ProductWarehouseRepo.GetByFlagAndWarehouseID(c.Context(), string(flagName), warehouse.Id)
|
||||||
|
if pwErr != nil {
|
||||||
|
return nil, pwErr
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, pw := range productWarehouses {
|
||||||
|
if pw.Quantity > 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Stok %s masih tersedia (product warehouse %d: %.2f)", flagName, pw.Id, pw.Quantity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closeTime := now
|
||||||
|
if req.ClosedDate != nil {
|
||||||
|
parsed, perr := utils.ParseDateString(strings.TrimSpace(*req.ClosedDate))
|
||||||
|
if perr != nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "closed_date tidak valid, gunakan format YYYY-MM-DD")
|
||||||
|
}
|
||||||
|
closeTime = parsed
|
||||||
|
}
|
||||||
|
if err := s.Repository.UpdateClosedAt(c.Context(), id, &closeTime); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if s.KandangRepo != nil {
|
||||||
|
if err := s.KandangRepo.UpdateStatusByIDs(
|
||||||
|
c.Context(),
|
||||||
|
[]uint{pfk.KandangId},
|
||||||
|
utils.KandangStatusNonActive,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.ApprovalSvc != nil {
|
||||||
|
closeAction := entity.ApprovalActionApproved
|
||||||
|
if _, aerr := s.ApprovalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlockKandang,
|
||||||
|
id,
|
||||||
|
utils.ProjectFlockKandangStepClosed,
|
||||||
|
&closeAction,
|
||||||
|
actorID,
|
||||||
|
nil,
|
||||||
|
); aerr != nil {
|
||||||
|
return nil, aerr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika semua kandang dalam project sudah ditutup, set approval project flock ke SELESAI.
|
||||||
|
pfks, ferr := s.Repository.GetByProjectFlockID(c.Context(), pfk.ProjectFlockId)
|
||||||
|
if ferr != nil {
|
||||||
|
return nil, ferr
|
||||||
|
}
|
||||||
|
allClosed := true
|
||||||
|
for _, item := range pfks {
|
||||||
|
if item.ClosedAt == nil {
|
||||||
|
allClosed = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if allClosed {
|
||||||
|
latestPF, lerr := s.ApprovalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlock, pfk.ProjectFlockId, nil)
|
||||||
|
if lerr != nil {
|
||||||
|
return nil, lerr
|
||||||
|
}
|
||||||
|
if latestPF == nil || latestPF.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
|
||||||
|
if _, aerr := s.ApprovalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlock,
|
||||||
|
pfk.ProjectFlockId,
|
||||||
|
utils.ProjectFlockStepSelesai,
|
||||||
|
&closeAction,
|
||||||
|
actorID,
|
||||||
|
nil,
|
||||||
|
); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
|
||||||
|
return nil, aerr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "unclose":
|
||||||
|
if pfk.ClosedAt == nil {
|
||||||
|
return nil, fiber.NewError(fiber.StatusConflict, "Kandang belum closed")
|
||||||
|
}
|
||||||
|
openNewer, err := s.Repository.HasOpenNewerPeriod(c.Context(), pfk.KandangId, pfk.Period, &pfk.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if openNewer {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Tidak dapat un-close: ada periode yang sedang berjalan")
|
||||||
|
}
|
||||||
|
if err := s.Repository.UpdateClosedAt(c.Context(), id, nil); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if s.KandangRepo != nil {
|
||||||
|
if err := s.KandangRepo.UpdateStatusByIDs(
|
||||||
|
c.Context(),
|
||||||
|
[]uint{pfk.KandangId},
|
||||||
|
utils.KandangStatusActive,
|
||||||
|
); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "action harus close atau unclose")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.Repository.GetByID(c.Context(), id)
|
||||||
|
}
|
||||||
|
|
||||||
func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {
|
func (s projectFlockKandangService) calculateAvailableQuantityForProductWarehouse(c *fiber.Ctx, projectFlockKandang *entity.ProjectFlockKandang, productWarehouse *entity.ProductWarehouse) (float64, error) {
|
||||||
availableQty := productWarehouse.Quantity
|
availableQty := productWarehouse.Quantity
|
||||||
|
|
||||||
|
|||||||
+5
@@ -22,3 +22,8 @@ type Query struct {
|
|||||||
SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"`
|
SortOrder string `query:"sort_order" validate:"omitempty,oneof=ASC DESC"`
|
||||||
StepName string `query:"step_name" validate:"omitempty,max=50"`
|
StepName string `query:"step_name" validate:"omitempty,max=50"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type Closing struct {
|
||||||
|
Action string `json:"action" validate:"required,oneof=close unclose"`
|
||||||
|
ClosedDate *string `json:"closed_date,omitempty"`
|
||||||
|
}
|
||||||
@@ -165,33 +165,6 @@ func (u *ProjectflockController) CreateOne(c *fiber.Ctx) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *ProjectflockController) UpdateOne(c *fiber.Ctx) error {
|
|
||||||
req := new(validation.Update)
|
|
||||||
param := c.Params("id")
|
|
||||||
|
|
||||||
id, err := strconv.Atoi(param)
|
|
||||||
if err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid Id")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.BodyParser(req); err != nil {
|
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := u.ProjectflockService.UpdateOne(c, req, uint(id))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return c.Status(fiber.StatusOK).
|
|
||||||
JSON(response.Success{
|
|
||||||
Code: fiber.StatusOK,
|
|
||||||
Status: "success",
|
|
||||||
Message: "Update projectflock successfully",
|
|
||||||
Data: dto.ToProjectFlockListDTO(*result),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *ProjectflockController) DeleteOne(c *fiber.Ctx) error {
|
func (u *ProjectflockController) DeleteOne(c *fiber.Ctx) error {
|
||||||
param := c.Params("id")
|
param := c.Params("id")
|
||||||
|
|
||||||
|
|||||||
+60
-21
@@ -3,6 +3,7 @@ package repository
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"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"
|
||||||
@@ -14,18 +15,20 @@ type ProjectFlockKandangRepository interface {
|
|||||||
GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error)
|
GetByID(ctx context.Context, id uint) (*entity.ProjectFlockKandang, error)
|
||||||
GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error)
|
GetByProjectFlockAndKandang(ctx context.Context, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, error)
|
||||||
GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error)
|
GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error)
|
||||||
|
UpdateClosedAt(ctx context.Context, id uint, t *time.Time) error
|
||||||
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
|
CreateMany(ctx context.Context, records []*entity.ProjectFlockKandang) error
|
||||||
DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
|
DeleteMany(ctx context.Context, projectFlockID uint, kandangIDs []uint) error
|
||||||
GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error)
|
GetAll(ctx context.Context, offset int, limit int, modifier func(*gorm.DB) *gorm.DB) ([]entity.ProjectFlockKandang, int64, error)
|
||||||
GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error)
|
GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error)
|
||||||
|
GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error)
|
||||||
ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
|
ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error)
|
||||||
HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error)
|
HasKandangsLinkedToOtherProject(ctx context.Context, kandangIDs []uint, exceptProjectID *uint) (bool, error)
|
||||||
FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error)
|
FindKandangsWithRecordings(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]entity.Kandang, error)
|
||||||
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
|
MaxPeriodByBaseName(ctx context.Context, baseName string) (int, error)
|
||||||
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
ProjectPeriodsByProjectIDs(ctx context.Context, projectIDs []uint) (map[uint]int, error)
|
||||||
|
HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error)
|
||||||
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
WithTx(tx *gorm.DB) ProjectFlockKandangRepository
|
||||||
IdExists(ctx context.Context, id uint) (bool, error)
|
IdExists(ctx context.Context, id uint) (bool, error)
|
||||||
DB() *gorm.DB
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type projectFlockKandangRepositoryImpl struct {
|
type projectFlockKandangRepositoryImpl struct {
|
||||||
@@ -75,6 +78,16 @@ func (r *projectFlockKandangRepositoryImpl) GetAll(ctx context.Context, offset i
|
|||||||
return records, total, nil
|
return records, total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockID(ctx context.Context, projectFlockID uint) ([]entity.ProjectFlockKandang, error) {
|
||||||
|
var records []entity.ProjectFlockKandang
|
||||||
|
if err := r.db.WithContext(ctx).
|
||||||
|
Where("project_flock_id = ?", projectFlockID).
|
||||||
|
Find(&records).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return records, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) {
|
func (r *projectFlockKandangRepositoryImpl) GetAllWithFilters(ctx context.Context, offset int, limit int, params interface{}) ([]entity.ProjectFlockKandang, int64, error) {
|
||||||
var records []entity.ProjectFlockKandang
|
var records []entity.ProjectFlockKandang
|
||||||
var total int64
|
var total int64
|
||||||
@@ -223,32 +236,57 @@ func (r *projectFlockKandangRepositoryImpl) GetByProjectFlockAndKandang(ctx cont
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *projectFlockKandangRepositoryImpl) GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error) {
|
func (r *projectFlockKandangRepositoryImpl) GetActiveByKandangID(ctx context.Context, kandangID uint) (*entity.ProjectFlockKandang, error) {
|
||||||
record := new(entity.ProjectFlockKandang)
|
latestApprovalSubQuery := r.db.
|
||||||
|
Table("approvals").
|
||||||
|
Select("DISTINCT ON (approvable_id) approvable_id, step_name, action_at").
|
||||||
|
Where("approvable_type = ?", "PROJECT_FLOCKS").
|
||||||
|
Order("approvable_id, action_at DESC")
|
||||||
|
|
||||||
|
var pfkID uint
|
||||||
if err := r.db.WithContext(ctx).
|
if err := r.db.WithContext(ctx).
|
||||||
|
Model(&entity.ProjectFlockKandang{}).
|
||||||
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
|
Joins("JOIN project_flocks ON project_flocks.id = project_flock_kandangs.project_flock_id").
|
||||||
Joins(`
|
Joins("JOIN (?) AS latest_approval ON latest_approval.approvable_id = project_flocks.id", latestApprovalSubQuery).
|
||||||
INNER JOIN (
|
|
||||||
SELECT DISTINCT ON (approvable_id) approvable_id, step_name, action_at
|
|
||||||
FROM approvals
|
|
||||||
WHERE approvable_type = 'PROJECT_FLOCKS'
|
|
||||||
ORDER BY approvable_id, action_at DESC
|
|
||||||
) latest_approval ON latest_approval.approvable_id = project_flocks.id
|
|
||||||
`).
|
|
||||||
Where("project_flock_kandangs.kandang_id = ?", kandangID).
|
Where("project_flock_kandangs.kandang_id = ?", kandangID).
|
||||||
|
Where("project_flock_kandangs.closed_at IS NULL").
|
||||||
Where("LOWER(latest_approval.step_name) = LOWER(?)", "Aktif").
|
Where("LOWER(latest_approval.step_name) = LOWER(?)", "Aktif").
|
||||||
Order("project_flock_kandangs.id DESC").
|
Order("project_flock_kandangs.id DESC").
|
||||||
Preload("ProjectFlock").
|
Limit(1).
|
||||||
Preload("ProjectFlock.Fcr").
|
Pluck("project_flock_kandangs.id", &pfkID).Error; err != nil {
|
||||||
Preload("ProjectFlock.Area").
|
|
||||||
Preload("ProjectFlock.Location").
|
|
||||||
Preload("ProjectFlock.CreatedUser").
|
|
||||||
Preload("ProjectFlock.Kandangs").
|
|
||||||
Preload("ProjectFlock.KandangHistory").
|
|
||||||
Preload("Kandang").
|
|
||||||
First(record).Error; err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return record, nil
|
|
||||||
|
if pfkID == 0 {
|
||||||
|
return nil, gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return r.GetByID(ctx, pfkID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) UpdateClosedAt(ctx context.Context, id uint, t *time.Time) error {
|
||||||
|
return r.db.WithContext(ctx).
|
||||||
|
Model(&entity.ProjectFlockKandang{}).
|
||||||
|
Where("id = ?", id).
|
||||||
|
Update("closed_at", t).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *projectFlockKandangRepositoryImpl) HasOpenNewerPeriod(ctx context.Context, kandangID uint, currentPeriod int, excludeID *uint) (bool, error) {
|
||||||
|
if kandangID == 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
q := r.db.WithContext(ctx).
|
||||||
|
Table("project_flock_kandangs").
|
||||||
|
Where("kandang_id = ?", kandangID).
|
||||||
|
Where("period > ?", currentPeriod).
|
||||||
|
Where("closed_at IS NULL")
|
||||||
|
if excludeID != nil {
|
||||||
|
q = q.Where("id <> ?", *excludeID)
|
||||||
|
}
|
||||||
|
var count int64
|
||||||
|
if err := q.Count(&count).Error; err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return count > 0, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *projectFlockKandangRepositoryImpl) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) {
|
func (r *projectFlockKandangRepositoryImpl) ListExistingKandangIDs(ctx context.Context, projectFlockID uint, kandangIDs []uint) ([]uint, error) {
|
||||||
@@ -269,7 +307,8 @@ func (r *projectFlockKandangRepositoryImpl) HasKandangsLinkedToOtherProject(ctx
|
|||||||
}
|
}
|
||||||
q := r.db.WithContext(ctx).
|
q := r.db.WithContext(ctx).
|
||||||
Table("project_flock_kandangs").
|
Table("project_flock_kandangs").
|
||||||
Where("kandang_id IN ?", kandangIDs)
|
Where("kandang_id IN ?", kandangIDs).
|
||||||
|
Where("closed_at IS NULL")
|
||||||
if exceptProjectID != nil {
|
if exceptProjectID != nil {
|
||||||
q = q.Where("project_flock_id <> ?", *exceptProjectID)
|
q = q.Where("project_flock_id <> ?", *exceptProjectID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ func ProjectflockRoutes(v1 fiber.Router, u user.UserService, s projectflock.Proj
|
|||||||
route.Get("/", ctrl.GetAll)
|
route.Get("/", ctrl.GetAll)
|
||||||
route.Post("/", ctrl.CreateOne)
|
route.Post("/", ctrl.CreateOne)
|
||||||
route.Get("/:id", ctrl.GetOne)
|
route.Get("/:id", ctrl.GetOne)
|
||||||
route.Patch("/:id", ctrl.UpdateOne)
|
|
||||||
route.Delete("/:id", ctrl.DeleteOne)
|
route.Delete("/:id", ctrl.DeleteOne)
|
||||||
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
route.Get("/kandangs/lookup", ctrl.LookupProjectFlockKandang)
|
||||||
route.Post("/approvals", ctrl.Approval)
|
route.Post("/approvals", ctrl.Approval)
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ type ProjectflockService interface {
|
|||||||
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error)
|
GetAll(ctx *fiber.Ctx, params *validation.Query) ([]entity.ProjectFlock, int64, map[uint]*flockDTO.FlockRelationDTO, error)
|
||||||
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error)
|
GetOne(ctx *fiber.Ctx, id uint) (*entity.ProjectFlock, *flockDTO.FlockRelationDTO, error)
|
||||||
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
|
CreateOne(ctx *fiber.Ctx, req *validation.Create) (*entity.ProjectFlock, error)
|
||||||
UpdateOne(ctx *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error)
|
|
||||||
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
|
GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID uint) (float64, error)
|
||||||
DeleteOne(ctx *fiber.Ctx, id uint) error
|
DeleteOne(ctx *fiber.Ctx, id uint) error
|
||||||
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
GetProjectFlockKandangByProjectAndKandang(ctx *fiber.Ctx, projectFlockID uint, kandangID uint) (*entity.ProjectFlockKandang, float64, error)
|
||||||
@@ -255,6 +254,16 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var location entity.Location
|
||||||
|
if err := s.Repository.DB().WithContext(c.Context()).
|
||||||
|
Where("id = ? AND area_id = ?", req.LocationId, req.AreaId).
|
||||||
|
First(&location).Error; err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "Lokasi tidak berada pada area yang diminta")
|
||||||
|
}
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Gagal memvalidasi relasi area-lokasi")
|
||||||
|
}
|
||||||
|
|
||||||
canonicalBase := baseName
|
canonicalBase := baseName
|
||||||
if s.FlockRepo != nil {
|
if s.FlockRepo != nil {
|
||||||
baseFlock, err := s.ensureFlockByName(c.Context(), actorID, baseName)
|
baseFlock, err := s.ensureFlockByName(c.Context(), actorID, baseName)
|
||||||
@@ -348,365 +357,6 @@ func (s *projectflockService) CreateOne(c *fiber.Ctx, req *validation.Create) (*
|
|||||||
return s.getOneEntityOnly(c, createBody.Id)
|
return s.getOneEntityOnly(c, createBody.Id)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s projectflockService) UpdateOne(c *fiber.Ctx, req *validation.Update, id uint) (*entity.ProjectFlock, error) {
|
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
s.Log.Errorf("Failed to fetch projectflock %d before update: %+v", id, err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch project flock")
|
|
||||||
}
|
|
||||||
updateBody := make(map[string]any)
|
|
||||||
hasBodyChanges := false
|
|
||||||
var relationChecks []commonSvc.RelationCheck
|
|
||||||
existingBase := pfutils.DeriveBaseName(existing.FlockName)
|
|
||||||
targetBaseName := existingBase
|
|
||||||
needFlockNameRegenerate := false
|
|
||||||
|
|
||||||
if req.FlockName != nil {
|
|
||||||
trimmed := strings.TrimSpace(*req.FlockName)
|
|
||||||
if trimmed == "" {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Flock name cannot be empty")
|
|
||||||
}
|
|
||||||
canonicalBase := trimmed
|
|
||||||
if s.FlockRepo != nil {
|
|
||||||
flockEntity, err := s.ensureFlockByName(c.Context(), actorID, trimmed)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
canonicalBase = flockEntity.Name
|
|
||||||
}
|
|
||||||
if !strings.EqualFold(canonicalBase, existingBase) {
|
|
||||||
needFlockNameRegenerate = true
|
|
||||||
targetBaseName = canonicalBase
|
|
||||||
hasBodyChanges = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if req.AreaId != nil {
|
|
||||||
updateBody["area_id"] = *req.AreaId
|
|
||||||
hasBodyChanges = true
|
|
||||||
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
|
||||||
Name: "Area",
|
|
||||||
ID: req.AreaId,
|
|
||||||
Exists: s.Repository.AreaExists,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if req.Category != nil {
|
|
||||||
cat := strings.ToUpper(*req.Category)
|
|
||||||
if !utils.IsValidProjectFlockCategory(cat) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid category")
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBody["category"] = cat
|
|
||||||
}
|
|
||||||
if req.FcrId != nil {
|
|
||||||
updateBody["fcr_id"] = *req.FcrId
|
|
||||||
hasBodyChanges = true
|
|
||||||
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
|
||||||
Name: "FCR",
|
|
||||||
ID: req.FcrId,
|
|
||||||
Exists: s.Repository.FcrExists,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if req.LocationId != nil {
|
|
||||||
updateBody["location_id"] = *req.LocationId
|
|
||||||
hasBodyChanges = true
|
|
||||||
relationChecks = append(relationChecks, commonSvc.RelationCheck{
|
|
||||||
Name: "Location",
|
|
||||||
ID: req.LocationId,
|
|
||||||
Exists: s.Repository.LocationExists,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(relationChecks) > 0 {
|
|
||||||
if err := commonSvc.EnsureRelations(c.Context(), relationChecks...); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var newKandangIDs []uint
|
|
||||||
hasKandangChanges := false
|
|
||||||
if req.KandangIds != nil {
|
|
||||||
hasKandangChanges = true
|
|
||||||
if len(req.KandangIds) == 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "kandang_ids cannot be empty")
|
|
||||||
}
|
|
||||||
newKandangIDs = uniqueUintSlice(req.KandangIds)
|
|
||||||
kandangs, err := s.KandangRepo.GetByIDs(c.Context(), newKandangIDs, nil)
|
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
|
||||||
}
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to fetch kandangs")
|
|
||||||
}
|
|
||||||
if len(kandangs) != len(newKandangIDs) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Some kandangs not found")
|
|
||||||
}
|
|
||||||
targetLocationID := existing.LocationId
|
|
||||||
if req.LocationId != nil && *req.LocationId > 0 {
|
|
||||||
targetLocationID = *req.LocationId
|
|
||||||
}
|
|
||||||
for _, kandang := range kandangs {
|
|
||||||
if kandang.LocationId != targetLocationID {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Kandang %d tidak berada pada lokasi yang sama dengan project flock", kandang.Id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if linked, err := s.pivotRepo().HasKandangsLinkedToOtherProject(c.Context(), newKandangIDs, &id); err != nil {
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate kandangs linkage")
|
|
||||||
} else if linked {
|
|
||||||
return nil, fiber.NewError(fiber.StatusConflict, "Beberapa kandang sudah terikat dengan project flock lain")
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
hasChanges := hasBodyChanges || hasKandangChanges
|
|
||||||
if !hasChanges {
|
|
||||||
return s.getOneEntityOnly(c, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
|
||||||
projectRepo := repository.NewProjectflockRepository(dbTransaction)
|
|
||||||
|
|
||||||
baseForGeneration := targetBaseName
|
|
||||||
if strings.TrimSpace(baseForGeneration) == "" {
|
|
||||||
baseForGeneration = existingBase
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(baseForGeneration) == "" {
|
|
||||||
baseForGeneration = strings.TrimSpace(existing.FlockName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if needFlockNameRegenerate {
|
|
||||||
newName, _, err := s.generateSequentialFlockName(c.Context(), projectRepo, baseForGeneration, 1, &id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
updateBody["flock_name"] = newName
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(updateBody) > 0 {
|
|
||||||
if err := projectRepo.PatchOne(c.Context(), id, updateBody, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if _, err := projectRepo.GetByID(c.Context(), id, nil); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.KandangIds != nil {
|
|
||||||
existingIDs := make(map[uint]struct{}, len(existing.Kandangs))
|
|
||||||
for _, k := range existing.Kandangs {
|
|
||||||
existingIDs[k.Id] = struct{}{}
|
|
||||||
}
|
|
||||||
newSet := make(map[uint]struct{}, len(newKandangIDs))
|
|
||||||
for _, kid := range newKandangIDs {
|
|
||||||
newSet[kid] = struct{}{}
|
|
||||||
}
|
|
||||||
|
|
||||||
var toDetach []uint
|
|
||||||
for kid := range existingIDs {
|
|
||||||
if _, ok := newSet[kid]; !ok {
|
|
||||||
toDetach = append(toDetach, kid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var toAttach []uint
|
|
||||||
for kid := range newSet {
|
|
||||||
if _, ok := existingIDs[kid]; !ok {
|
|
||||||
toAttach = append(toAttach, kid)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(toDetach) > 0 {
|
|
||||||
if err := s.detachKandangs(c.Context(), dbTransaction, id, toDetach, true); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(toAttach) > 0 {
|
|
||||||
currentPeriod, err := projectRepo.GetCurrentProjectPeriod(c.Context(), id)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
periods := make(map[uint]int, len(toAttach))
|
|
||||||
if currentPeriod > 0 {
|
|
||||||
for _, kid := range toAttach {
|
|
||||||
periods[kid] = currentPeriod
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
periods, err = projectRepo.GetNextPeriodsForKandangs(c.Context(), toAttach)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.attachKandangs(c.Context(), dbTransaction, id, toAttach, periods); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasChanges {
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
|
||||||
if approvalSvc != nil {
|
|
||||||
latestBeforeReset, err := approvalSvc.LatestByTarget(c.Context(), s.approvalWorkflow, id, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
shouldRecordUpdate := latestBeforeReset == nil ||
|
|
||||||
latestBeforeReset.StepNumber != uint16(utils.ProjectFlockStepPengajuan) ||
|
|
||||||
latestBeforeReset.Action == nil ||
|
|
||||||
(latestBeforeReset.Action != nil && *latestBeforeReset.Action != entity.ApprovalActionUpdated)
|
|
||||||
|
|
||||||
if shouldRecordUpdate {
|
|
||||||
action := entity.ApprovalActionUpdated
|
|
||||||
if _, err := approvalSvc.CreateApproval(
|
|
||||||
c.Context(),
|
|
||||||
utils.ApprovalWorkflowProjectFlock,
|
|
||||||
id,
|
|
||||||
utils.ProjectFlockStepPengajuan,
|
|
||||||
&action,
|
|
||||||
actorID,
|
|
||||||
nil,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if fiberErr, ok := err.(*fiber.Error); ok {
|
|
||||||
return nil, fiberErr
|
|
||||||
}
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
|
||||||
}
|
|
||||||
s.Log.Errorf("Failed to update projectflock %d: %+v", id, err)
|
|
||||||
if errors.Is(err, gorm.ErrDuplicatedKey) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusConflict, "Project flock period already exists")
|
|
||||||
}
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update project flock")
|
|
||||||
}
|
|
||||||
|
|
||||||
return s.getOneEntityOnly(c, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) {
|
|
||||||
if err := s.Validate.Struct(req); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
actorID, err := m.ActorIDFromContext(c)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var action entity.ApprovalAction
|
|
||||||
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
|
|
||||||
case string(entity.ApprovalActionRejected):
|
|
||||||
action = entity.ApprovalActionRejected
|
|
||||||
case string(entity.ApprovalActionApproved):
|
|
||||||
action = entity.ApprovalActionApproved
|
|
||||||
default:
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
|
|
||||||
}
|
|
||||||
|
|
||||||
approvableIDs := uniqueUintSlice(req.ApprovableIds)
|
|
||||||
if len(approvableIDs) == 0 {
|
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
|
|
||||||
}
|
|
||||||
|
|
||||||
step := utils.ProjectFlockStepPengajuan
|
|
||||||
if action == entity.ApprovalActionApproved {
|
|
||||||
step = utils.ProjectFlockStepAktif
|
|
||||||
}
|
|
||||||
|
|
||||||
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
|
||||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
|
||||||
kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
|
|
||||||
projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
|
|
||||||
|
|
||||||
for _, approvableID := range approvableIDs {
|
|
||||||
if _, err := projectRepoTx.GetByID(c.Context(), approvableID, nil); err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Projectflock %d not found", approvableID))
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := approvalSvc.CreateApproval(
|
|
||||||
c.Context(),
|
|
||||||
utils.ApprovalWorkflowProjectFlock,
|
|
||||||
approvableID,
|
|
||||||
step,
|
|
||||||
&action,
|
|
||||||
actorID,
|
|
||||||
req.Notes,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
switch action {
|
|
||||||
case entity.ApprovalActionApproved:
|
|
||||||
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
|
|
||||||
c.Context(),
|
|
||||||
approvableID,
|
|
||||||
utils.KandangStatusActive,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
case entity.ApprovalActionRejected:
|
|
||||||
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
|
|
||||||
c.Context(),
|
|
||||||
approvableID,
|
|
||||||
utils.KandangStatusNonActive,
|
|
||||||
); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if fiberErr, ok := err.(*fiber.Error); ok {
|
|
||||||
return nil, fiberErr
|
|
||||||
}
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
|
||||||
}
|
|
||||||
s.Log.Errorf("Failed to record approval for projectflocks %+v: %+v", approvableIDs, err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
|
|
||||||
}
|
|
||||||
|
|
||||||
updated := make([]entity.ProjectFlock, 0, len(approvableIDs))
|
|
||||||
for _, approvableID := range approvableIDs {
|
|
||||||
project, err := s.getOneEntityOnly(c, approvableID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
updated = append(updated, *project)
|
|
||||||
}
|
|
||||||
|
|
||||||
return updated, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
|
func (s projectflockService) DeleteOne(c *fiber.Ctx, id uint) error {
|
||||||
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
existing, err := s.Repository.GetByID(c.Context(), id, s.Repository.WithDefaultRelations())
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
@@ -827,6 +477,27 @@ func (s projectflockService) GetAvailableDocQuantity(ctx *fiber.Ctx, kandangID u
|
|||||||
return total, nil
|
return total, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getProjectFlockClosingDate mengembalikan tanggal closing Project Flock jika sudah mencapai step SELESAI (Approved).
|
||||||
|
// func (s projectflockService) getProjectFlockClosingDate(ctx context.Context, projectFlockID uint) (*time.Time, error) {
|
||||||
|
// if projectFlockID == 0 || s.ApprovalSvc == nil {
|
||||||
|
// return nil, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// latest, err := s.ApprovalSvc.LatestByTarget(ctx, utils.ApprovalWorkflowProjectFlock, projectFlockID, nil)
|
||||||
|
// if err != nil {
|
||||||
|
// return nil, err
|
||||||
|
// }
|
||||||
|
// if latest == nil || latest.Action == nil || *latest.Action != entity.ApprovalActionApproved {
|
||||||
|
// return nil, nil
|
||||||
|
// }
|
||||||
|
// if latest.StepNumber != uint16(utils.ProjectFlockStepSelesai) {
|
||||||
|
// return nil, nil
|
||||||
|
// }
|
||||||
|
|
||||||
|
// t := latest.ActionAt
|
||||||
|
// return &t, nil
|
||||||
|
// }
|
||||||
|
|
||||||
func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) {
|
func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint) (map[uint]int, error) {
|
||||||
if len(projectIDs) == 0 {
|
if len(projectIDs) == 0 {
|
||||||
return map[uint]int{}, nil
|
return map[uint]int{}, nil
|
||||||
@@ -834,6 +505,133 @@ func (s projectflockService) GetProjectPeriods(c *fiber.Ctx, projectIDs []uint)
|
|||||||
return s.pivotRepo().ProjectPeriodsByProjectIDs(c.Context(), projectIDs)
|
return s.pivotRepo().ProjectPeriodsByProjectIDs(c.Context(), projectIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s projectflockService) Approval(c *fiber.Ctx, req *validation.Approve) ([]entity.ProjectFlock, error) {
|
||||||
|
if err := s.Validate.Struct(req); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
actorID, err := m.ActorIDFromContext(c)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var action entity.ApprovalAction
|
||||||
|
switch strings.ToUpper(strings.TrimSpace(req.Action)) {
|
||||||
|
case string(entity.ApprovalActionRejected):
|
||||||
|
action = entity.ApprovalActionRejected
|
||||||
|
case string(entity.ApprovalActionApproved):
|
||||||
|
action = entity.ApprovalActionApproved
|
||||||
|
default:
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
|
||||||
|
}
|
||||||
|
|
||||||
|
approvableIDs := uniqueUintSlice(req.ApprovableIds)
|
||||||
|
if len(approvableIDs) == 0 {
|
||||||
|
return nil, fiber.NewError(fiber.StatusBadRequest, "approvable_ids must contain at least one id")
|
||||||
|
}
|
||||||
|
|
||||||
|
step := utils.ProjectFlockStepPengajuan
|
||||||
|
if action == entity.ApprovalActionApproved {
|
||||||
|
step = utils.ProjectFlockStepAktif
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Repository.DB().WithContext(c.Context()).Transaction(func(dbTransaction *gorm.DB) error {
|
||||||
|
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(dbTransaction))
|
||||||
|
kandangRepoTx := kandangRepository.NewKandangRepository(dbTransaction)
|
||||||
|
projectRepoTx := repository.NewProjectflockRepository(dbTransaction)
|
||||||
|
projectFlockKandangRepoTx := repository.NewProjectFlockKandangRepository(dbTransaction)
|
||||||
|
|
||||||
|
for _, approvableID := range approvableIDs {
|
||||||
|
if _, err := projectRepoTx.GetByID(c.Context(), approvableID, nil); err != nil {
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Projectflock %d not found", approvableID))
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := approvalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlock,
|
||||||
|
approvableID,
|
||||||
|
step,
|
||||||
|
&action,
|
||||||
|
actorID,
|
||||||
|
req.Notes,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case entity.ApprovalActionApproved:
|
||||||
|
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
|
||||||
|
c.Context(),
|
||||||
|
approvableID,
|
||||||
|
utils.KandangStatusActive,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pfks, err := projectFlockKandangRepoTx.GetByProjectFlockID(c.Context(), approvableID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, pfk := range pfks {
|
||||||
|
latest, lerr := approvalSvc.LatestByTarget(c.Context(), utils.ApprovalWorkflowProjectFlockKandang, pfk.Id, nil)
|
||||||
|
if lerr != nil {
|
||||||
|
return lerr
|
||||||
|
}
|
||||||
|
if latest != nil && latest.StepNumber == uint16(utils.ProjectFlockKandangStepDisetujui) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, aerr := approvalSvc.CreateApproval(
|
||||||
|
c.Context(),
|
||||||
|
utils.ApprovalWorkflowProjectFlockKandang,
|
||||||
|
pfk.Id,
|
||||||
|
utils.ProjectFlockKandangStepDisetujui,
|
||||||
|
&action,
|
||||||
|
actorID,
|
||||||
|
req.Notes,
|
||||||
|
); aerr != nil && !errors.Is(aerr, gorm.ErrDuplicatedKey) {
|
||||||
|
return aerr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case entity.ApprovalActionRejected:
|
||||||
|
if err := kandangRepoTx.UpdateStatusByProjectFlockID(
|
||||||
|
c.Context(),
|
||||||
|
approvableID,
|
||||||
|
utils.KandangStatusNonActive,
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if fiberErr, ok := err.(*fiber.Error); ok {
|
||||||
|
return nil, fiberErr
|
||||||
|
}
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, fiber.NewError(fiber.StatusNotFound, "Projectflock not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to record approval for projectflocks %+v: %+v", approvableIDs, err)
|
||||||
|
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record approval")
|
||||||
|
}
|
||||||
|
|
||||||
|
updated := make([]entity.ProjectFlock, 0, len(approvableIDs))
|
||||||
|
for _, approvableID := range approvableIDs {
|
||||||
|
project, err := s.getOneEntityOnly(c, approvableID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
updated = append(updated, *project)
|
||||||
|
}
|
||||||
|
|
||||||
|
return updated, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s projectflockService) GetPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) {
|
func (s projectflockService) GetPeriodSummary(c *fiber.Ctx, locationID uint) ([]KandangPeriodSummary, error) {
|
||||||
if locationID == 0 {
|
if locationID == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "location_id is required")
|
return nil, fiber.NewError(fiber.StatusBadRequest, "location_id is required")
|
||||||
|
|||||||
@@ -10,15 +10,6 @@ type Create struct {
|
|||||||
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"`
|
ProjectBudgets []ProjectBudget `json:"project_budgets" validate:"required,min=1,dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Update struct {
|
|
||||||
FlockName *string `json:"flock_name,omitempty" validate:"omitempty"`
|
|
||||||
AreaId *uint `json:"area_id,omitempty" validate:"omitempty,number,gt=0"`
|
|
||||||
Category *string `json:"category,omitempty" validate:"omitempty"`
|
|
||||||
FcrId *uint `json:"fcr_id,omitempty" validate:"omitempty,number,gt=0"`
|
|
||||||
LocationId *uint `json:"location_id,omitempty" validate:"omitempty,number,gt=0"`
|
|
||||||
KandangIds []uint `json:"kandang_ids,omitempty" validate:"omitempty,min=1,dive,gt=0"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Query struct {
|
type Query struct {
|
||||||
Page int `query:"page" validate:"omitempty,number,min=1"`
|
Page int `query:"page" validate:"omitempty,number,min=1"`
|
||||||
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
Limit int `query:"limit" validate:"omitempty,number,min=1,max=100"`
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
package recordings
|
|
||||||
|
|
||||||
const (
|
|
||||||
PermissionRecordingRead = "recording.read"
|
|
||||||
PermissionRecordingCreate = "recording.write"
|
|
||||||
PermissionRecordingUpdate = "recording.update"
|
|
||||||
PermissionRecordingDelete = "recording.delete"
|
|
||||||
)
|
|
||||||
@@ -14,11 +14,12 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type PurchaseRelationDTO struct {
|
type PurchaseRelationDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `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"`
|
PoDate *time.Time `json:"po_date"`
|
||||||
Notes *string `json:"notes"`
|
CreditTerm int `json:"credit_term"`
|
||||||
|
Notes *string `json:"notes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PurchaseListDTO struct {
|
type PurchaseListDTO struct {
|
||||||
@@ -42,7 +43,6 @@ type PurchaseDetailDTO struct {
|
|||||||
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
LatestApproval *approvalDTO.ApprovalRelationDTO `json:"latest_approval"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type PurchaseItemDTO struct {
|
type PurchaseItemDTO struct {
|
||||||
Id uint `json:"id"`
|
Id uint `json:"id"`
|
||||||
ProductID uint `json:"product_id"`
|
ProductID uint `json:"product_id"`
|
||||||
@@ -59,16 +59,18 @@ type PurchaseItemDTO struct {
|
|||||||
TravelNumber *string `json:"travel_number"`
|
TravelNumber *string `json:"travel_number"`
|
||||||
TravelDocumentPath *string `json:"travel_document_path"`
|
TravelDocumentPath *string `json:"travel_document_path"`
|
||||||
VehicleNumber *string `json:"vehicle_number"`
|
VehicleNumber *string `json:"vehicle_number"`
|
||||||
|
TransportPerItem *float64 `json:"transport_per_item,omitempty"`
|
||||||
|
ExpeditionVendor *supplierDTO.SupplierRelationDTO `json:"expedition_vendor,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func ToPurchaseRelationDTO(p *entity.Purchase) PurchaseRelationDTO {
|
func ToPurchaseRelationDTO(p *entity.Purchase) PurchaseRelationDTO {
|
||||||
return PurchaseRelationDTO{
|
return PurchaseRelationDTO{
|
||||||
Id: p.Id,
|
Id: p.Id,
|
||||||
PrNumber: p.PrNumber,
|
PrNumber: p.PrNumber,
|
||||||
PoNumber: p.PoNumber,
|
PoNumber: p.PoNumber,
|
||||||
PoDate: p.PoDate,
|
PoDate: p.PoDate,
|
||||||
Notes: p.Notes,
|
CreditTerm: p.CreditTerm,
|
||||||
|
Notes: p.Notes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,6 +109,20 @@ func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
|
|||||||
dto.Warehouse = &summary
|
dto.Warehouse = &summary
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if item.ExpenseNonstock != nil {
|
||||||
|
priceCopy := item.ExpenseNonstock.Price
|
||||||
|
dto.TransportPerItem = &priceCopy
|
||||||
|
|
||||||
|
if item.ExpenseNonstock.Expense != nil {
|
||||||
|
exp := item.ExpenseNonstock.Expense
|
||||||
|
|
||||||
|
if exp.Supplier != nil && exp.Supplier.Id != 0 {
|
||||||
|
supplierSummary := supplierDTO.ToSupplierRelationDTO(*exp.Supplier)
|
||||||
|
dto.ExpeditionVendor = &supplierSummary
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return dto
|
return dto
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -155,109 +155,6 @@ func (b *expenseBridge) OnItemsDeleted(ctx context.Context, _ uint, items []enti
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *expenseBridge) cleanupExistingNonstocks(ctx context.Context, updates []ExpenseReceivingPayload) error {
|
|
||||||
if len(updates) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
itemIDs := make([]uint, 0, len(updates))
|
|
||||||
for _, upd := range updates {
|
|
||||||
if upd.PurchaseItemID != 0 {
|
|
||||||
itemIDs = append(itemIDs, upd.PurchaseItemID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(itemIDs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
var links []struct {
|
|
||||||
ItemID uint
|
|
||||||
ExpenseNonstockID *uint64
|
|
||||||
}
|
|
||||||
if err := tx.Model(&entity.PurchaseItem{}).
|
|
||||||
Select("id as item_id, expense_nonstock_id").
|
|
||||||
Where("id IN ?", itemIDs).
|
|
||||||
Scan(&links).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
expenseIDs := make(map[uint64]struct{})
|
|
||||||
expenseNonstockIDs := make([]uint64, 0)
|
|
||||||
for _, link := range links {
|
|
||||||
if link.ExpenseNonstockID != nil && *link.ExpenseNonstockID != 0 {
|
|
||||||
expenseNonstockIDs = append(expenseNonstockIDs, *link.ExpenseNonstockID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(expenseNonstockIDs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, nsID := range expenseNonstockIDs {
|
|
||||||
var expenseID uint64
|
|
||||||
if err := tx.Model(&entity.ExpenseNonstock{}).
|
|
||||||
Select("expense_id").
|
|
||||||
Where("id = ?", nsID).
|
|
||||||
Scan(&expenseID).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if expenseID != 0 {
|
|
||||||
expenseIDs[expenseID] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tx.Delete(&entity.ExpenseNonstock{}, expenseNonstockIDs).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
|
|
||||||
for expenseID := range expenseIDs {
|
|
||||||
var count int64
|
|
||||||
if err := tx.Model(&entity.ExpenseNonstock{}).
|
|
||||||
Where("expense_id = ?", expenseID).
|
|
||||||
Count(&count).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if count == 0 {
|
|
||||||
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(expenseID)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Delete(&entity.Expense{}, expenseID).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *expenseBridge) cleanupEmptyExpenses(ctx context.Context, expenseIDs []uint64) error {
|
|
||||||
if len(expenseIDs) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return b.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
||||||
approvalRepoTx := commonRepo.NewApprovalRepository(tx)
|
|
||||||
for _, id := range expenseIDs {
|
|
||||||
var count int64
|
|
||||||
if err := tx.Model(&entity.ExpenseNonstock{}).
|
|
||||||
Where("expense_id = ?", id).
|
|
||||||
Count(&count).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if count == 0 {
|
|
||||||
if err := approvalRepoTx.DeleteByTarget(ctx, utils.ApprovalWorkflowExpense.String(), uint(id)); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := tx.Delete(&entity.Expense{}, id).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[uint64]struct{}, actorID uint) error {
|
func (b *expenseBridge) markExpensesUpdated(ctx context.Context, expenseIDs map[uint64]struct{}, actorID uint) error {
|
||||||
if len(expenseIDs) == 0 {
|
if len(expenseIDs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ func (s *purchaseService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
|
|
||||||
return db.
|
return db.
|
||||||
Preload("Supplier").
|
Preload("Supplier").
|
||||||
|
Preload("CreatedUser").
|
||||||
Preload("Items", func(db *gorm.DB) *gorm.DB {
|
Preload("Items", func(db *gorm.DB) *gorm.DB {
|
||||||
return db.Order("id ASC")
|
return db.Order("id ASC")
|
||||||
}).
|
}).
|
||||||
@@ -109,7 +110,10 @@ func (s *purchaseService) withRelations(db *gorm.DB) *gorm.DB {
|
|||||||
Preload("Items.Product.Flags").
|
Preload("Items.Product.Flags").
|
||||||
Preload("Items.Warehouse.Area").
|
Preload("Items.Warehouse.Area").
|
||||||
Preload("Items.Warehouse.Location").
|
Preload("Items.Warehouse.Location").
|
||||||
Preload("Items.ProductWarehouse")
|
Preload("Items.ProductWarehouse").
|
||||||
|
Preload("Items.ExpenseNonstock").
|
||||||
|
Preload("Items.ExpenseNonstock.Expense").
|
||||||
|
Preload("Items.ExpenseNonstock.Expense.Supplier")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Purchase, int64, error) {
|
func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]entity.Purchase, int64, error) {
|
||||||
@@ -121,7 +125,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|||||||
|
|
||||||
createdFrom, createdTo, err := utils.ParseDateRangeForQuery(params.CreatedFrom, params.CreatedTo)
|
createdFrom, createdTo, err := utils.ParseDateRangeForQuery(params.CreatedFrom, params.CreatedTo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, 0, fiber.NewError(fiber.StatusBadRequest, err.Error())
|
return nil, 0, utils.BadRequest(err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
purchases, total, err := s.PurchaseRepo.GetAll(c.Context(), offset, params.Limit, func(db *gorm.DB) *gorm.DB {
|
||||||
@@ -180,7 +184,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to get purchases: %+v", err)
|
s.Log.Errorf("Failed to get purchases: %+v", err)
|
||||||
return nil, 0, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchases")
|
return nil, 0, utils.Internal("Failed to get purchases")
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range purchases {
|
for i := range purchases {
|
||||||
@@ -193,19 +197,7 @@ func (s *purchaseService) GetAll(c *fiber.Ctx, params *validation.Query) ([]enti
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *purchaseService) GetOne(c *fiber.Ctx, id uint) (*entity.Purchase, error) {
|
func (s *purchaseService) GetOne(c *fiber.Ctx, id uint) (*entity.Purchase, error) {
|
||||||
purchase, err := s.PurchaseRepo.GetByID(c.Context(), id, s.withRelations)
|
return s.loadPurchase(c.Context(), id)
|
||||||
if err != nil {
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
|
||||||
}
|
|
||||||
s.Log.Errorf("Failed to get purchase: %+v", err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.attachLatestApproval(c.Context(), purchase); err != nil {
|
|
||||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err)
|
|
||||||
}
|
|
||||||
return purchase, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) {
|
func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) {
|
||||||
@@ -220,10 +212,10 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|||||||
|
|
||||||
if _, err := s.SupplierRepo.GetByID(c.Context(), req.SupplierID, nil); err != nil {
|
if _, err := s.SupplierRepo.GetByID(c.Context(), req.SupplierID, nil); err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Supplier not found")
|
return nil, utils.NotFound("Supplier not found")
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to get supplier: %+v", err)
|
s.Log.Errorf("Failed to get supplier: %+v", err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get supplier")
|
return nil, utils.Internal("Failed to get supplier")
|
||||||
}
|
}
|
||||||
|
|
||||||
type aggregatedItem struct {
|
type aggregatedItem struct {
|
||||||
@@ -234,7 +226,7 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(req.Items) == 0 {
|
if len(req.Items) == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty")
|
return nil, utils.BadRequest("Items must not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
warehouseCache := make(map[uint]*entity.Warehouse)
|
warehouseCache := make(map[uint]*entity.Warehouse)
|
||||||
@@ -249,24 +241,27 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Warehouse %d not found", id))
|
return nil, nil, utils.NotFound(fmt.Sprintf("Warehouse %d not found", id))
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to get warehouse %d: %+v", id, err)
|
s.Log.Errorf("Failed to get warehouse %d: %+v", id, err)
|
||||||
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
return nil, nil, utils.Internal("Failed to get warehouse")
|
||||||
}
|
}
|
||||||
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
if warehouse.KandangId == nil || *warehouse.KandangId == 0 {
|
||||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse %d is not linked to a kandang", id))
|
return nil, nil, utils.BadRequest(fmt.Sprintf("Warehouse %d is not linked to a kandang", id))
|
||||||
}
|
}
|
||||||
var pfkID *uint
|
var pfkID *uint
|
||||||
if s.ProjectFlockKandangRepo != nil {
|
if s.ProjectFlockKandangRepo != nil {
|
||||||
if pfk, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(c.Context(), uint(*warehouse.KandangId)); err == nil && pfk != nil {
|
if pfk, err := s.ProjectFlockKandangRepo.GetActiveByKandangID(c.Context(), uint(*warehouse.KandangId)); err == nil && pfk != nil {
|
||||||
|
if pfk.ClosedAt != nil {
|
||||||
|
return nil, nil, utils.BadRequest("Project sudah closing")
|
||||||
|
}
|
||||||
idCopy := uint(pfk.Id)
|
idCopy := uint(pfk.Id)
|
||||||
pfkID = &idCopy
|
pfkID = &idCopy
|
||||||
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse %d has no active project flock", id))
|
return nil, nil, utils.BadRequest(fmt.Sprintf("Warehouse %d has no active project flock", id))
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
s.Log.Errorf("Failed to validate project flock for warehouse %d: %+v", id, err)
|
s.Log.Errorf("Failed to validate project flock for warehouse %d: %+v", id, err)
|
||||||
return nil, nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate project flock")
|
return nil, nil, utils.Internal("Failed to validate project flock")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,10 +282,10 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|||||||
linked, err := s.ProductRepo.IsLinkedToSupplier(c.Context(), item.ProductID, req.SupplierID)
|
linked, err := s.ProductRepo.IsLinkedToSupplier(c.Context(), item.ProductID, req.SupplierID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to validate product %d for supplier %d: %+v", item.ProductID, req.SupplierID, err)
|
s.Log.Errorf("Failed to validate product %d for supplier %d: %+v", item.ProductID, req.SupplierID, err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product for supplier")
|
return nil, utils.Internal("Failed to validate product for supplier")
|
||||||
}
|
}
|
||||||
if !linked {
|
if !linked {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Product %d is not linked to supplier %d", item.ProductID, req.SupplierID))
|
return nil, utils.BadRequest(fmt.Sprintf("Product %d is not linked to supplier %d", item.ProductID, req.SupplierID))
|
||||||
}
|
}
|
||||||
productSupplierCache[item.ProductID] = true
|
productSupplierCache[item.ProductID] = true
|
||||||
}
|
}
|
||||||
@@ -315,17 +310,13 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|||||||
}
|
}
|
||||||
|
|
||||||
var dueDate *time.Time
|
var dueDate *time.Time
|
||||||
if req.DueDate != nil && strings.TrimSpace(*req.DueDate) != "" {
|
now := time.Now().UTC()
|
||||||
parsed, err := utils.ParseDateString(strings.TrimSpace(*req.DueDate))
|
d := now.AddDate(0, 0, req.CreditTerm)
|
||||||
if err != nil {
|
dueDate = &d
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid due_date, expected YYYY-MM-DD")
|
|
||||||
}
|
|
||||||
parsed = parsed.UTC()
|
|
||||||
dueDate = &parsed
|
|
||||||
}
|
|
||||||
|
|
||||||
purchase := &entity.Purchase{
|
purchase := &entity.Purchase{
|
||||||
SupplierId: uint(req.SupplierID),
|
SupplierId: uint(req.SupplierID),
|
||||||
|
CreditTerm: req.CreditTerm,
|
||||||
DueDate: dueDate,
|
DueDate: dueDate,
|
||||||
Notes: req.Notes,
|
Notes: req.Notes,
|
||||||
CreatedBy: uint(actorID),
|
CreatedBy: uint(actorID),
|
||||||
@@ -373,13 +364,13 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
|
|||||||
})
|
})
|
||||||
if transactionErr != nil {
|
if transactionErr != nil {
|
||||||
s.Log.Errorf("Failed to create purchase: %+v", transactionErr)
|
s.Log.Errorf("Failed to create purchase: %+v", transactionErr)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create purchase")
|
return nil, utils.Internal("Failed to create purchase")
|
||||||
}
|
}
|
||||||
|
|
||||||
created, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
created, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to load created purchase: %+v", err)
|
s.Log.Errorf("Failed to load created purchase: %+v", err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
return nil, utils.Internal("Failed to load purchase")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.attachLatestApproval(c.Context(), created); err != nil {
|
if err := s.attachLatestApproval(c.Context(), created); err != nil {
|
||||||
@@ -405,17 +396,15 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
purchase, err := s.loadPurchase(ctx, id)
|
||||||
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
return nil, err
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
|
||||||
}
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.attachLatestApproval(ctx, purchase); err != nil {
|
if action == entity.ApprovalActionApproved {
|
||||||
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
if err := s.ensureProjectFlockNotClosedForPurchase(ctx, purchase); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var latestStep uint16
|
var latestStep uint16
|
||||||
@@ -429,7 +418,7 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
|||||||
|
|
||||||
isInitialApproval := latestStep < uint16(utils.PurchaseStepStaffPurchase)
|
isInitialApproval := latestStep < uint16(utils.PurchaseStepStaffPurchase)
|
||||||
if isInitialApproval && latestStep != uint16(utils.PurchaseStepPengajuan) {
|
if isInitialApproval && latestStep != uint16(utils.PurchaseStepPengajuan) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase is not ready for staff approval")
|
return nil, utils.BadRequest("Purchase is not ready for staff approval")
|
||||||
}
|
}
|
||||||
|
|
||||||
hasReceivingData := false
|
hasReceivingData := false
|
||||||
@@ -442,8 +431,8 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
|||||||
|
|
||||||
syncReceiving := !isInitialApproval && hasReceivingData
|
syncReceiving := !isInitialApproval && hasReceivingData
|
||||||
|
|
||||||
if len(req.Items) == 0 {
|
if action == entity.ApprovalActionApproved && len(req.Items) == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty for staff approval")
|
return nil, utils.BadRequest("Items must not be empty for staff approval")
|
||||||
}
|
}
|
||||||
|
|
||||||
payload, err := s.buildStaffAdjustmentPayload(c.Context(), purchase, req, syncReceiving)
|
payload, err := s.buildStaffAdjustmentPayload(c.Context(), purchase, req, syncReceiving)
|
||||||
@@ -491,18 +480,18 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
|
|||||||
})
|
})
|
||||||
if transactionErr != nil {
|
if transactionErr != nil {
|
||||||
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found")
|
return nil, utils.NotFound("Purchase item not found")
|
||||||
}
|
}
|
||||||
if isInitialApproval {
|
if isInitialApproval {
|
||||||
s.Log.Errorf("Failed to approve purchase %d: %+v", purchase.Id, transactionErr)
|
s.Log.Errorf("Failed to approve purchase %d: %+v", purchase.Id, transactionErr)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to approve purchase")
|
return nil, utils.Internal("Failed to approve purchase")
|
||||||
}
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to update purchase pricing")
|
return nil, utils.Internal("Failed to update purchase pricing")
|
||||||
}
|
}
|
||||||
|
|
||||||
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
return nil, utils.Internal("Failed to load purchase")
|
||||||
}
|
}
|
||||||
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
||||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
||||||
@@ -526,22 +515,19 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
purchase, err := s.PurchaseRepo.GetByID(c.Context(), id, s.withRelations)
|
purchase, err := s.loadPurchase(c.Context(), id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
return nil, err
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
}
|
||||||
|
|
||||||
|
if action == entity.ApprovalActionApproved {
|
||||||
|
if err := s.ensureProjectFlockNotClosedForPurchase(c.Context(), purchase); err != nil {
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to get purchase: %+v", err)
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.attachLatestApproval(c.Context(), purchase); err != nil {
|
|
||||||
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if purchase.LatestApproval == nil ||
|
if purchase.LatestApproval == nil ||
|
||||||
purchase.LatestApproval.StepNumber < uint16(utils.PurchaseStepStaffPurchase) {
|
purchase.LatestApproval.StepNumber < uint16(utils.PurchaseStepStaffPurchase) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must reach staff purchase step before manager approval")
|
return nil, utils.BadRequest("Purchase must reach staff purchase step before manager approval")
|
||||||
}
|
}
|
||||||
|
|
||||||
if action == entity.ApprovalActionRejected {
|
if action == entity.ApprovalActionRejected {
|
||||||
@@ -601,7 +587,7 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val
|
|||||||
})
|
})
|
||||||
if transactionErr != nil {
|
if transactionErr != nil {
|
||||||
s.Log.Errorf("Failed to approve manager purchase %d: %+v", purchase.Id, transactionErr)
|
s.Log.Errorf("Failed to approve manager purchase %d: %+v", purchase.Id, transactionErr)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to generate purchase order")
|
return nil, utils.Internal("Failed to generate purchase order")
|
||||||
}
|
}
|
||||||
|
|
||||||
if generatedNumber != "" {
|
if generatedNumber != "" {
|
||||||
@@ -612,7 +598,7 @@ func (s *purchaseService) ApproveManagerPurchase(c *fiber.Ctx, id uint, req *val
|
|||||||
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to load purchase after manager approval: %+v", err)
|
s.Log.Errorf("Failed to load purchase after manager approval: %+v", err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
return nil, utils.Internal("Failed to load purchase")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
||||||
@@ -639,29 +625,26 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
purchase, err := s.loadPurchase(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
return nil, err
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
|
||||||
}
|
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase ")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if purchase.PoNumber == nil || strings.TrimSpace(*purchase.PoNumber) == "" {
|
if purchase.PoNumber == nil || strings.TrimSpace(*purchase.PoNumber) == "" {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase order has not been generated")
|
return nil, utils.BadRequest("Purchase order has not been generated")
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.attachLatestApproval(c.Context(), purchase); err != nil {
|
|
||||||
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if purchase.LatestApproval == nil ||
|
if purchase.LatestApproval == nil ||
|
||||||
purchase.LatestApproval.StepNumber < uint16(utils.PurchaseStepManager) {
|
purchase.LatestApproval.StepNumber < uint16(utils.PurchaseStepManager) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must be approved by manager before receiving products")
|
return nil, utils.BadRequest("Purchase must be approved by manager before receiving products")
|
||||||
|
}
|
||||||
|
if action == entity.ApprovalActionApproved {
|
||||||
|
if err := s.ensureProjectFlockNotClosedForPurchase(ctx, purchase); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if action == entity.ApprovalActionApproved && len(req.Items) == 0 {
|
if action == entity.ApprovalActionApproved && len(req.Items) == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Receiving data must not be empty")
|
return nil, utils.BadRequest("Receiving data must not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
if action == entity.ApprovalActionRejected {
|
if action == entity.ApprovalActionRejected {
|
||||||
@@ -670,7 +653,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
}
|
}
|
||||||
updated, err := s.PurchaseRepo.GetByID(ctx, purchase.Id, s.withRelations)
|
updated, err := s.PurchaseRepo.GetByID(ctx, purchase.Id, s.withRelations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
return nil, utils.Internal("Failed to load purchase")
|
||||||
}
|
}
|
||||||
if err := s.attachLatestApproval(ctx, updated); err != nil {
|
if err := s.attachLatestApproval(ctx, updated); err != nil {
|
||||||
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
||||||
@@ -696,17 +679,22 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
|
|
||||||
visitedItems := make(map[uint]struct{}, len(req.Items))
|
visitedItems := make(map[uint]struct{}, len(req.Items))
|
||||||
prepared := make([]preparedReceiving, 0, len(req.Items))
|
prepared := make([]preparedReceiving, 0, len(req.Items))
|
||||||
|
var earliestReceived *time.Time
|
||||||
for _, payload := range req.Items {
|
for _, payload := range req.Items {
|
||||||
item, exists := itemMap[payload.PurchaseItemID]
|
item, exists := itemMap[payload.PurchaseItemID]
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Purchase item %d not found", payload.PurchaseItemID))
|
return nil, utils.BadRequest(fmt.Sprintf("Purchase item %d not found", payload.PurchaseItemID))
|
||||||
}
|
}
|
||||||
|
|
||||||
receivedDate, err := utils.ParseDateString(payload.ReceivedDate)
|
receivedDate, err := utils.ParseDateString(payload.ReceivedDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Invalid received_date for item %d", payload.PurchaseItemID))
|
return nil, utils.BadRequest(fmt.Sprintf("Invalid received_date for item %d", payload.PurchaseItemID))
|
||||||
}
|
}
|
||||||
receivedDate = receivedDate.UTC()
|
receivedDate = receivedDate.UTC()
|
||||||
|
if earliestReceived == nil || receivedDate.Before(*earliestReceived) {
|
||||||
|
copy := receivedDate
|
||||||
|
earliestReceived = ©
|
||||||
|
}
|
||||||
|
|
||||||
warehouseID := uint(item.WarehouseId)
|
warehouseID := uint(item.WarehouseId)
|
||||||
overrideWarehouse := false
|
overrideWarehouse := false
|
||||||
@@ -715,10 +703,10 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
overrideWarehouse = true
|
overrideWarehouse = true
|
||||||
}
|
}
|
||||||
if warehouseID == 0 {
|
if warehouseID == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse must be specified for item %d", payload.PurchaseItemID))
|
return nil, utils.BadRequest(fmt.Sprintf("Warehouse must be specified for item %d", payload.PurchaseItemID))
|
||||||
}
|
}
|
||||||
if payload.WarehouseID != nil && uint(item.WarehouseId) != warehouseID {
|
if payload.WarehouseID != nil && uint(item.WarehouseId) != warehouseID {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Receiving does not allow changing warehouse")
|
return nil, utils.BadRequest("Receiving does not allow changing warehouse")
|
||||||
}
|
}
|
||||||
|
|
||||||
var receivedQty float64
|
var receivedQty float64
|
||||||
@@ -728,14 +716,14 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
receivedQty = item.SubQty
|
receivedQty = item.SubQty
|
||||||
}
|
}
|
||||||
if receivedQty < 0 {
|
if receivedQty < 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Received quantity for item %d cannot be negative", payload.PurchaseItemID))
|
return nil, utils.BadRequest(fmt.Sprintf("Received quantity for item %d cannot be negative", payload.PurchaseItemID))
|
||||||
}
|
}
|
||||||
if receivedQty > item.SubQty {
|
if receivedQty > item.SubQty {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Received quantity for item %d cannot exceed ordered quantity (%.3f)", payload.PurchaseItemID, item.SubQty))
|
return nil, utils.BadRequest(fmt.Sprintf("Received quantity for item %d cannot exceed ordered quantity (%.3f)", payload.PurchaseItemID, item.SubQty))
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, dup := visitedItems[payload.PurchaseItemID]; dup {
|
if _, dup := visitedItems[payload.PurchaseItemID]; dup {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate receiving data for item %d", payload.PurchaseItemID))
|
return nil, utils.BadRequest(fmt.Sprintf("Duplicate receiving data for item %d", payload.PurchaseItemID))
|
||||||
}
|
}
|
||||||
visitedItems[payload.PurchaseItemID] = struct{}{}
|
visitedItems[payload.PurchaseItemID] = struct{}{}
|
||||||
|
|
||||||
@@ -747,7 +735,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
var transportPerItem *float64
|
var transportPerItem *float64
|
||||||
if payload.TransportPerItem != nil {
|
if payload.TransportPerItem != nil {
|
||||||
if *payload.TransportPerItem < 0 {
|
if *payload.TransportPerItem < 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("transport_per_item for item %d cannot be negative", payload.PurchaseItemID))
|
return nil, utils.BadRequest(fmt.Sprintf("transport_per_item for item %d cannot be negative", payload.PurchaseItemID))
|
||||||
}
|
}
|
||||||
val := *payload.TransportPerItem
|
val := *payload.TransportPerItem
|
||||||
transportPerItem = &val
|
transportPerItem = &val
|
||||||
@@ -768,7 +756,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
// Require receiving payload to cover all purchase items so that
|
// Require receiving payload to cover all purchase items so that
|
||||||
// receiving cannot be submitted partially item-by-item.
|
// receiving cannot be submitted partially item-by-item.
|
||||||
if len(visitedItems) != len(itemMap) {
|
if len(visitedItems) != len(itemMap) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Receiving data must be provided for all purchase items")
|
return nil, utils.BadRequest("Receiving data must be provided for all purchase items")
|
||||||
}
|
}
|
||||||
|
|
||||||
receivingAction := action
|
receivingAction := action
|
||||||
@@ -792,7 +780,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to inspect receiving approval for purchase %d: %+v", purchase.Id, err)
|
s.Log.Errorf("Failed to inspect receiving approval for purchase %d: %+v", purchase.Id, err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record purchase receiving")
|
return nil, utils.Internal("Failed to record purchase receiving")
|
||||||
}
|
}
|
||||||
|
|
||||||
if latestReceiving != nil {
|
if latestReceiving != nil {
|
||||||
@@ -882,6 +870,16 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update due_date based on earliest received date when receiving approved.
|
||||||
|
if earliestReceived != nil {
|
||||||
|
due := earliestReceived.AddDate(0, 0, purchase.CreditTerm)
|
||||||
|
if err := tx.Model(&entity.Purchase{}).
|
||||||
|
Where("id = ?", purchase.Id).
|
||||||
|
Update("due_date", due).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if s.FifoSvc != nil {
|
if s.FifoSvc != nil {
|
||||||
for _, adj := range fifoAdds {
|
for _, adj := range fifoAdds {
|
||||||
if adj.pwID == 0 || adj.qty <= 0 {
|
if adj.pwID == 0 || adj.qty <= 0 {
|
||||||
@@ -903,15 +901,15 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
})
|
})
|
||||||
if transactionErr != nil {
|
if transactionErr != nil {
|
||||||
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found for receiving")
|
return nil, utils.NotFound("Purchase item not found for receiving")
|
||||||
}
|
}
|
||||||
s.Log.Errorf("Failed to save purchase receiving %d: %+v", purchase.Id, transactionErr)
|
s.Log.Errorf("Failed to save purchase receiving %d: %+v", purchase.Id, transactionErr)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to record purchase receiving")
|
return nil, utils.Internal("Failed to record purchase receiving")
|
||||||
}
|
}
|
||||||
|
|
||||||
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchase.Id, s.withRelations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase ")
|
return nil, utils.Internal("Failed to load purchase ")
|
||||||
}
|
}
|
||||||
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
||||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
||||||
@@ -936,7 +934,7 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
|
|||||||
if fe, ok := err.(*fiber.Error); ok {
|
if fe, ok := err.(*fiber.Error); ok {
|
||||||
return nil, fe
|
return nil, fe
|
||||||
}
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to sync expense")
|
return nil, utils.Internal("Failed to sync expense")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create approvals only after expense sync succeeds
|
// Create approvals only after expense sync succeeds
|
||||||
@@ -959,20 +957,23 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
|||||||
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
return nil, utils.NotFound("Purchase not found")
|
||||||
}
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
return nil, utils.Internal("Failed to get purchase")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.attachLatestApproval(ctx, purchase); err != nil {
|
if err := s.attachLatestApproval(ctx, purchase); err != nil {
|
||||||
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
s.Log.Warnf("Unable to load latest approval for purchase %d: %+v", id, err)
|
||||||
}
|
}
|
||||||
|
if err := s.ensureProjectFlockNotClosedForPurchase(ctx, purchase); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if purchase.LatestApproval == nil || purchase.LatestApproval.StepNumber == uint16(utils.PurchaseStepPengajuan) {
|
if purchase.LatestApproval == nil || purchase.LatestApproval.StepNumber == uint16(utils.PurchaseStepPengajuan) {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase cannot delete items before staff purchase approval")
|
return nil, utils.BadRequest("Purchase cannot delete items before staff purchase approval")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(purchase.Items) == 0 {
|
if len(purchase.Items) == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase has no items to delete")
|
return nil, utils.BadRequest("Purchase has no items to delete")
|
||||||
}
|
}
|
||||||
|
|
||||||
requested := make(map[uint]struct{}, len(req.ItemIDs))
|
requested := make(map[uint]struct{}, len(req.ItemIDs))
|
||||||
@@ -992,7 +993,7 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(toDelete) == 0 {
|
if len(toDelete) == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Requested items were not found in this purchase")
|
return nil, utils.BadRequest("Requested items were not found in this purchase")
|
||||||
}
|
}
|
||||||
|
|
||||||
toDeleteSet := make(map[uint]struct{}, len(toDelete))
|
toDeleteSet := make(map[uint]struct{}, len(toDelete))
|
||||||
@@ -1007,7 +1008,7 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(purchase.Items)-len(toDelete) <= 0 {
|
if len(purchase.Items)-len(toDelete) <= 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase must keep at least one item")
|
return nil, utils.BadRequest("Purchase must keep at least one item")
|
||||||
}
|
}
|
||||||
|
|
||||||
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
@@ -1021,9 +1022,9 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
|||||||
})
|
})
|
||||||
if transactionErr != nil {
|
if transactionErr != nil {
|
||||||
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
||||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found")
|
return nil, utils.NotFound("Purchase item not found")
|
||||||
}
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to delete purchase items")
|
return nil, utils.Internal("Failed to delete purchase items")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(itemsToDelete) > 0 {
|
if len(itemsToDelete) > 0 {
|
||||||
@@ -1032,13 +1033,13 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
|||||||
if fe, ok := err.(*fiber.Error); ok {
|
if fe, ok := err.(*fiber.Error); ok {
|
||||||
return nil, fe
|
return nil, fe
|
||||||
}
|
}
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to sync expense")
|
return nil, utils.Internal("Failed to sync expense")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updated, err := s.PurchaseRepo.GetByID(ctx, purchase.Id, s.withRelations)
|
updated, err := s.PurchaseRepo.GetByID(ctx, purchase.Id, s.withRelations)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
return nil, utils.Internal("Failed to load purchase")
|
||||||
}
|
}
|
||||||
if err := s.attachLatestApproval(ctx, updated); err != nil {
|
if err := s.attachLatestApproval(ctx, updated); err != nil {
|
||||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
||||||
@@ -1049,16 +1050,16 @@ func (s *purchaseService) DeleteItems(c *fiber.Ctx, id uint, req *validation.Del
|
|||||||
|
|
||||||
func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
||||||
if id == 0 {
|
if id == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase id")
|
return utils.BadRequest("Invalid purchase id")
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := c.Context()
|
ctx := c.Context()
|
||||||
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
purchase, err := s.loadPurchase(ctx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
return err
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
}
|
||||||
}
|
if err := s.ensureProjectFlockNotClosedForPurchase(ctx, purchase); err != nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase")
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
itemsToDelete := make([]entity.PurchaseItem, len(purchase.Items))
|
itemsToDelete := make([]entity.PurchaseItem, len(purchase.Items))
|
||||||
@@ -1080,9 +1081,9 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
|||||||
})
|
})
|
||||||
if transactionErr != nil {
|
if transactionErr != nil {
|
||||||
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
||||||
return fiber.NewError(fiber.StatusNotFound, "Purchase not found")
|
return utils.NotFound("Purchase not found")
|
||||||
}
|
}
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to delete purchase")
|
return utils.Internal("Failed to delete purchase")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(itemsToDelete) > 0 {
|
if len(itemsToDelete) > 0 {
|
||||||
@@ -1091,7 +1092,7 @@ func (s *purchaseService) DeletePurchase(c *fiber.Ctx, id uint) error {
|
|||||||
if fe, ok := err.(*fiber.Error); ok {
|
if fe, ok := err.(*fiber.Error); ok {
|
||||||
return fe
|
return fe
|
||||||
}
|
}
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Failed to sync expense")
|
return utils.Internal("Failed to sync expense")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1109,7 +1110,7 @@ func (s *purchaseService) createPurchaseApproval(
|
|||||||
allowDuplicate bool,
|
allowDuplicate bool,
|
||||||
) error {
|
) error {
|
||||||
if purchaseID == 0 {
|
if purchaseID == 0 {
|
||||||
return fiber.NewError(fiber.StatusBadRequest, "Purchase is invalid for approval")
|
return utils.BadRequest("Purchase is invalid for approval")
|
||||||
}
|
}
|
||||||
if actorID == 0 {
|
if actorID == 0 {
|
||||||
actorID = 1
|
actorID = 1
|
||||||
@@ -1117,7 +1118,7 @@ func (s *purchaseService) createPurchaseApproval(
|
|||||||
|
|
||||||
svc := s.approvalServiceForDB(db)
|
svc := s.approvalServiceForDB(db)
|
||||||
if svc == nil {
|
if svc == nil {
|
||||||
return fiber.NewError(fiber.StatusInternalServerError, "Approval service not available")
|
return utils.Internal("Approval service not available")
|
||||||
}
|
}
|
||||||
|
|
||||||
modifier := func(db *gorm.DB) *gorm.DB {
|
modifier := func(db *gorm.DB) *gorm.DB {
|
||||||
@@ -1175,7 +1176,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|||||||
syncReceiving bool,
|
syncReceiving bool,
|
||||||
) (*staffAdjustmentPayload, error) {
|
) (*staffAdjustmentPayload, error) {
|
||||||
if len(req.Items) == 0 {
|
if len(req.Items) == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty")
|
return nil, utils.BadRequest("Items must not be empty")
|
||||||
}
|
}
|
||||||
|
|
||||||
requestItems := make(map[uint]validation.StaffPurchaseApprovalItem, len(req.Items))
|
requestItems := make(map[uint]validation.StaffPurchaseApprovalItem, len(req.Items))
|
||||||
@@ -1187,7 +1188,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, exists := requestItems[item.PurchaseItemID]; exists {
|
if _, exists := requestItems[item.PurchaseItemID]; exists {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Duplicate pricing data for item %d", item.PurchaseItemID))
|
return nil, utils.BadRequest(fmt.Sprintf("Duplicate pricing data for item %d", item.PurchaseItemID))
|
||||||
}
|
}
|
||||||
requestItems[item.PurchaseItemID] = item
|
requestItems[item.PurchaseItemID] = item
|
||||||
}
|
}
|
||||||
@@ -1205,34 +1206,31 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|||||||
allowedWarehouses[item.WarehouseId] = struct{}{}
|
allowedWarehouses[item.WarehouseId] = struct{}{}
|
||||||
}
|
}
|
||||||
if len(allowedWarehouses) == 0 && len(newPayloads) > 0 {
|
if len(allowedWarehouses) == 0 && len(newPayloads) > 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "No available warehouses for this purchase")
|
return nil, utils.BadRequest("No available warehouses for this purchase")
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, item := range purchase.Items {
|
for _, item := range purchase.Items {
|
||||||
data, ok := requestItems[item.Id]
|
data, ok := requestItems[item.Id]
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Missing pricing data for item %d", item.Id))
|
return nil, utils.BadRequest(fmt.Sprintf("Missing pricing data for item %d", item.Id))
|
||||||
}
|
}
|
||||||
if data.ProductID != 0 && data.ProductID != item.ProductId {
|
if data.ProductID != 0 && data.ProductID != item.ProductId {
|
||||||
return nil, fiber.NewError(
|
return nil, utils.BadRequest(fmt.Sprintf("Cannot change product for item %d. Delete the item and create a new one instead", item.Id))
|
||||||
fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Cannot change product for item %d. Delete the item and create a new one instead", item.Id),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if data.WarehouseID != 0 && data.WarehouseID != item.WarehouseId {
|
if data.WarehouseID != 0 && data.WarehouseID != item.WarehouseId {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Warehouse mismatch for item %d", item.Id))
|
return nil, utils.BadRequest(fmt.Sprintf("Warehouse mismatch for item %d", item.Id))
|
||||||
}
|
}
|
||||||
|
|
||||||
effectiveQty := item.SubQty
|
effectiveQty := item.SubQty
|
||||||
if data.Qty != nil {
|
if data.Qty != nil {
|
||||||
if *data.Qty <= 0 {
|
if *data.Qty <= 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity for item %d must be greater than 0", item.Id))
|
return nil, utils.BadRequest(fmt.Sprintf("Quantity for item %d must be greater than 0", item.Id))
|
||||||
}
|
}
|
||||||
if item.TotalUsed > 0 && *data.Qty < item.TotalUsed {
|
if item.TotalUsed > 0 && *data.Qty < item.TotalUsed {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity for item %d cannot be lower than used amount (%.3f)", item.Id, item.TotalUsed))
|
return nil, utils.BadRequest(fmt.Sprintf("Quantity for item %d cannot be lower than used amount (%.3f)", item.Id, item.TotalUsed))
|
||||||
}
|
}
|
||||||
if (item.TotalQty > 0 || item.TotalUsed > 0) && !syncReceiving {
|
if (item.TotalQty > 0 || item.TotalUsed > 0) && !syncReceiving {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Cannot change quantity for item %d because it already has receiving data", item.Id))
|
return nil, utils.BadRequest(fmt.Sprintf("Cannot change quantity for item %d because it already has receiving data", item.Id))
|
||||||
}
|
}
|
||||||
effectiveQty = *data.Qty
|
effectiveQty = *data.Qty
|
||||||
}
|
}
|
||||||
@@ -1261,7 +1259,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|||||||
delete(requestItems, item.Id)
|
delete(requestItems, item.Id)
|
||||||
}
|
}
|
||||||
if len(requestItems) > 0 {
|
if len(requestItems) > 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Found pricing data for items that do not belong to this purchase")
|
return nil, utils.BadRequest("Found pricing data for items that do not belong to this purchase")
|
||||||
}
|
}
|
||||||
|
|
||||||
productSupplierCache := make(map[uint]bool)
|
productSupplierCache := make(map[uint]bool)
|
||||||
@@ -1270,37 +1268,28 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|||||||
|
|
||||||
for _, payload := range newPayloads {
|
for _, payload := range newPayloads {
|
||||||
if payload.ProductID == 0 || payload.WarehouseID == 0 {
|
if payload.ProductID == 0 || payload.WarehouseID == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Product and warehouse must be provided for new items")
|
return nil, utils.BadRequest("Product and warehouse must be provided for new items")
|
||||||
}
|
}
|
||||||
if payload.Qty == nil || *payload.Qty <= 0 {
|
if payload.Qty == nil || *payload.Qty <= 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity must be greater than 0 for product %d", payload.ProductID))
|
return nil, utils.BadRequest(fmt.Sprintf("Quantity must be greater than 0 for product %d", payload.ProductID))
|
||||||
}
|
}
|
||||||
if _, ok := allowedWarehouses[payload.WarehouseID]; !ok {
|
if _, ok := allowedWarehouses[payload.WarehouseID]; !ok {
|
||||||
return nil, fiber.NewError(
|
return nil, utils.BadRequest(fmt.Sprintf("Warehouse %d is not available for this purchase", payload.WarehouseID))
|
||||||
fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Warehouse %d is not available for this purchase", payload.WarehouseID),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
key := fmt.Sprintf("%d:%d", payload.ProductID, payload.WarehouseID)
|
key := fmt.Sprintf("%d:%d", payload.ProductID, payload.WarehouseID)
|
||||||
if _, exists := existingCombos[key]; exists {
|
if _, exists := existingCombos[key]; exists {
|
||||||
return nil, fiber.NewError(
|
return nil, utils.BadRequest(fmt.Sprintf("Product %d in warehouse %d already exists in this purchase", payload.ProductID, payload.WarehouseID))
|
||||||
fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Product %d in warehouse %d already exists in this purchase", payload.ProductID, payload.WarehouseID),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, checked := productSupplierCache[payload.ProductID]; !checked {
|
if _, checked := productSupplierCache[payload.ProductID]; !checked {
|
||||||
linked, err := s.ProductRepo.IsLinkedToSupplier(ctx, uint(payload.ProductID), uint(purchase.SupplierId))
|
linked, err := s.ProductRepo.IsLinkedToSupplier(ctx, uint(payload.ProductID), uint(purchase.SupplierId))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.Log.Errorf("Failed to validate product %d for supplier %d: %+v", payload.ProductID, purchase.SupplierId, err)
|
s.Log.Errorf("Failed to validate product %d for supplier %d: %+v", payload.ProductID, purchase.SupplierId, err)
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to validate product for supplier")
|
return nil, utils.Internal("Failed to validate product for supplier")
|
||||||
}
|
}
|
||||||
if !linked {
|
if !linked {
|
||||||
return nil, fiber.NewError(
|
return nil, utils.BadRequest(fmt.Sprintf("Product %d is not linked to supplier %d", payload.ProductID, purchase.SupplierId))
|
||||||
fiber.StatusBadRequest,
|
|
||||||
fmt.Sprintf("Product %d is not linked to supplier %d", payload.ProductID, purchase.SupplierId),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
productSupplierCache[payload.ProductID] = true
|
productSupplierCache[payload.ProductID] = true
|
||||||
}
|
}
|
||||||
@@ -1328,7 +1317,7 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(updates) == 0 && len(newItems) == 0 {
|
if len(updates) == 0 && len(newItems) == 0 {
|
||||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase has no items to process")
|
return nil, utils.BadRequest("Purchase has no items to process")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &staffAdjustmentPayload{
|
return &staffAdjustmentPayload{
|
||||||
@@ -1340,10 +1329,10 @@ func (s *purchaseService) buildStaffAdjustmentPayload(
|
|||||||
// ? helper
|
// ? helper
|
||||||
func calculateTotalPrice(quantity float64, price float64, provided *float64, ref string) (float64, error) {
|
func calculateTotalPrice(quantity float64, price float64, provided *float64, ref string) (float64, error) {
|
||||||
if quantity <= 0 {
|
if quantity <= 0 {
|
||||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Quantity for %s must be greater than 0", ref))
|
return 0, utils.BadRequest(fmt.Sprintf("Quantity for %s must be greater than 0", ref))
|
||||||
}
|
}
|
||||||
if price <= 0 {
|
if price <= 0 {
|
||||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Price for %s must be greater than 0", ref))
|
return 0, utils.BadRequest(fmt.Sprintf("Price for %s must be greater than 0", ref))
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedTotal := price * quantity
|
expectedTotal := price * quantity
|
||||||
@@ -1352,10 +1341,10 @@ func calculateTotalPrice(quantity float64, price float64, provided *float64, ref
|
|||||||
return expectedTotal, nil
|
return expectedTotal, nil
|
||||||
}
|
}
|
||||||
if *provided <= 0 {
|
if *provided <= 0 {
|
||||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total price for %s must be greater than 0", ref))
|
return 0, utils.BadRequest(fmt.Sprintf("Total price for %s must be greater than 0", ref))
|
||||||
}
|
}
|
||||||
if math.Abs(*provided-expectedTotal) > priceTolerance {
|
if math.Abs(*provided-expectedTotal) > priceTolerance {
|
||||||
return 0, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total price for %s must equal quantity x price", ref))
|
return 0, utils.BadRequest(fmt.Sprintf("Total price for %s must equal quantity x price", ref))
|
||||||
}
|
}
|
||||||
return *provided, nil
|
return *provided, nil
|
||||||
}
|
}
|
||||||
@@ -1384,7 +1373,7 @@ func parseApprovalActionInput(raw string) (entity.ApprovalAction, error) {
|
|||||||
case string(entity.ApprovalActionRejected):
|
case string(entity.ApprovalActionRejected):
|
||||||
return entity.ApprovalActionRejected, nil
|
return entity.ApprovalActionRejected, nil
|
||||||
default:
|
default:
|
||||||
return "", fiber.NewError(fiber.StatusBadRequest, "action must be APPROVED or REJECTED")
|
return "", utils.BadRequest("action must be APPROVED or REJECTED")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1399,13 +1388,58 @@ func (s *purchaseService) rejectAndReload(
|
|||||||
if err := s.createPurchaseApproval(c.Context(), nil, purchaseID, step, entity.ApprovalActionRejected, actorID, notes, false); err != nil {
|
if err := s.createPurchaseApproval(c.Context(), nil, purchaseID, step, entity.ApprovalActionRejected, actorID, notes, false); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
return s.loadPurchase(c.Context(), purchaseID)
|
||||||
updated, err := s.PurchaseRepo.GetByID(c.Context(), purchaseID, s.withRelations)
|
}
|
||||||
if err != nil {
|
func (s *purchaseService) loadPurchase(
|
||||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
|
ctx context.Context,
|
||||||
}
|
id uint,
|
||||||
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
|
) (*entity.Purchase, error) {
|
||||||
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
|
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
|
||||||
}
|
if err != nil {
|
||||||
return updated, nil
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil, utils.NotFound("Purchase not found")
|
||||||
|
}
|
||||||
|
s.Log.Errorf("Failed to get purchase %d: %+v", id, err)
|
||||||
|
return nil, utils.Internal("Failed to get purchase")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := s.attachLatestApproval(ctx, purchase); err != nil {
|
||||||
|
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return purchase, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectPFKIDsFromPurchase(p *entity.Purchase) []uint {
|
||||||
|
seen := make(map[uint]struct{})
|
||||||
|
ids := make([]uint, 0)
|
||||||
|
|
||||||
|
for _, item := range p.Items {
|
||||||
|
if item.ProjectFlockKandangId == nil || *item.ProjectFlockKandangId == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id := uint(*item.ProjectFlockKandangId)
|
||||||
|
if _, ok := seen[id]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
return ids
|
||||||
|
}
|
||||||
|
func (s *purchaseService) ensureProjectFlockNotClosedForPurchase(
|
||||||
|
ctx context.Context,
|
||||||
|
purchase *entity.Purchase,
|
||||||
|
) error {
|
||||||
|
pfkIDs := collectPFKIDsFromPurchase(purchase)
|
||||||
|
if len(pfkIDs) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
db := s.PurchaseRepo.DB()
|
||||||
|
if db == nil {
|
||||||
|
return utils.Internal("DB not available for project flock validation")
|
||||||
|
}
|
||||||
|
|
||||||
|
return commonSvc.EnsureProjectFlockNotClosedForProductWarehouses(ctx, db, pfkIDs)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ type PurchaseItemPayload struct {
|
|||||||
|
|
||||||
type CreatePurchaseRequest struct {
|
type CreatePurchaseRequest struct {
|
||||||
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
|
SupplierID uint `json:"supplier_id" validate:"required,gt=0"`
|
||||||
|
CreditTerm int `json:"credit_term" validate:"required,number,gte=0"`
|
||||||
DueDate *string `json:"due_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
DueDate *string `json:"due_date,omitempty" validate:"omitempty,datetime=2006-01-02"`
|
||||||
Notes *string `json:"notes" validate:"omitempty,max=500"`
|
Notes *string `json:"notes" validate:"omitempty,max=500"`
|
||||||
Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"`
|
Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"`
|
||||||
|
|||||||
@@ -165,17 +165,33 @@ var ProjectFlockApprovalSteps = map[approvalutils.ApprovalStep]string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
// Project Flock Kandang Approval
|
// Chickin Approval
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
const (
|
||||||
|
ApprovalWorkflowChickin approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("CHICKINS")
|
||||||
|
ChickinStepPengajuan approvalutils.ApprovalStep = 1
|
||||||
|
ChickinStepDisetujui approvalutils.ApprovalStep = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
var ChickinApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||||
|
ChickinStepPengajuan: "Pengajuan",
|
||||||
|
ChickinStepDisetujui: "Disetujui",
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------
|
||||||
|
// Project-Flock kandang Approval
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
const (
|
const (
|
||||||
ApprovalWorkflowProjectFlockKandang approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PROJECT_FLOCK_KANDANGS")
|
ApprovalWorkflowProjectFlockKandang approvalutils.ApprovalWorkflowKey = approvalutils.ApprovalWorkflowKey("PROJECT_FLOCK_KANDANGS")
|
||||||
ProjectFlockKandangStepPengajuan approvalutils.ApprovalStep = 1
|
ProjectFlockKandangStepPengajuan approvalutils.ApprovalStep = 1
|
||||||
ProjectFlockKandangStepDisetujui approvalutils.ApprovalStep = 2
|
ProjectFlockKandangStepDisetujui approvalutils.ApprovalStep = 2
|
||||||
|
ProjectFlockKandangStepClosed approvalutils.ApprovalStep = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
var ProjectFlockKandangApprovalSteps = map[approvalutils.ApprovalStep]string{
|
var ProjectFlockKandangApprovalSteps = map[approvalutils.ApprovalStep]string{
|
||||||
ProjectFlockKandangStepPengajuan: "Pengajuan",
|
ProjectFlockKandangStepPengajuan: "Pengajuan",
|
||||||
ProjectFlockKandangStepDisetujui: "Disetujui",
|
ProjectFlockKandangStepDisetujui: "Disetujui",
|
||||||
|
ProjectFlockKandangStepClosed: "Selesai",
|
||||||
}
|
}
|
||||||
|
|
||||||
// -------------------------------------------------------------------
|
// -------------------------------------------------------------------
|
||||||
|
|||||||
@@ -25,3 +25,16 @@ func ErrorHandler(c *fiber.Ctx, err error) error {
|
|||||||
func NotFoundHandler(c *fiber.Ctx) error {
|
func NotFoundHandler(c *fiber.Ctx) error {
|
||||||
return response.Error(c, fiber.StatusNotFound, "Endpoint Not Found", nil)
|
return response.Error(c, fiber.StatusNotFound, "Endpoint Not Found", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func BadRequest(msg string) error {
|
||||||
|
return fiber.NewError(fiber.StatusBadRequest, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NotFound(msg string) error {
|
||||||
|
return fiber.NewError(fiber.StatusNotFound, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Internal(msg string) error {
|
||||||
|
return fiber.NewError(fiber.StatusInternalServerError, msg)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user