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 }