unfinished purchase

This commit is contained in:
ragilap
2025-11-05 18:58:06 +07:00
parent 4aed480662
commit 8f74391f1e
23 changed files with 1155 additions and 52 deletions
@@ -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,
}
}
+13
View File
@@ -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
}
+59
View File
@@ -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"`
}