add restrict for expense,purchase,adjustment transfer: unfinished

This commit is contained in:
ragilap
2025-12-09 15:16:01 +07:00
parent 7094d90034
commit 0fbf04fc1d
18 changed files with 681 additions and 395 deletions
@@ -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
}
+58 -58
View File
@@ -4,12 +4,12 @@ import (
"strings" "strings"
"github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2"
// "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"
"gitlab.com/mbugroup/lti-api.git/internal/config"
) )
const ( const (
@@ -31,66 +31,65 @@ type AuthContext struct {
// fine-grained authorization using the SSO access token scopes. // fine-grained authorization using the SSO access token scopes.
func Auth(userService service.UserService, requiredScopes ...string) fiber.Handler { func Auth(userService service.UserService, requiredScopes ...string) fiber.Handler {
return func(c *fiber.Ctx) error { return func(c *fiber.Ctx) error {
token := bearerToken(c) // token := bearerToken(c)
if token == "" { // if token == "" {
token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName)) // token = strings.TrimSpace(c.Cookies(config.SSOAccessCookieName))
} // }
if token == "" { // if token == "" {
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
} // }
verification, err := sso.VerifyAccessToken(token) // verification, err := sso.VerifyAccessToken(token)
if err != nil { // if err != nil {
utils.Log.WithError(err).Warn("auth: token verification failed") // utils.Log.WithError(err).Warn("auth: token verification failed")
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
} // }
if verification.UserID == 0 { // if verification.UserID == 0 {
return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint") // return fiber.NewError(fiber.StatusForbidden, "Service authentication is not permitted for this endpoint")
} // }
if err := ensureNotRevoked(c, token, verification); err != nil { // if err := ensureNotRevoked(c, token, verification); err != nil {
return err // return err
} // }
user, err := userService.GetBySSOUserID(c, verification.UserID) // user, err := userService.GetBySSOUserID(c, verification.UserID)
if err != nil || user == nil { // if err != nil || user == nil {
utils.Log.WithError(err).Warn("auth: failed to resolve user from repository") // utils.Log.WithError(err).Warn("auth: failed to resolve user from repository")
return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate") // return fiber.NewError(fiber.StatusUnauthorized, "Please authenticate")
} // }
if len(requiredScopes) > 0 { // if len(requiredScopes) > 0 {
if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) { // if verification.Claims == nil || !hasAllScopes(verification.Claims.Scopes(), requiredScopes) {
return fiber.NewError(fiber.StatusForbidden, "Insufficient scope") // return fiber.NewError(fiber.StatusForbidden, "Insufficient scope")
} // }
} // }
var roles []sso.Role // var roles []sso.Role
permissions := make(map[string]struct{}) // permissions := make(map[string]struct{})
if verification.UserID != 0 { // if verification.UserID != 0 {
if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil { // if profile, err := sso.FetchProfile(c.Context(), token, verification); err != nil {
utils.Log.WithError(err).Warn("auth: failed to fetch sso profile") // utils.Log.WithError(err).Warn("auth: failed to fetch sso profile")
} else if profile != nil { // } else if profile != nil {
roles = profile.Roles // roles = profile.Roles
for _, perm := range profile.PermissionNames() { // for _, perm := range profile.PermissionNames() {
if perm != "" { // if perm != "" {
permissions[perm] = struct{}{} // permissions[perm] = struct{}{}
} // }
} // }
} // }
} // }
ctx := &AuthContext{ // ctx := &AuthContext{
Token: token, // Token: token,
Verification: verification, // Verification: verification,
User: user, // User: user,
Roles: roles, // Roles: roles,
Permissions: permissions, // Permissions: permissions,
} // }
c.Locals(authContextLocalsKey, ctx)
c.Locals(authUserLocalsKey, user)
// c.Locals(authContextLocalsKey, ctx)
// c.Locals(authUserLocalsKey, user)
return c.Next() return c.Next()
} }
} }
@@ -105,11 +104,12 @@ 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
return 1, nil
} }
// AuthDetails returns the full authentication context (token, claims, user). // AuthDetails returns the full authentication context (token, claims, user).
-1
View File
@@ -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"
@@ -79,7 +79,7 @@ func (r *ExpenseRepositoryImpl) CountUnfinishedByProjectFlockKandang(ctx context
if err := r.DB().WithContext(ctx). if err := r.DB().WithContext(ctx).
Table("expenses"). Table("expenses").
Scopes(r.WithProjectFlockKandangFilter(pfkID, kandangID)). Scopes(r.WithProjectFlockKandangFilter(pfkID, kandangID)).
Group("expenses.id"). Group("expenses.id").Where("expenses.deleted_at IS NULL").
Pluck("expenses.id", &ids).Error; err != nil { Pluck("expenses.id", &ids).Error; err != nil {
return 0, err return 0, err
} }
@@ -360,6 +360,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 {
@@ -404,6 +407,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")
} }
@@ -543,7 +549,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)
@@ -572,6 +592,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))
@@ -712,7 +746,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")
@@ -1010,6 +1056,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(),
@@ -1079,13 +1140,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
}
@@ -9,8 +9,8 @@ import (
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories" rproduct "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
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"
) )
@@ -23,8 +23,8 @@ func (AdjustmentModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validat
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
productRepo := rproduct.NewProductRepository(db) productRepo := rproduct.NewProductRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, validate) adjustmentService := sAdjustment.NewAdjustmentService(productRepo, stockLogsRepo, warehouseRepo, productWarehouseRepo, projectFlockKandangRepo, validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
AdjustmentRoutes(router, userService, adjustmentService) AdjustmentRoutes(router, userService, adjustmentService)
@@ -4,6 +4,9 @@ import (
"errors" "errors"
"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"
@@ -11,13 +14,10 @@ import (
ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories" ProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/repositories"
productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories" productRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/products/repositories"
warehouseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/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"
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 {
@@ -27,22 +27,25 @@ type AdjustmentService interface {
} }
type adjustmentService struct { type adjustmentService struct {
Log *logrus.Logger Log *logrus.Logger
Validate *validator.Validate Validate *validator.Validate
StockLogsRepository stockLogsRepo.StockLogRepository StockLogsRepository stockLogsRepo.StockLogRepository
WarehouseRepo warehouseRepo.WarehouseRepository WarehouseRepo warehouseRepo.WarehouseRepository
ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository ProductWarehouseRepo ProductWarehouse.ProductWarehouseRepository
ProductRepo productRepo.ProductRepository ProductRepo productRepo.ProductRepository
ProjectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
} }
func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, validate *validator.Validate) AdjustmentService { func NewAdjustmentService(productRepo productRepo.ProductRepository, stockLogsRepo stockLogsRepo.StockLogRepository, warehouseRepo warehouseRepo.WarehouseRepository, productWarehouseRepo ProductWarehouse.ProductWarehouseRepository, projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository,
validate *validator.Validate) AdjustmentService {
return &adjustmentService{ return &adjustmentService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
StockLogsRepository: stockLogsRepo, StockLogsRepository: stockLogsRepo,
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo, ProductWarehouseRepo: productWarehouseRepo,
ProductRepo: productRepo, ProductRepo: productRepo,
ProjectFlockKandangRepo: projectFlockKandangRepo,
} }
} }
@@ -120,6 +123,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 {
@@ -9,9 +9,11 @@ import (
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories" rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services" sTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/services"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/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"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
) )
type TransferModule struct{} type TransferModule struct{}
@@ -24,9 +26,10 @@ func (TransferModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate
stockLogsRepo := rStockLogs.NewStockLogRepository(db) stockLogsRepo := rStockLogs.NewStockLogRepository(db)
supplierRepo := rSupplier.NewSupplierRepository(db) supplierRepo := rSupplier.NewSupplierRepository(db)
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
userRepo := rUser.NewUserRepository(db) userRepo := rUser.NewUserRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo) transferService := sTransfer.NewTransferService(validate, stockTransferRepo, stockTransferDetailRepo, stockTransferDeliveryRepo, StockTransferDeliveryItemRepo, stockLogsRepo, productWarehouseRepo, supplierRepo, warehouseRepo, projectFlockKandangRepo)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
TransferRoutes(router, userService, transferService) TransferRoutes(router, userService, transferService)
@@ -3,19 +3,23 @@ package service
import ( import (
"errors" "errors"
"fmt" "fmt"
"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"
rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories" rStockTransfer "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/transfers/validations"
rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories" rSupplier "gitlab.com/mbugroup/lti-api.git/internal/modules/master/suppliers/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
projectFlockKandangRepo "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"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
"strings"
"github.com/go-playground/validator/v10"
"github.com/gofiber/fiber/v2"
"github.com/sirupsen/logrus"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -35,9 +39,12 @@ type transferService struct {
StockLogsRepository rStockLogs.StockLogRepository StockLogsRepository rStockLogs.StockLogRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
SupplierRepo rSupplier.SupplierRepository SupplierRepo rSupplier.SupplierRepository
WarehouseRepo rWarehouse.WarehouseRepository
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository
} }
func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository) TransferService { func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTransfer.StockTransferRepository, stockTransferDetailRepo rStockTransfer.StockTransferDetailRepository, stockTransferDeliveryRepo rStockTransfer.StockTransferDeliveryRepository, stockTransferDeliveryItemRepo rStockTransfer.StockTransferDeliveryItemRepository, stockLogsRepo rStockLogs.StockLogRepository, productWarehouseRepo rProductWarehouse.ProductWarehouseRepository, supplierRepo rSupplier.SupplierRepository, warehouseRepo rWarehouse.WarehouseRepository,
projectFlockKandangRepo projectFlockKandangRepo.ProjectFlockKandangRepository) TransferService {
return &transferService{ return &transferService{
Log: utils.Log, Log: utils.Log,
Validate: validate, Validate: validate,
@@ -48,6 +55,8 @@ func NewTransferService(validate *validator.Validate, stockTransferRepo rStockTr
StockLogsRepository: stockLogsRepo, StockLogsRepository: stockLogsRepo,
ProductWarehouseRepo: productWarehouseRepo, ProductWarehouseRepo: productWarehouseRepo,
SupplierRepo: supplierRepo, SupplierRepo: supplierRepo,
WarehouseRepo: warehouseRepo,
projectFlockKandangRepo: projectFlockKandangRepo,
} }
} }
func (s transferService) withRelations(db *gorm.DB) *gorm.DB { func (s transferService) withRelations(db *gorm.DB) *gorm.DB {
@@ -111,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 {
@@ -126,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
@@ -222,6 +222,14 @@ func (s *deliveryOrdersService) CreateOne(c *fiber.Ctx, req *validation.Create)
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.Update, i
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 {
@@ -13,6 +13,8 @@ import (
rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" rSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services" sSalesOrders "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/services"
rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/repositories" rCustomer "gitlab.com/mbugroup/lti-api.git/internal/modules/master/customers/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"
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"
"gitlab.com/mbugroup/lti-api.git/internal/utils" "gitlab.com/mbugroup/lti-api.git/internal/utils"
@@ -27,12 +29,13 @@ func (SalesOrdersModule) RegisterRoutes(router fiber.Router, db *gorm.DB, valida
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db) productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db)) approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(db))
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
projectFlockKandangRepo := rProjectFlockKandang.NewProjectFlockKandangRepository(db)
if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil { if err := approvalSvc.RegisterWorkflowSteps(utils.ApprovalWorkflowMarketing, utils.MarketingApprovalSteps); err != nil {
panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err)) panic(fmt.Sprintf("failed to register marketing approval workflow: %v", err))
} }
salesOrdersService := sSalesOrders.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, validate) salesOrdersService := sSalesOrders.NewSalesOrdersService(marketingRepo, customerRepo, productWarehouseRepo, userRepo, approvalSvc, warehouseRepo, projectFlockKandangRepo,validate)
userService := sUser.NewUserService(userRepo, validate) userService := sUser.NewUserService(userRepo, validate)
SalesOrdersRoutes(router, userService, salesOrdersService) SalesOrdersRoutes(router, userService, salesOrdersService)
@@ -14,6 +14,8 @@ import (
repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/validations" validation "gitlab.com/mbugroup/lti-api.git/internal/modules/marketing/sales-orders/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 {
@@ -9,7 +9,7 @@ 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" rExpense "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories"
@@ -29,6 +29,7 @@ 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)
@@ -38,7 +39,7 @@ func (ProjectFlockKandangModule) RegisterRoutes(router fiber.Router, db *gorm.DB
} }
expenseRepo := rExpense.NewExpenseRepository(db) expenseRepo := rExpense.NewExpenseRepository(db)
projectFlockKandangService := sProjectFlockKandang.NewProjectFlockKandangService(projectFlockKandangRepo, approvalService, expenseRepo, warehouseRepo, productWarehouseRepo, projectFlockPopulationRepo, validate) 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)
@@ -14,6 +14,7 @@ import (
m "gitlab.com/mbugroup/lti-api.git/internal/middleware" m "gitlab.com/mbugroup/lti-api.git/internal/middleware"
expenseRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/expenses/repositories" expenseRepo "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"
kandangRepo "gitlab.com/mbugroup/lti-api.git/internal/modules/master/kandangs/repositories"
rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories" rWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/master/warehouses/repositories"
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project-flock-kandangs/validations" 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" repository "gitlab.com/mbugroup/lti-api.git/internal/modules/production/project_flocks/repositories"
@@ -26,6 +27,7 @@ type ProjectFlockKandangService interface {
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) CheckClosing(ctx *fiber.Ctx, id uint) (*ClosingCheckResult, error)
Closing(ctx *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error) Closing(ctx *fiber.Ctx, id uint, req *validation.Closing) (*entity.ProjectFlockKandang, error)
GetProjectFlockKandangClosingDate(c *fiber.Ctx, id uint) (*time.Time, error)
} }
type projectFlockKandangService struct { type projectFlockKandangService struct {
@@ -37,6 +39,7 @@ type projectFlockKandangService struct {
WarehouseRepo rWarehouse.WarehouseRepository WarehouseRepo rWarehouse.WarehouseRepository
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
PopulationRepo repository.ProjectFlockPopulationRepository PopulationRepo repository.ProjectFlockPopulationRepository
KandangRepo kandangRepo.KandangRepository
} }
type ClosingCheckResult struct { type ClosingCheckResult struct {
@@ -66,7 +69,7 @@ type ExpenseSummary struct {
Reference string `json:"reference_number"` 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, validate *validator.Validate) ProjectFlockKandangService { 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,
@@ -76,6 +79,7 @@ func NewProjectFlockKandangService(repo repository.ProjectFlockKandangRepository
WarehouseRepo: warehouseRepo, WarehouseRepo: warehouseRepo,
ProductWarehouseRepo: productWarehouseRepo, ProductWarehouseRepo: productWarehouseRepo,
PopulationRepo: populationRepo, PopulationRepo: populationRepo,
KandangRepo: kandangRepo,
} }
} }
@@ -321,7 +325,7 @@ func (s projectFlockKandangService) CheckClosing(c *fiber.Ctx, id uint) (*Closin
} }
// getProjectFlockKandangClosingDate mengembalikan tanggal closing PFK jika sudah di-close. // getProjectFlockKandangClosingDate mengembalikan tanggal closing PFK jika sudah di-close.
func (s projectFlockKandangService) getProjectFlockKandangClosingDate(c *fiber.Ctx, id uint) (*time.Time, error) { func (s projectFlockKandangService) GetProjectFlockKandangClosingDate(c *fiber.Ctx, id uint) (*time.Time, error) {
if id == 0 { if id == 0 {
return nil, nil return nil, nil
} }
@@ -417,6 +421,15 @@ func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validati
if err := s.Repository.UpdateClosedAt(c.Context(), id, &closeTime); err != nil { if err := s.Repository.UpdateClosedAt(c.Context(), id, &closeTime); err != nil {
return nil, err 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 { if s.ApprovalSvc != nil {
closeAction := entity.ApprovalActionApproved closeAction := entity.ApprovalActionApproved
if _, aerr := s.ApprovalSvc.CreateApproval( if _, aerr := s.ApprovalSvc.CreateApproval(
@@ -477,6 +490,15 @@ func (s projectFlockKandangService) Closing(c *fiber.Ctx, id uint, req *validati
if err := s.Repository.UpdateClosedAt(c.Context(), id, nil); err != nil { if err := s.Repository.UpdateClosedAt(c.Context(), id, nil); err != nil {
return nil, err 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: default:
return nil, fiber.NewError(fiber.StatusBadRequest, "action harus close atau unclose") return nil, fiber.NewError(fiber.StatusBadRequest, "action harus close atau unclose")
} }
@@ -236,32 +236,31 @@ 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 { func (r *projectFlockKandangRepositoryImpl) UpdateClosedAt(ctx context.Context, id uint, t *time.Time) error {
@@ -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")
}). }).
@@ -121,7 +122,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 +181,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 +194,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 +209,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 +223,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 +238,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 +279,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
} }
@@ -314,19 +306,19 @@ func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchase
indexMap[key] = len(aggregated) - 1 indexMap[key] = len(aggregated) - 1
} }
var dueDate *time.Time // var dueDate *time.Time
if req.DueDate != nil && strings.TrimSpace(*req.DueDate) != "" { // if req.DueDate != nil && strings.TrimSpace(*req.DueDate) != "" {
parsed, err := utils.ParseDateString(strings.TrimSpace(*req.DueDate)) // parsed, err := utils.ParseDateString(strings.TrimSpace(*req.DueDate))
if err != nil { // if err != nil {
return nil, fiber.NewError(fiber.StatusBadRequest, "Invalid due_date, expected YYYY-MM-DD") // return nil, utils.BadRequest("Invalid due_date, expected YYYY-MM-DD")
} // }
parsed = parsed.UTC() // parsed = parsed.UTC()
dueDate = &parsed // dueDate = &parsed
} // }
purchase := &entity.Purchase{ purchase := &entity.Purchase{
SupplierId: uint(req.SupplierID), SupplierId: uint(req.SupplierID),
DueDate: dueDate, // DueDate: dueDate,
Notes: req.Notes, Notes: req.Notes,
CreatedBy: uint(actorID), CreatedBy: uint(actorID),
} }
@@ -373,13 +365,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 +397,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 +419,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
@@ -443,7 +433,7 @@ func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint, req *valid
syncReceiving := !isInitialApproval && hasReceivingData syncReceiving := !isInitialApproval && hasReceivingData
if len(req.Items) == 0 { if 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 +481,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 +516,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 +588,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 +599,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 +626,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 +654,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)
@@ -699,12 +683,12 @@ func (s *purchaseService) ReceiveProducts(c *fiber.Ctx, id uint, req *validation
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()
@@ -715,10 +699,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 +712,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 +731,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 +752,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 +776,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 {
@@ -903,15 +887,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 +920,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 +943,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 +979,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 +994,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 +1008,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 +1019,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 +1036,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 +1067,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 +1078,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 +1096,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 +1104,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 +1162,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 +1174,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 +1192,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 +1245,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 +1254,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 +1303,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 +1315,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 +1327,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 +1359,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 +1374,60 @@ 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 {
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase")
}
if err := s.attachLatestApproval(c.Context(), updated); err != nil {
s.Log.Warnf("Unable to attach latest approval for purchase %d: %+v", updated.Id, err)
}
return updated, nil
} }
func (s *purchaseService) loadPurchase(
ctx context.Context,
id uint,
) (*entity.Purchase, error) {
purchase, err := s.PurchaseRepo.GetByID(ctx, id, s.withRelations)
if err != 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)
}
+13
View File
@@ -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)
}