mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-20 13:31:56 +00:00
unfinished purchase
This commit is contained in:
@@ -0,0 +1,337 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
rProductWarehouse "gitlab.com/mbugroup/lti-api.git/internal/modules/inventory/product-warehouses/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"
|
||||
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"github.com/google/uuid"
|
||||
"github.com/sirupsen/logrus"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PurchaseService interface {
|
||||
CreateOne(ctx *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error)
|
||||
ApproveStaffPurchase(ctx *fiber.Ctx, id uint64, req *validation.ApproveStaffPurchaseRequest) (*entity.Purchase, error)
|
||||
}
|
||||
|
||||
type purchaseService struct {
|
||||
Log *logrus.Logger
|
||||
Validate *validator.Validate
|
||||
PurchaseRepo rPurchase.PurchaseRepository
|
||||
ProductWarehouseRepo rProductWarehouse.ProductWarehouseRepository
|
||||
WarehouseRepo rWarehouse.WarehouseRepository
|
||||
SupplierRepo rSupplier.SupplierRepository
|
||||
ApprovalRepo commonRepo.ApprovalRepository
|
||||
}
|
||||
|
||||
func NewPurchaseService(
|
||||
validate *validator.Validate,
|
||||
purchaseRepo rPurchase.PurchaseRepository,
|
||||
productWarehouseRepo rProductWarehouse.ProductWarehouseRepository,
|
||||
warehouseRepo rWarehouse.WarehouseRepository,
|
||||
supplierRepo rSupplier.SupplierRepository,
|
||||
approvalRepo commonRepo.ApprovalRepository,
|
||||
) PurchaseService {
|
||||
return &purchaseService{
|
||||
Log: utils.Log,
|
||||
Validate: validate,
|
||||
PurchaseRepo: purchaseRepo,
|
||||
ProductWarehouseRepo: productWarehouseRepo,
|
||||
WarehouseRepo: warehouseRepo,
|
||||
SupplierRepo: supplierRepo,
|
||||
ApprovalRepo: approvalRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func uint64Ptr(v uint64) *uint64 {
|
||||
return &v
|
||||
}
|
||||
|
||||
func (s *purchaseService) CreateOne(c *fiber.Ctx, req *validation.CreatePurchaseRequest) (*entity.Purchase, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
supplier, err := s.SupplierRepo.GetByID(c.Context(), req.SupplierID, nil)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Supplier not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to get supplier: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get supplier")
|
||||
}
|
||||
|
||||
warehouse, err := s.WarehouseRepo.GetDetailByID(c.Context(), req.WarehouseID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Warehouse not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to get warehouse: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get warehouse")
|
||||
}
|
||||
|
||||
if warehouse.AreaId != req.AreaID {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse does not belong to the provided area")
|
||||
}
|
||||
if warehouse.LocationId == nil || *warehouse.LocationId != req.LocationID {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Warehouse does not belong to the provided location")
|
||||
}
|
||||
|
||||
type aggregatedItem struct {
|
||||
productId uint64
|
||||
warehouseId uint64
|
||||
productWarehouseId *uint64
|
||||
subQty float64
|
||||
}
|
||||
|
||||
if len(req.Items) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Items must not be empty")
|
||||
}
|
||||
|
||||
aggregated := make([]*aggregatedItem, 0, len(req.Items))
|
||||
indexMap := make(map[string]int)
|
||||
|
||||
for _, item := range req.Items {
|
||||
var (
|
||||
productId = uint64(item.ProductID)
|
||||
warehouseId = uint64(req.WarehouseID)
|
||||
productWarehouseId *uint64
|
||||
)
|
||||
|
||||
if item.ProductWarehouseID != nil {
|
||||
productWarehouse, err := s.ProductWarehouseRepo.GetDetailByID(c.Context(), *item.ProductWarehouseID)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, fmt.Sprintf("Product warehouse %d not found", *item.ProductWarehouseID))
|
||||
}
|
||||
s.Log.Errorf("Failed to get product warehouse %d: %+v", *item.ProductWarehouseID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||
}
|
||||
|
||||
if productWarehouse.WarehouseId != req.WarehouseID {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Product warehouse does not match selected warehouse")
|
||||
}
|
||||
|
||||
productId = uint64(productWarehouse.ProductId)
|
||||
warehouseId = uint64(productWarehouse.WarehouseId)
|
||||
idCopy := uint64(productWarehouse.Id)
|
||||
productWarehouseId = &idCopy
|
||||
} else {
|
||||
productWarehouse, err := s.ProductWarehouseRepo.GetProductWarehouseByProductAndWarehouseID(c.Context(), item.ProductID, req.WarehouseID)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
s.Log.Errorf("Failed to get product warehouse for product %d and warehouse %d: %+v", item.ProductID, req.WarehouseID, err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get product warehouse")
|
||||
}
|
||||
if err == nil {
|
||||
idCopy := uint64(productWarehouse.Id)
|
||||
productWarehouseId = &idCopy
|
||||
}
|
||||
}
|
||||
|
||||
key := fmt.Sprintf("%d:%d", productId, warehouseId)
|
||||
if idx, ok := indexMap[key]; ok {
|
||||
aggregated[idx].subQty += item.Quantity
|
||||
continue
|
||||
}
|
||||
|
||||
entry := &aggregatedItem{
|
||||
productId: productId,
|
||||
warehouseId: warehouseId,
|
||||
productWarehouseId: productWarehouseId,
|
||||
subQty: item.Quantity,
|
||||
}
|
||||
aggregated = append(aggregated, entry)
|
||||
indexMap[key] = len(aggregated) - 1
|
||||
}
|
||||
|
||||
prNumber := fmt.Sprintf("PR-%s-%s", time.Now().Format("20060102"), uuid.NewString()[:8])
|
||||
|
||||
var creditTerm *int
|
||||
var dueDate *time.Time
|
||||
|
||||
if supplier.DueDate > 0 {
|
||||
ct := supplier.DueDate
|
||||
creditTerm = &ct
|
||||
d := time.Now().UTC().AddDate(0, 0, ct)
|
||||
dueDate = &d
|
||||
}
|
||||
|
||||
purchase := &entity.Purchase{
|
||||
PrNumber: prNumber,
|
||||
SupplierId: uint64(req.SupplierID),
|
||||
CreditTerm: creditTerm,
|
||||
DueDate: dueDate,
|
||||
GrandTotal: 0,
|
||||
Notes: req.Notes,
|
||||
CreatedBy: 1, // TODO: replace with authenticated user id once available
|
||||
}
|
||||
|
||||
items := make([]*entity.PurchaseItem, 0, len(aggregated))
|
||||
for _, item := range aggregated {
|
||||
items = append(items, &entity.PurchaseItem{
|
||||
ProductId: item.productId,
|
||||
WarehouseId: item.warehouseId,
|
||||
ProductWarehouseId: item.productWarehouseId,
|
||||
SubQty: item.subQty,
|
||||
TotalQty: item.subQty,
|
||||
TotalUsed: 0,
|
||||
Price: 0,
|
||||
TotalPrice: 0,
|
||||
})
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
purchaseRepoTx := rPurchase.NewPurchaseRepository(tx)
|
||||
if err := purchaseRepoTx.CreateWithItems(ctx, purchase, items); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
actorID := uint(purchase.CreatedBy)
|
||||
if actorID == 0 {
|
||||
actorID = 1
|
||||
}
|
||||
action := entity.ApprovalActionCreated
|
||||
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||
if _, err := approvalSvc.CreateApproval(
|
||||
ctx,
|
||||
utils.ApprovalWorkflowPurchase,
|
||||
uint(purchase.Id),
|
||||
utils.PurchaseStepPengajuan,
|
||||
&action,
|
||||
actorID,
|
||||
nil,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if transactionErr != nil {
|
||||
s.Log.Errorf("Failed to create purchase requisition: %+v", transactionErr)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to create purchase requisition")
|
||||
}
|
||||
|
||||
created, err := s.PurchaseRepo.GetByIDWithRelations(ctx, purchase.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to load created purchase requisition: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase requisition")
|
||||
}
|
||||
|
||||
return created, nil
|
||||
}
|
||||
|
||||
func (s *purchaseService) ApproveStaffPurchase(c *fiber.Ctx, id uint64, req *validation.ApproveStaffPurchaseRequest) (*entity.Purchase, error) {
|
||||
if err := s.Validate.Struct(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ctx := c.Context()
|
||||
|
||||
purchase, err := s.PurchaseRepo.GetByIDWithRelations(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase requisition not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to get purchase requisition: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to get purchase requisition")
|
||||
}
|
||||
|
||||
if len(purchase.Items) == 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Purchase requisition has no items to approve")
|
||||
}
|
||||
|
||||
requestItems := make(map[uint64]validation.StaffPurchaseApprovalItem, len(req.Items))
|
||||
for _, item := range req.Items {
|
||||
requestItems[item.PurchaseItemID] = item
|
||||
}
|
||||
|
||||
updates := make([]rPurchase.PurchasePricingUpdate, 0, len(purchase.Items))
|
||||
var grandTotal float64
|
||||
|
||||
for _, item := range purchase.Items {
|
||||
data, ok := requestItems[item.Id]
|
||||
if !ok {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Missing pricing data for item %d", item.Id))
|
||||
}
|
||||
delete(requestItems, item.Id)
|
||||
|
||||
if data.Price <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Price for item %d must be greater than 0", item.Id))
|
||||
}
|
||||
|
||||
totalPrice := data.TotalPrice
|
||||
if totalPrice == nil {
|
||||
calculated := data.Price * item.TotalQty
|
||||
totalPrice = &calculated
|
||||
}
|
||||
if *totalPrice <= 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, fmt.Sprintf("Total price for item %d must be greater than 0", item.Id))
|
||||
}
|
||||
|
||||
updates = append(updates, rPurchase.PurchasePricingUpdate{
|
||||
ItemID: item.Id,
|
||||
Price: data.Price,
|
||||
TotalPrice: *totalPrice,
|
||||
})
|
||||
grandTotal += *totalPrice
|
||||
}
|
||||
|
||||
if len(requestItems) > 0 {
|
||||
return nil, fiber.NewError(fiber.StatusBadRequest, "Found pricing data for items that do not belong to this purchase requisition")
|
||||
}
|
||||
|
||||
action := entity.ApprovalActionApproved
|
||||
actorID := uint(1) // TODO: replace with authenticated user id once available
|
||||
|
||||
transactionErr := s.PurchaseRepo.DB().WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
purchaseRepoTx := rPurchase.NewPurchaseRepository(tx)
|
||||
if err := purchaseRepoTx.UpdatePricing(ctx, purchase.Id, updates, grandTotal); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
approvalSvc := commonSvc.NewApprovalService(commonRepo.NewApprovalRepository(tx))
|
||||
if _, err := approvalSvc.CreateApproval(
|
||||
ctx,
|
||||
utils.ApprovalWorkflowPurchase,
|
||||
uint(purchase.Id),
|
||||
utils.PurchaseStepStaffPurchase,
|
||||
&action,
|
||||
actorID,
|
||||
req.Notes,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
if transactionErr != nil {
|
||||
if errors.Is(transactionErr, gorm.ErrRecordNotFound) {
|
||||
return nil, fiber.NewError(fiber.StatusNotFound, "Purchase item not found")
|
||||
}
|
||||
s.Log.Errorf("Failed to approve purchase requisition %d: %+v", purchase.Id, transactionErr)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to approve purchase requisition")
|
||||
}
|
||||
|
||||
updated, err := s.PurchaseRepo.GetByIDWithRelations(ctx, purchase.Id)
|
||||
if err != nil {
|
||||
s.Log.Errorf("Failed to load purchase requisition after approval: %+v", err)
|
||||
return nil, fiber.NewError(fiber.StatusInternalServerError, "Failed to load purchase requisition")
|
||||
}
|
||||
|
||||
return updated, nil
|
||||
}
|
||||
Reference in New Issue
Block a user