Add coa purchase and vendors
This commit is contained in:
@@ -0,0 +1,441 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type PurchaseOrderProcessor interface {
|
||||
CreatePurchaseOrder(ctx context.Context, organizationID uuid.UUID, req *models.CreatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error)
|
||||
UpdatePurchaseOrder(ctx context.Context, id, organizationID 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)
|
||||
}
|
||||
|
||||
type PurchaseOrderProcessorImpl struct {
|
||||
purchaseOrderRepo PurchaseOrderRepository
|
||||
vendorRepo VendorRepository
|
||||
ingredientRepo IngredientRepository
|
||||
unitRepo UnitRepository
|
||||
fileRepo FileRepository
|
||||
inventoryMovementService InventoryMovementService
|
||||
unitConverterRepo IngredientUnitConverterRepository
|
||||
}
|
||||
|
||||
func NewPurchaseOrderProcessorImpl(
|
||||
purchaseOrderRepo PurchaseOrderRepository,
|
||||
vendorRepo VendorRepository,
|
||||
ingredientRepo IngredientRepository,
|
||||
unitRepo UnitRepository,
|
||||
fileRepo FileRepository,
|
||||
inventoryMovementService InventoryMovementService,
|
||||
unitConverterRepo IngredientUnitConverterRepository,
|
||||
) *PurchaseOrderProcessorImpl {
|
||||
return &PurchaseOrderProcessorImpl{
|
||||
purchaseOrderRepo: purchaseOrderRepo,
|
||||
vendorRepo: vendorRepo,
|
||||
ingredientRepo: ingredientRepo,
|
||||
unitRepo: unitRepo,
|
||||
fileRepo: fileRepo,
|
||||
inventoryMovementService: inventoryMovementService,
|
||||
unitConverterRepo: unitConverterRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PurchaseOrderProcessorImpl) CreatePurchaseOrder(ctx context.Context, organizationID uuid.UUID, req *models.CreatePurchaseOrderRequest) (*models.PurchaseOrderResponse, error) {
|
||||
// Check if vendor exists and belongs to organization
|
||||
_, err := p.vendorRepo.GetByIDAndOrganizationID(ctx, req.VendorID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("vendor not found: %w", 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 ingredients and units exist
|
||||
for i, item := range req.Items {
|
||||
_, 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total amount
|
||||
totalAmount := 0.0
|
||||
for _, item := range req.Items {
|
||||
totalAmount += item.Amount
|
||||
}
|
||||
|
||||
// Create purchase order entity
|
||||
poEntity := &entities.PurchaseOrder{
|
||||
OrganizationID: organizationID,
|
||||
VendorID: req.VendorID,
|
||||
PONumber: req.PONumber,
|
||||
TransactionDate: req.TransactionDate,
|
||||
DueDate: req.DueDate,
|
||||
Reference: req.Reference,
|
||||
Status: "draft", // Default status
|
||||
Message: req.Message,
|
||||
TotalAmount: totalAmount,
|
||||
}
|
||||
|
||||
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,
|
||||
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, 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// Update items if provided
|
||||
if req.Items != nil {
|
||||
// Delete existing items
|
||||
err = p.purchaseOrderRepo.DeleteItemsByPurchaseOrderID(ctx, poEntity.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to delete existing items: %w", err)
|
||||
}
|
||||
|
||||
// Create new items
|
||||
totalAmount := 0.0
|
||||
for _, itemReq := range req.Items {
|
||||
// Validate ingredients and units exist
|
||||
if itemReq.IngredientID != nil {
|
||||
_, err := p.ingredientRepo.GetByID(ctx, *itemReq.IngredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("ingredient not found: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if itemReq.UnitID != nil {
|
||||
_, err := p.unitRepo.GetByID(ctx, *itemReq.UnitID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unit not found: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Use existing values if not provided
|
||||
ingredientID := poEntity.Items[0].IngredientID // This is a simplified approach
|
||||
unitID := poEntity.Items[0].UnitID
|
||||
quantity := poEntity.Items[0].Quantity
|
||||
amount := poEntity.Items[0].Amount
|
||||
description := poEntity.Items[0].Description
|
||||
|
||||
if itemReq.IngredientID != nil {
|
||||
ingredientID = *itemReq.IngredientID
|
||||
}
|
||||
if itemReq.UnitID != nil {
|
||||
unitID = *itemReq.UnitID
|
||||
}
|
||||
if itemReq.Quantity != nil {
|
||||
quantity = *itemReq.Quantity
|
||||
}
|
||||
if itemReq.Amount != nil {
|
||||
amount = *itemReq.Amount
|
||||
}
|
||||
if itemReq.Description != nil {
|
||||
description = itemReq.Description
|
||||
}
|
||||
|
||||
itemEntity := &entities.PurchaseOrderItem{
|
||||
PurchaseOrderID: poEntity.ID,
|
||||
IngredientID: ingredientID,
|
||||
Description: description,
|
||||
Quantity: quantity,
|
||||
UnitID: unitID,
|
||||
Amount: amount,
|
||||
}
|
||||
|
||||
err = p.purchaseOrderRepo.CreateItem(ctx, itemEntity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create purchase order item: %w", err)
|
||||
}
|
||||
|
||||
totalAmount += amount
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
// Check if status is changing to "received" and current status is not "received"
|
||||
if status == "received" && po.Status != "received" {
|
||||
// Get purchase order with items for inventory update
|
||||
poWithItems, err := p.purchaseOrderRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get purchase order with items: %w", err)
|
||||
}
|
||||
|
||||
// Update inventory for each item
|
||||
for _, item := range poWithItems.Items {
|
||||
// Get ingredient to find its base unit
|
||||
ingredient, err := p.ingredientRepo.GetByID(ctx, item.IngredientID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ingredient %s: %w", item.IngredientID, err)
|
||||
}
|
||||
|
||||
// Convert quantity to ingredient's base unit if needed
|
||||
quantityToAdd := item.Quantity
|
||||
if item.UnitID != ingredient.UnitID {
|
||||
// Convert from purchase unit to ingredient's base unit
|
||||
convertedQuantity, err := p.unitConverterRepo.ConvertQuantity(ctx, item.IngredientID, item.UnitID, ingredient.UnitID, organizationID, item.Quantity)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert quantity for ingredient %s from unit %s to %s: %w", item.IngredientID, item.UnitID, ingredient.UnitID, err)
|
||||
}
|
||||
quantityToAdd = convertedQuantity
|
||||
}
|
||||
|
||||
// Calculate unit cost in ingredient's base unit
|
||||
unitCost := 0.0
|
||||
if quantityToAdd > 0 {
|
||||
unitCost = item.Amount / quantityToAdd
|
||||
}
|
||||
|
||||
// Create inventory movement for ingredient purchase
|
||||
reason := fmt.Sprintf("Purchase order %s received", po.PONumber)
|
||||
referenceType := entities.InventoryMovementReferenceTypePurchaseOrder
|
||||
referenceID := &id
|
||||
|
||||
err = p.inventoryMovementService.CreateIngredientMovement(
|
||||
ctx,
|
||||
item.IngredientID,
|
||||
organizationID,
|
||||
outletID,
|
||||
userID,
|
||||
entities.InventoryMovementTypePurchase,
|
||||
quantityToAdd,
|
||||
unitCost,
|
||||
reason,
|
||||
&referenceType,
|
||||
referenceID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create inventory movement for ingredient %s: %w", item.IngredientID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update the purchase order status
|
||||
err = p.purchaseOrderRepo.UpdateStatus(ctx, id, status)
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user