566 lines
21 KiB
Go
566 lines
21 KiB
Go
package processor
|
|
|
|
import (
|
|
"apskel-pos-be/internal/entities"
|
|
"apskel-pos-be/internal/mappers"
|
|
"apskel-pos-be/internal/models"
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
type PurchaseOrderProcessor interface {
|
|
CreatePurchaseOrder(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, req *models.CreatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error)
|
|
UpdatePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID, outletID *uuid.UUID, req *models.UpdatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error)
|
|
DeletePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID) error
|
|
GetPurchaseOrderByID(ctx context.Context, id, organizationID uuid.UUID) (*models.PurchaseOrderResponse, error)
|
|
ListPurchaseOrders(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.PurchaseOrderResponse, int, error)
|
|
GetPurchaseOrdersByStatus(ctx context.Context, organizationID uuid.UUID, status string) ([]*models.PurchaseOrderResponse, error)
|
|
GetOverduePurchaseOrders(ctx context.Context, organizationID uuid.UUID) ([]*models.PurchaseOrderResponse, error)
|
|
UpdatePurchaseOrderStatus(ctx context.Context, id, organizationID, userID, outletID uuid.UUID, status string) (*models.PurchaseOrderResponse, error)
|
|
ListPurchaseTeams(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) (*models.ListPurchaseTeamsResponse, error)
|
|
}
|
|
|
|
type PurchaseOrderProcessorImpl struct {
|
|
purchaseOrderRepo PurchaseOrderRepository
|
|
vendorRepo VendorRepository
|
|
ingredientRepo IngredientRepository
|
|
purchaseCategoryRepo PurchaseCategoryRepository
|
|
categoryRepo CategoryRepository
|
|
cashAdvanceRepo CashAdvanceRepository
|
|
unitRepo UnitRepository
|
|
fileRepo FileRepository
|
|
// Kept wired but currently unused: purchase orders are a record of spending
|
|
// only, so nothing here moves stock or converts units. These stay so that
|
|
// tying purchases back to inventory is a change in one place.
|
|
inventoryMovementService InventoryMovementService
|
|
unitConverterRepo IngredientUnitConverterRepository
|
|
}
|
|
|
|
func NewPurchaseOrderProcessorImpl(
|
|
purchaseOrderRepo PurchaseOrderRepository,
|
|
vendorRepo VendorRepository,
|
|
ingredientRepo IngredientRepository,
|
|
purchaseCategoryRepo PurchaseCategoryRepository,
|
|
categoryRepo CategoryRepository,
|
|
cashAdvanceRepo CashAdvanceRepository,
|
|
unitRepo UnitRepository,
|
|
fileRepo FileRepository,
|
|
inventoryMovementService InventoryMovementService,
|
|
unitConverterRepo IngredientUnitConverterRepository,
|
|
) *PurchaseOrderProcessorImpl {
|
|
return &PurchaseOrderProcessorImpl{
|
|
purchaseOrderRepo: purchaseOrderRepo,
|
|
vendorRepo: vendorRepo,
|
|
ingredientRepo: ingredientRepo,
|
|
purchaseCategoryRepo: purchaseCategoryRepo,
|
|
categoryRepo: categoryRepo,
|
|
cashAdvanceRepo: cashAdvanceRepo,
|
|
unitRepo: unitRepo,
|
|
fileRepo: fileRepo,
|
|
inventoryMovementService: inventoryMovementService,
|
|
unitConverterRepo: unitConverterRepo,
|
|
}
|
|
}
|
|
|
|
func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, req *models.CreatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) {
|
|
// Check if vendor exists and belongs to organization when provided.
|
|
if req.VendorID != nil {
|
|
_, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, *req.VendorID, organizationID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("vendor not found: %w", err)
|
|
}
|
|
}
|
|
|
|
teamScope, teamCategoryID, err := p.resolvePurchaseTeam(ctx, organizationID, outletID, req.TeamScope, req.TeamCategoryID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
teamScope, teamCategoryID, err = p.applyCashAdvance(ctx, organizationID, outletID, req.CashAdvanceID, teamScope, teamCategoryID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check if PO number already exists in organization
|
|
existingPO, err := p.purchaseOrderRepo.GetByPONumber(ctx, req.PONumber, organizationID)
|
|
if err == nil && existingPO != nil {
|
|
return nil, fmt.Errorf("purchase order with PO number %s already exists in this organization", req.PONumber)
|
|
}
|
|
|
|
// Validate categories and inventory fields per item type.
|
|
for i, item := range req.Items {
|
|
category, err := p.validatePurchaseCategory(ctx, item.PurchaseCategoryID, organizationID, i)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch category.Type {
|
|
case entities.PurchaseCategoryTypeRawMaterial:
|
|
if item.IngredientID == nil {
|
|
return nil, fmt.Errorf("ingredient_id is required for raw_material item %d", i)
|
|
}
|
|
if item.Quantity == nil {
|
|
return nil, fmt.Errorf("quantity is required for raw_material item %d", i)
|
|
}
|
|
if item.UnitID == nil {
|
|
return nil, fmt.Errorf("unit_id is required for raw_material item %d", i)
|
|
}
|
|
|
|
_, err := p.ingredientRepo.GetByID(ctx, *item.IngredientID, organizationID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ingredient not found for item %d: %w", i, err)
|
|
}
|
|
|
|
_, err = p.unitRepo.GetByID(ctx, *item.UnitID, organizationID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unit not found for item %d: %w", i, err)
|
|
}
|
|
case entities.PurchaseCategoryTypeExpense:
|
|
if item.IngredientID != nil || item.Quantity != nil || item.UnitID != nil {
|
|
return nil, fmt.Errorf("ingredient_id, quantity, and unit_id must be empty for expense item %d", i)
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("purchase category for item %d has unsupported type %s", i, category.Type)
|
|
}
|
|
}
|
|
|
|
// Calculate total amount
|
|
totalAmount := 0.0
|
|
for _, item := range req.Items {
|
|
totalAmount += calculatePurchaseOrderItemTotal(item.Quantity, item.Amount)
|
|
}
|
|
|
|
// Create purchase order entity
|
|
poEntity := &entities.PurchaseOrder{
|
|
OrganizationID: organizationID,
|
|
OutletID: outletID,
|
|
VendorID: req.VendorID,
|
|
PONumber: req.PONumber,
|
|
TransactionDate: req.TransactionDate,
|
|
DueDate: req.DueDate,
|
|
Reference: req.Reference,
|
|
Status: "draft", // Default status
|
|
Message: req.Message,
|
|
TotalAmount: totalAmount,
|
|
TeamScope: teamScope,
|
|
TeamCategoryID: teamCategoryID,
|
|
CashAdvanceID: req.CashAdvanceID,
|
|
}
|
|
|
|
if req.Status != nil {
|
|
poEntity.Status = *req.Status
|
|
}
|
|
|
|
// Create purchase order
|
|
err = p.purchaseOrderRepo.Create(ctx, poEntity)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create purchase order: %w", err)
|
|
}
|
|
|
|
// Create purchase order items
|
|
for _, itemReq := range req.Items {
|
|
itemEntity := &entities.PurchaseOrderItem{
|
|
PurchaseOrderID: poEntity.ID,
|
|
IngredientID: itemReq.IngredientID,
|
|
PurchaseCategoryID: itemReq.PurchaseCategoryID,
|
|
Description: itemReq.Description,
|
|
Quantity: itemReq.Quantity,
|
|
UnitID: itemReq.UnitID,
|
|
Amount: itemReq.Amount,
|
|
}
|
|
|
|
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create purchase order item: %w", err)
|
|
}
|
|
}
|
|
|
|
// Create attachments if provided
|
|
for _, fileID := range req.AttachmentFileIDs {
|
|
attachmentEntity := &entities.PurchaseOrderAttachment{
|
|
PurchaseOrderID: poEntity.ID,
|
|
FileID: fileID,
|
|
}
|
|
|
|
err = p.purchaseOrderRepo.CreateAttachment(ctx, attachmentEntity)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create purchase order attachment: %w", err)
|
|
}
|
|
}
|
|
|
|
// Get the created purchase order with all relations
|
|
createdPO, err := p.purchaseOrderRepo.GetByID(ctx, poEntity.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get created purchase order: %w", err)
|
|
}
|
|
|
|
return mappers.PurchaseOrderEntityToResponse(createdPO), nil
|
|
}
|
|
|
|
func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID, outletID *uuid.UUID, req *models.UpdatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) {
|
|
// Get existing purchase order
|
|
poEntity, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("purchase order not found: %w", err)
|
|
}
|
|
if poEntity.OutletID == nil && outletID != nil {
|
|
poEntity.OutletID = outletID
|
|
}
|
|
|
|
// Check if vendor exists and belongs to organization (if vendor is being updated)
|
|
if req.VendorID != nil {
|
|
_, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, *req.VendorID, organizationID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("vendor not found: %w", err)
|
|
}
|
|
poEntity.VendorID = req.VendorID
|
|
}
|
|
|
|
// Check if PO number already exists (if PO number is being updated)
|
|
if req.PONumber != nil && *req.PONumber != poEntity.PONumber {
|
|
existingPO, err := p.purchaseOrderRepo.GetByPONumber(ctx, *req.PONumber, organizationID)
|
|
if err == nil && existingPO != nil {
|
|
return nil, fmt.Errorf("purchase order with PO number %s already exists in this organization", *req.PONumber)
|
|
}
|
|
poEntity.PONumber = *req.PONumber
|
|
}
|
|
|
|
// Update other fields
|
|
if req.TransactionDate != nil {
|
|
poEntity.TransactionDate = *req.TransactionDate
|
|
}
|
|
if req.DueDate != nil {
|
|
poEntity.DueDate = req.DueDate
|
|
}
|
|
if req.Reference != nil {
|
|
poEntity.Reference = req.Reference
|
|
}
|
|
if req.Status != nil {
|
|
poEntity.Status = *req.Status
|
|
}
|
|
if req.Message != nil {
|
|
poEntity.Message = req.Message
|
|
}
|
|
|
|
// An omitted team_scope leaves the team as it is; an empty one clears it.
|
|
if req.TeamScope != nil {
|
|
teamScope, teamCategoryID, err := p.resolvePurchaseTeam(ctx, organizationID, poEntity.OutletID, req.TeamScope, req.TeamCategoryID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
poEntity.TeamScope = teamScope
|
|
poEntity.TeamCategoryID = teamCategoryID
|
|
}
|
|
|
|
// An all-zero cash advance id unlinks the purchase; omitting the field leaves it alone.
|
|
if req.CashAdvanceID != nil {
|
|
if *req.CashAdvanceID == uuid.Nil {
|
|
poEntity.CashAdvanceID = nil
|
|
} else {
|
|
poEntity.CashAdvanceID = req.CashAdvanceID
|
|
}
|
|
}
|
|
|
|
// Recheck the pairing whenever either side moved: a purchase can end up on a
|
|
// cash advance belonging to another team otherwise.
|
|
if poEntity.CashAdvanceID != nil && (req.CashAdvanceID != nil || req.TeamScope != nil) {
|
|
teamScope, teamCategoryID, err := p.applyCashAdvance(ctx, organizationID, poEntity.OutletID, poEntity.CashAdvanceID, poEntity.TeamScope, poEntity.TeamCategoryID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
poEntity.TeamScope = teamScope
|
|
poEntity.TeamCategoryID = teamCategoryID
|
|
}
|
|
|
|
// Update items if provided
|
|
if req.Items != nil {
|
|
totalAmount := 0.0
|
|
items := make([]*entities.PurchaseOrderItem, len(req.Items))
|
|
for i, itemReq := range req.Items {
|
|
if itemReq.PurchaseCategoryID == nil {
|
|
return nil, fmt.Errorf("purchase_category_id is required for item %d", i)
|
|
}
|
|
|
|
ingredientID := itemReq.IngredientID
|
|
purchaseCategoryID := *itemReq.PurchaseCategoryID
|
|
unitID := itemReq.UnitID
|
|
quantity := itemReq.Quantity
|
|
amount := 0.0
|
|
if itemReq.Amount != nil {
|
|
amount = *itemReq.Amount
|
|
}
|
|
description := itemReq.Description
|
|
|
|
category, err := p.validatePurchaseCategory(ctx, purchaseCategoryID, organizationID, i)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
switch category.Type {
|
|
case entities.PurchaseCategoryTypeRawMaterial:
|
|
if ingredientID == nil {
|
|
return nil, fmt.Errorf("ingredient_id is required for raw_material item %d", i)
|
|
}
|
|
if quantity == nil {
|
|
return nil, fmt.Errorf("quantity is required for raw_material item %d", i)
|
|
}
|
|
if unitID == nil {
|
|
return nil, fmt.Errorf("unit_id is required for raw_material item %d", i)
|
|
}
|
|
|
|
_, err := p.ingredientRepo.GetByID(ctx, *ingredientID, organizationID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ingredient not found: %w", err)
|
|
}
|
|
|
|
_, err = p.unitRepo.GetByID(ctx, *unitID, organizationID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unit not found: %w", err)
|
|
}
|
|
case entities.PurchaseCategoryTypeExpense:
|
|
if ingredientID != nil || quantity != nil || unitID != nil {
|
|
return nil, fmt.Errorf("ingredient_id, quantity, and unit_id must be empty for expense item %d", i)
|
|
}
|
|
default:
|
|
return nil, fmt.Errorf("purchase category for item %d has unsupported type %s", i, category.Type)
|
|
}
|
|
|
|
items[i] = &entities.PurchaseOrderItem{
|
|
PurchaseOrderID: poEntity.ID,
|
|
IngredientID: ingredientID,
|
|
PurchaseCategoryID: purchaseCategoryID,
|
|
Description: description,
|
|
Quantity: quantity,
|
|
UnitID: unitID,
|
|
Amount: amount,
|
|
}
|
|
totalAmount += calculatePurchaseOrderItemTotal(quantity, amount)
|
|
}
|
|
|
|
// Delete and recreate only after all replacement items are valid.
|
|
err = p.purchaseOrderRepo.DeleteItemsByPurchaseOrderID(ctx, poEntity.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to delete existing items: %w", err)
|
|
}
|
|
|
|
for _, itemEntity := range items {
|
|
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create purchase order item: %w", err)
|
|
}
|
|
}
|
|
|
|
poEntity.TotalAmount = totalAmount
|
|
}
|
|
|
|
// Update attachments if provided
|
|
if req.AttachmentFileIDs != nil {
|
|
// Delete existing attachments
|
|
err = p.purchaseOrderRepo.DeleteAttachmentsByPurchaseOrderID(ctx, poEntity.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to delete existing attachments: %w", err)
|
|
}
|
|
|
|
// Create new attachments
|
|
for _, fileID := range req.AttachmentFileIDs {
|
|
attachmentEntity := &entities.PurchaseOrderAttachment{
|
|
PurchaseOrderID: poEntity.ID,
|
|
FileID: fileID,
|
|
}
|
|
|
|
err = p.purchaseOrderRepo.CreateAttachment(ctx, attachmentEntity)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create purchase order attachment: %w", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Update purchase order
|
|
err = p.purchaseOrderRepo.Update(ctx, poEntity)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update purchase order: %w", err)
|
|
}
|
|
|
|
// Get the updated purchase order with all relations
|
|
updatedPO, err := p.purchaseOrderRepo.GetByID(ctx, poEntity.ID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get updated purchase order: %w", err)
|
|
}
|
|
|
|
return mappers.PurchaseOrderEntityToResponse(updatedPO), nil
|
|
}
|
|
|
|
func (p *PurchaseOrderProcessorImpl) DeletePurchaseOrder(ctx context.Context, id, organizationID uuid.UUID) error {
|
|
_, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
|
|
if err != nil {
|
|
return fmt.Errorf("purchase order not found: %w", err)
|
|
}
|
|
|
|
err = p.purchaseOrderRepo.Delete(ctx, id)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to delete purchase order: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *PurchaseOrderProcessorImpl) GetPurchaseOrderByID(ctx context.Context, id, organizationID uuid.UUID) (*models.PurchaseOrderResponse, error) {
|
|
poEntity, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("purchase order not found: %w", err)
|
|
}
|
|
|
|
return mappers.PurchaseOrderEntityToResponse(poEntity), nil
|
|
}
|
|
|
|
func (p *PurchaseOrderProcessorImpl) ListPurchaseOrders(ctx context.Context, organizationID uuid.UUID, filters map[string]interface{}, page, limit int) ([]*models.PurchaseOrderResponse, int, error) {
|
|
offset := (page - 1) * limit
|
|
poEntities, total, err := p.purchaseOrderRepo.List(ctx, organizationID, filters, limit, offset)
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to list purchase orders: %w", err)
|
|
}
|
|
|
|
poResponses := make([]*models.PurchaseOrderResponse, len(poEntities))
|
|
for i, poEntity := range poEntities {
|
|
poResponses[i] = mappers.PurchaseOrderEntityToResponse(poEntity)
|
|
}
|
|
|
|
totalPages := int((total + int64(limit) - 1) / int64(limit))
|
|
return poResponses, totalPages, nil
|
|
}
|
|
|
|
func (p *PurchaseOrderProcessorImpl) GetPurchaseOrdersByStatus(ctx context.Context, organizationID uuid.UUID, status string) ([]*models.PurchaseOrderResponse, error) {
|
|
poEntities, err := p.purchaseOrderRepo.GetByStatus(ctx, organizationID, status)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get purchase orders by status: %w", err)
|
|
}
|
|
|
|
poResponses := make([]*models.PurchaseOrderResponse, len(poEntities))
|
|
for i, poEntity := range poEntities {
|
|
poResponses[i] = mappers.PurchaseOrderEntityToResponse(poEntity)
|
|
}
|
|
|
|
return poResponses, nil
|
|
}
|
|
|
|
func (p *PurchaseOrderProcessorImpl) GetOverduePurchaseOrders(ctx context.Context, organizationID uuid.UUID) ([]*models.PurchaseOrderResponse, error) {
|
|
poEntities, err := p.purchaseOrderRepo.GetOverdue(ctx, organizationID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get overdue purchase orders: %w", err)
|
|
}
|
|
|
|
poResponses := make([]*models.PurchaseOrderResponse, len(poEntities))
|
|
for i, poEntity := range poEntities {
|
|
poResponses[i] = mappers.PurchaseOrderEntityToResponse(poEntity)
|
|
}
|
|
|
|
return poResponses, nil
|
|
}
|
|
|
|
func (p *PurchaseOrderProcessorImpl) UpdatePurchaseOrderStatus(ctx context.Context, id, organizationID, userID, outletID uuid.UUID, status string) (*models.PurchaseOrderResponse, error) {
|
|
// Get the purchase order with items to check current status
|
|
po, err := p.purchaseOrderRepo.GetByIDAndOrganizationID(ctx, id, organizationID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("purchase order not found: %w", err)
|
|
}
|
|
|
|
fmt.Println("status:", po.Status)
|
|
|
|
// A purchase order is a record of spending only. Receiving one does not move
|
|
// ingredient stock, does not recalculate ingredient cost, and never converts
|
|
// units: the quantity and unit on an item are kept exactly as the user
|
|
// entered them. Raw material items are therefore treated the same way expense
|
|
// items already were, and the ingredient on an item is just a reference.
|
|
|
|
// Update the purchase order status
|
|
statusOutletID := po.OutletID
|
|
if statusOutletID == nil && outletID != uuid.Nil {
|
|
statusOutletID = &outletID
|
|
}
|
|
|
|
err = p.purchaseOrderRepo.UpdateStatusAndOutlet(ctx, id, status, statusOutletID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to update purchase order status: %w", err)
|
|
}
|
|
|
|
// Get the updated purchase order
|
|
updatedPO, err := p.purchaseOrderRepo.GetByID(ctx, id)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get updated purchase order: %w", err)
|
|
}
|
|
|
|
return mappers.PurchaseOrderEntityToResponse(updatedPO), nil
|
|
}
|
|
|
|
// ListPurchaseTeams returns the teams a purchase can be charged to. Cash advances are
|
|
// charged to the same teams, so the list itself is built in one shared place.
|
|
func (p *PurchaseOrderProcessorImpl) ListPurchaseTeams(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID) (*models.ListPurchaseTeamsResponse, error) {
|
|
return listTeams(ctx, p.categoryRepo, organizationID, outletID)
|
|
}
|
|
|
|
// resolvePurchaseTeam turns a requested team into the scope/category pair stored on
|
|
// the purchase order. A nil or empty scope leaves the purchase without a team, which
|
|
// is deliberately different from Pusat.
|
|
func (p *PurchaseOrderProcessorImpl) resolvePurchaseTeam(ctx context.Context, organizationID uuid.UUID, outletID *uuid.UUID, scope *string, categoryID *uuid.UUID) (*string, *uuid.UUID, error) {
|
|
return resolveTeamSelection(ctx, p.categoryRepo, organizationID, outletID, scope, categoryID)
|
|
}
|
|
|
|
// applyCashAdvance checks a purchase may be charged to the cash advance it names, and returns
|
|
// the team it should carry. A purchase paid out of a team's cash belongs to that
|
|
// team, so an unassigned purchase inherits it and an assigned one has to agree.
|
|
func (p *PurchaseOrderProcessorImpl) applyCashAdvance(ctx context.Context, organizationID uuid.UUID, outletID, cashAdvanceID *uuid.UUID, teamScope *string, teamCategoryID *uuid.UUID) (*string, *uuid.UUID, error) {
|
|
if cashAdvanceID == nil {
|
|
return teamScope, teamCategoryID, nil
|
|
}
|
|
|
|
cashAdvance, err := resolveSpendingCashAdvance(ctx, p.cashAdvanceRepo, *cashAdvanceID, organizationID, outletID)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
if teamScope == nil {
|
|
scope := cashAdvance.TeamScope
|
|
return &scope, cashAdvance.TeamCategoryID, nil
|
|
}
|
|
|
|
if *teamScope != cashAdvance.TeamScope || !sameUUID(teamCategoryID, cashAdvance.TeamCategoryID) {
|
|
return nil, nil, fmt.Errorf("purchase order team must match the team cash advance %s was issued to", cashAdvance.CodeNumber)
|
|
}
|
|
|
|
return teamScope, teamCategoryID, nil
|
|
}
|
|
|
|
func sameUUID(a, b *uuid.UUID) bool {
|
|
if a == nil || b == nil {
|
|
return a == nil && b == nil
|
|
}
|
|
return *a == *b
|
|
}
|
|
|
|
func (p *PurchaseOrderProcessorImpl) validatePurchaseCategory(ctx context.Context, categoryID, organizationID uuid.UUID, itemIndex int) (*entities.PurchaseCategory, error) {
|
|
category, err := p.purchaseCategoryRepo.GetByIDAndOrganizationID(ctx, categoryID, organizationID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("purchase category not found for item %d: %w", itemIndex, err)
|
|
}
|
|
|
|
if !category.IsActive {
|
|
return nil, fmt.Errorf("purchase category for item %d is inactive", itemIndex)
|
|
}
|
|
|
|
if category.Type != entities.PurchaseCategoryTypeRawMaterial && category.Type != entities.PurchaseCategoryTypeExpense {
|
|
return nil, fmt.Errorf("purchase category for item %d must be raw_material or expense", itemIndex)
|
|
}
|
|
|
|
return category, nil
|
|
}
|
|
|
|
func calculatePurchaseOrderItemTotal(quantity *float64, amount float64) float64 {
|
|
if quantity == nil {
|
|
return amount
|
|
}
|
|
|
|
return *quantity * amount
|
|
}
|