mirror of
https://gitlab.com/mbugroup/lti-api.git
synced 2026-05-23 23:05:44 +00:00
unfinished purchase
This commit is contained in:
@@ -0,0 +1,67 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/dto"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
||||
validation "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/validations"
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/response"
|
||||
|
||||
"github.com/gofiber/fiber/v2"
|
||||
)
|
||||
|
||||
type PurchaseController struct {
|
||||
service service.PurchaseService
|
||||
}
|
||||
|
||||
func NewPurchaseController(s service.PurchaseService) *PurchaseController {
|
||||
return &PurchaseController{service: s}
|
||||
}
|
||||
|
||||
func (ctrl *PurchaseController) CreateOne(c *fiber.Ctx) error {
|
||||
req := new(validation.CreatePurchaseRequest)
|
||||
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := ctrl.service.CreateOne(c, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusCreated).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusCreated,
|
||||
Status: "success",
|
||||
Message: "Purchase requisition created successfully",
|
||||
Data: dto.ToPurchaseDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
|
||||
func (ctrl *PurchaseController) ApproveStaffPurchase(c *fiber.Ctx) error {
|
||||
param := c.Params("id")
|
||||
id, err := strconv.ParseUint(param, 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid purchase requisition id")
|
||||
}
|
||||
|
||||
req := new(validation.ApproveStaffPurchaseRequest)
|
||||
if err := c.BodyParser(req); err != nil {
|
||||
return fiber.NewError(fiber.StatusBadRequest, "Invalid request body")
|
||||
}
|
||||
|
||||
result, err := ctrl.service.ApproveStaffPurchase(c, id, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return c.Status(fiber.StatusOK).
|
||||
JSON(response.Success{
|
||||
Code: fiber.StatusOK,
|
||||
Status: "success",
|
||||
Message: "Staff purchase approval recorded successfully",
|
||||
Data: dto.ToPurchaseDetailDTO(*result),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
)
|
||||
|
||||
type SupplierBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Alias string `json:"alias"`
|
||||
Type string `json:"type"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
type AreaBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type LocationBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type WarehouseBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Area *AreaBaseDTO `json:"area,omitempty"`
|
||||
Location *LocationBaseDTO `json:"location,omitempty"`
|
||||
}
|
||||
|
||||
type ProductBaseDTO struct {
|
||||
Id uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
SKU *string `json:"sku,omitempty"`
|
||||
}
|
||||
|
||||
type PurchaseItemDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
Product *ProductBaseDTO `json:"product,omitempty"`
|
||||
Warehouse *WarehouseBaseDTO `json:"warehouse,omitempty"`
|
||||
ProductWarehouseID *uint64 `json:"product_warehouse_id,omitempty"`
|
||||
SubQty float64 `json:"sub_qty"`
|
||||
TotalQty float64 `json:"total_qty"`
|
||||
TotalUsed float64 `json:"total_used"`
|
||||
Price float64 `json:"price"`
|
||||
TotalPrice float64 `json:"total_price"`
|
||||
}
|
||||
|
||||
type PurchaseDetailDTO struct {
|
||||
Id uint64 `json:"id"`
|
||||
PrNumber string `json:"pr_number"`
|
||||
Supplier *SupplierBaseDTO `json:"supplier,omitempty"`
|
||||
CreditTerm *int `json:"credit_term,omitempty"`
|
||||
DueDate *time.Time `json:"due_date,omitempty"`
|
||||
GrandTotal float64 `json:"grand_total"`
|
||||
Notes *string `json:"notes,omitempty"`
|
||||
Items []PurchaseItemDTO `json:"items"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func toSupplierBaseDTO(s entity.Supplier) *SupplierBaseDTO {
|
||||
if s.Id == 0 {
|
||||
return nil
|
||||
}
|
||||
return &SupplierBaseDTO{
|
||||
Id: s.Id,
|
||||
Name: s.Name,
|
||||
Alias: s.Alias,
|
||||
Type: s.Type,
|
||||
Category: s.Category,
|
||||
}
|
||||
}
|
||||
|
||||
func toWarehouseBaseDTO(w *entity.Warehouse) *WarehouseBaseDTO {
|
||||
if w == nil || w.Id == 0 {
|
||||
return nil
|
||||
}
|
||||
dto := &WarehouseBaseDTO{
|
||||
Id: w.Id,
|
||||
Name: w.Name,
|
||||
}
|
||||
if w.Area.Id != 0 {
|
||||
dto.Area = &AreaBaseDTO{
|
||||
Id: w.Area.Id,
|
||||
Name: w.Area.Name,
|
||||
}
|
||||
}
|
||||
if w.Location != nil && w.Location.Id != 0 {
|
||||
dto.Location = &LocationBaseDTO{
|
||||
Id: w.Location.Id,
|
||||
Name: w.Location.Name,
|
||||
}
|
||||
}
|
||||
return dto
|
||||
}
|
||||
|
||||
func toProductBaseDTO(p *entity.Product) *ProductBaseDTO {
|
||||
if p == nil || p.Id == 0 {
|
||||
return nil
|
||||
}
|
||||
dto := &ProductBaseDTO{
|
||||
Id: p.Id,
|
||||
Name: p.Name,
|
||||
}
|
||||
if p.Sku != nil {
|
||||
dto.SKU = p.Sku
|
||||
}
|
||||
return dto
|
||||
}
|
||||
|
||||
func ToPurchaseItemDTO(item entity.PurchaseItem) PurchaseItemDTO {
|
||||
dto := PurchaseItemDTO{
|
||||
Id: item.Id,
|
||||
ProductWarehouseID: item.ProductWarehouseId,
|
||||
SubQty: item.SubQty,
|
||||
TotalQty: item.TotalQty,
|
||||
TotalUsed: item.TotalUsed,
|
||||
Price: item.Price,
|
||||
TotalPrice: item.TotalPrice,
|
||||
}
|
||||
if item.Product != nil {
|
||||
dto.Product = toProductBaseDTO(item.Product)
|
||||
}
|
||||
if item.Warehouse != nil {
|
||||
dto.Warehouse = toWarehouseBaseDTO(item.Warehouse)
|
||||
}
|
||||
return dto
|
||||
}
|
||||
|
||||
func ToPurchaseItemDTOs(items []entity.PurchaseItem) []PurchaseItemDTO {
|
||||
result := make([]PurchaseItemDTO, len(items))
|
||||
for i, item := range items {
|
||||
result[i] = ToPurchaseItemDTO(item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ToPurchaseDetailDTO(p entity.Purchase) PurchaseDetailDTO {
|
||||
return PurchaseDetailDTO{
|
||||
Id: p.Id,
|
||||
PrNumber: p.PrNumber,
|
||||
Supplier: toSupplierBaseDTO(p.Supplier),
|
||||
CreditTerm: p.CreditTerm,
|
||||
DueDate: p.DueDate,
|
||||
GrandTotal: p.GrandTotal,
|
||||
Notes: p.Notes,
|
||||
Items: ToPurchaseItemDTOs(p.Items),
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package purchases
|
||||
|
||||
import (
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PurchaseModule struct{}
|
||||
|
||||
func (PurchaseModule) RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
RegisterRoutes(router, db, validate)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
package repositories
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
entity "gitlab.com/mbugroup/lti-api.git/internal/entities"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type PurchaseRepository interface {
|
||||
repository.BaseRepository[entity.Purchase]
|
||||
CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error
|
||||
GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error)
|
||||
UpdatePricing(ctx context.Context, purchaseID uint64, updates []PurchasePricingUpdate, grandTotal float64) error
|
||||
}
|
||||
|
||||
type PurchaseRepositoryImpl struct {
|
||||
*repository.BaseRepositoryImpl[entity.Purchase]
|
||||
}
|
||||
|
||||
func NewPurchaseRepository(db *gorm.DB) PurchaseRepository {
|
||||
return &PurchaseRepositoryImpl{
|
||||
BaseRepositoryImpl: repository.NewBaseRepository[entity.Purchase](db),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) CreateWithItems(ctx context.Context, purchase *entity.Purchase, items []*entity.PurchaseItem) error {
|
||||
db := r.DB().WithContext(ctx)
|
||||
|
||||
if err := db.Create(purchase).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(items) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
item.PurchaseId = purchase.Id
|
||||
}
|
||||
|
||||
if err := db.Create(&items).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) GetByIDWithRelations(ctx context.Context, id uint64) (*entity.Purchase, error) {
|
||||
var purchase entity.Purchase
|
||||
err := r.DB().WithContext(ctx).
|
||||
Preload("Supplier").
|
||||
Preload("Items", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("id ASC")
|
||||
}).
|
||||
Preload("Items.Product").
|
||||
Preload("Items.Warehouse").
|
||||
Preload("Items.Warehouse.Area").
|
||||
Preload("Items.Warehouse.Location").
|
||||
Preload("Items.ProductWarehouse").
|
||||
First(&purchase, id).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &purchase, nil
|
||||
}
|
||||
|
||||
type PurchasePricingUpdate struct {
|
||||
ItemID uint64
|
||||
Price float64
|
||||
TotalPrice float64
|
||||
}
|
||||
|
||||
func (r *PurchaseRepositoryImpl) UpdatePricing(
|
||||
ctx context.Context,
|
||||
purchaseID uint64,
|
||||
updates []PurchasePricingUpdate,
|
||||
grandTotal float64,
|
||||
) error {
|
||||
if len(updates) == 0 {
|
||||
return errors.New("pricing updates cannot be empty")
|
||||
}
|
||||
|
||||
db := r.DB().WithContext(ctx)
|
||||
|
||||
for _, upd := range updates {
|
||||
result := db.Model(&entity.PurchaseItem{}).
|
||||
Where("purchase_id = ? AND id = ?", purchaseID, upd.ItemID).
|
||||
Updates(map[string]interface{}{
|
||||
"price": upd.Price,
|
||||
"total_price": upd.TotalPrice,
|
||||
"updated_at": gorm.Expr("NOW()"),
|
||||
})
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
}
|
||||
|
||||
if err := db.Model(&entity.Purchase{}).
|
||||
Where("id = ?", purchaseID).
|
||||
Updates(map[string]interface{}{
|
||||
"grand_total": grandTotal,
|
||||
"updated_at": gorm.Expr("NOW()"),
|
||||
}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package purchases
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/gofiber/fiber/v2"
|
||||
commonRepo "gitlab.com/mbugroup/lti-api.git/internal/common/repository"
|
||||
commonSvc "gitlab.com/mbugroup/lti-api.git/internal/common/service"
|
||||
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"
|
||||
controller "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/controllers"
|
||||
rPurchase "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/repositories"
|
||||
service "gitlab.com/mbugroup/lti-api.git/internal/modules/purchases/services"
|
||||
rUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/repositories"
|
||||
sUser "gitlab.com/mbugroup/lti-api.git/internal/modules/users/services"
|
||||
utils "gitlab.com/mbugroup/lti-api.git/internal/utils"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func RegisterRoutes(router fiber.Router, db *gorm.DB, validate *validator.Validate) {
|
||||
group := router.Group("/purchases")
|
||||
|
||||
purchaseRepo := rPurchase.NewPurchaseRepository(db)
|
||||
productWarehouseRepo := rProductWarehouse.NewProductWarehouseRepository(db)
|
||||
warehouseRepo := rWarehouse.NewWarehouseRepository(db)
|
||||
supplierRepo := rSupplier.NewSupplierRepository(db)
|
||||
userRepo := rUser.NewUserRepository(db)
|
||||
|
||||
approvalRepo := commonRepo.NewApprovalRepository(db)
|
||||
approvalService := commonSvc.NewApprovalService(approvalRepo)
|
||||
if err := approvalService.RegisterWorkflowSteps(utils.ApprovalWorkflowPurchase, utils.PurchaseApprovalSteps); err != nil {
|
||||
panic(fmt.Sprintf("failed to register purchase approval workflow: %v", err))
|
||||
}
|
||||
|
||||
purchaseService := service.NewPurchaseService(
|
||||
validate,
|
||||
purchaseRepo,
|
||||
productWarehouseRepo,
|
||||
warehouseRepo,
|
||||
supplierRepo,
|
||||
approvalRepo,
|
||||
)
|
||||
userService := sUser.NewUserService(userRepo, validate)
|
||||
|
||||
PurchaseRoutes(group, userService, purchaseService)
|
||||
}
|
||||
|
||||
func PurchaseRoutes(v1 fiber.Router, u sUser.UserService, s service.PurchaseService) {
|
||||
ctrl := controller.NewPurchaseController(s)
|
||||
|
||||
route := v1.Group("/requisitions")
|
||||
|
||||
// route.Post("/", m.Auth(u), ctrl.CreateOne)
|
||||
|
||||
route.Post("/", ctrl.CreateOne)
|
||||
route.Post("/:id/approvals/staff", ctrl.ApproveStaffPurchase)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package validation
|
||||
|
||||
type PurchaseItemPayload struct {
|
||||
ProductID uint `json:"product_id" validate:"required"`
|
||||
ProductWarehouseID *uint `json:"product_warehouse_id,omitempty" validate:"omitempty,gt=0"`
|
||||
Quantity float64 `json:"quantity" validate:"required,gt=0"`
|
||||
}
|
||||
|
||||
type CreatePurchaseRequest struct {
|
||||
SupplierID uint `json:"supplier_id" validate:"required"`
|
||||
AreaID uint `json:"area_id" validate:"required"`
|
||||
LocationID uint `json:"location_id" validate:"required"`
|
||||
WarehouseID uint `json:"warehouse_id" validate:"required"`
|
||||
Notes *string `json:"notes" validate:"omitempty,max=500"`
|
||||
Items []PurchaseItemPayload `json:"items" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type StaffPurchaseApprovalItem struct {
|
||||
PurchaseItemID uint64 `json:"purchase_item_id" validate:"required,gt=0"`
|
||||
Price float64 `json:"price" validate:"required,gt=0"`
|
||||
TotalPrice *float64 `json:"total_price,omitempty" validate:"omitempty,gt=0"`
|
||||
}
|
||||
|
||||
type ApproveStaffPurchaseRequest struct {
|
||||
Items []StaffPurchaseApprovalItem `json:"items" validate:"required,min=1,dive"`
|
||||
Notes *string `json:"notes,omitempty" validate:"omitempty,max=500"`
|
||||
}
|
||||
Reference in New Issue
Block a user