init
This commit is contained in:
@@ -0,0 +1,884 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"apskel-pos-be/internal/constants"
|
||||
"apskel-pos-be/internal/entities"
|
||||
"apskel-pos-be/internal/mappers"
|
||||
"apskel-pos-be/internal/models"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type OrderProcessor interface {
|
||||
CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error)
|
||||
AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error)
|
||||
UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error)
|
||||
GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error)
|
||||
ListOrders(ctx context.Context, req *models.ListOrdersRequest) (*models.ListOrdersResponse, error)
|
||||
VoidOrder(ctx context.Context, req *models.VoidOrderRequest, voidedBy uuid.UUID) error
|
||||
RefundOrder(ctx context.Context, id uuid.UUID, req *models.RefundOrderRequest, refundedBy uuid.UUID) error
|
||||
CreatePayment(ctx context.Context, req *models.CreatePaymentRequest) (*models.PaymentResponse, error)
|
||||
RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error
|
||||
SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error)
|
||||
}
|
||||
|
||||
type OrderRepository interface {
|
||||
Create(ctx context.Context, order *entities.Order) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.Order, error)
|
||||
GetWithRelations(ctx context.Context, id uuid.UUID) (*entities.Order, error)
|
||||
Update(ctx context.Context, order *entities.Order) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
List(ctx context.Context, filters map[string]interface{}, limit, offset int) ([]*entities.Order, int64, error)
|
||||
GetByOrderNumber(ctx context.Context, orderNumber string) (*entities.Order, error)
|
||||
ExistsByOrderNumber(ctx context.Context, orderNumber string) (bool, error)
|
||||
VoidOrder(ctx context.Context, id uuid.UUID, reason string, voidedBy uuid.UUID) error
|
||||
VoidOrderWithStatus(ctx context.Context, id uuid.UUID, status entities.OrderStatus, reason string, voidedBy uuid.UUID) error
|
||||
RefundOrder(ctx context.Context, id uuid.UUID, reason string, refundedBy uuid.UUID) error
|
||||
UpdatePaymentStatus(ctx context.Context, id uuid.UUID, status entities.PaymentStatus) error
|
||||
UpdateStatus(ctx context.Context, id uuid.UUID, status entities.OrderStatus) error
|
||||
GetNextOrderNumber(ctx context.Context, organizationID, outletID uuid.UUID) (string, error)
|
||||
}
|
||||
|
||||
type OrderItemRepository interface {
|
||||
Create(ctx context.Context, orderItem *entities.OrderItem) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.OrderItem, error)
|
||||
GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.OrderItem, error)
|
||||
Update(ctx context.Context, orderItem *entities.OrderItem) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
RefundOrderItem(ctx context.Context, id uuid.UUID, refundQuantity int, refundAmount float64, reason string, refundedBy uuid.UUID) error
|
||||
VoidOrderItem(ctx context.Context, id uuid.UUID, voidQuantity int, reason string, voidedBy uuid.UUID) error
|
||||
UpdateStatus(ctx context.Context, id uuid.UUID, status entities.OrderItemStatus) error
|
||||
}
|
||||
|
||||
type PaymentRepository interface {
|
||||
Create(ctx context.Context, payment *entities.Payment) error
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.Payment, error)
|
||||
GetByOrderID(ctx context.Context, orderID uuid.UUID) ([]*entities.Payment, error)
|
||||
Update(ctx context.Context, payment *entities.Payment) error
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
RefundPayment(ctx context.Context, id uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error
|
||||
UpdateStatus(ctx context.Context, id uuid.UUID, status entities.PaymentTransactionStatus) error
|
||||
GetTotalPaidByOrderID(ctx context.Context, orderID uuid.UUID) (float64, error)
|
||||
}
|
||||
|
||||
type PaymentMethodRepository interface {
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.PaymentMethod, error)
|
||||
}
|
||||
|
||||
type ProductVariantRepository interface {
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*entities.ProductVariant, error)
|
||||
}
|
||||
|
||||
type CustomerRepository interface {
|
||||
GetByIDAndOrganization(ctx context.Context, id, organizationID uuid.UUID) (*entities.Customer, error)
|
||||
}
|
||||
|
||||
type SimplePaymentMethodRepository struct{}
|
||||
|
||||
func (r *SimplePaymentMethodRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.PaymentMethod, error) {
|
||||
// TODO: Implement proper payment method repository
|
||||
// For now, return a mock payment method
|
||||
return &entities.PaymentMethod{
|
||||
ID: id,
|
||||
Name: "Cash",
|
||||
Type: entities.PaymentMethodTypeCash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type OrderProcessorImpl struct {
|
||||
orderRepo OrderRepository
|
||||
orderItemRepo OrderItemRepository
|
||||
paymentRepo PaymentRepository
|
||||
productRepo ProductRepository
|
||||
paymentMethodRepo PaymentMethodRepository
|
||||
inventoryRepo InventoryRepository
|
||||
productVariantRepo ProductVariantRepository
|
||||
outletRepo OutletRepository
|
||||
customerRepo CustomerRepository
|
||||
}
|
||||
|
||||
func NewOrderProcessorImpl(
|
||||
orderRepo OrderRepository,
|
||||
orderItemRepo OrderItemRepository,
|
||||
paymentRepo PaymentRepository,
|
||||
productRepo ProductRepository,
|
||||
paymentMethodRepo PaymentMethodRepository,
|
||||
inventoryRepo InventoryRepository,
|
||||
productVariantRepo ProductVariantRepository,
|
||||
outletRepo OutletRepository,
|
||||
customerRepo CustomerRepository,
|
||||
) *OrderProcessorImpl {
|
||||
return &OrderProcessorImpl{
|
||||
orderRepo: orderRepo,
|
||||
orderItemRepo: orderItemRepo,
|
||||
paymentRepo: paymentRepo,
|
||||
productRepo: productRepo,
|
||||
paymentMethodRepo: paymentMethodRepo,
|
||||
inventoryRepo: inventoryRepo,
|
||||
productVariantRepo: productVariantRepo,
|
||||
outletRepo: outletRepo,
|
||||
customerRepo: customerRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error) {
|
||||
orderNumber, err := p.orderRepo.GetNextOrderNumber(ctx, organizationID, req.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate order number: %w", err)
|
||||
}
|
||||
|
||||
outlet, err := p.outletRepo.GetByID(ctx, req.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
var subtotal, totalCost float64
|
||||
var orderItems []*entities.OrderItem
|
||||
|
||||
for _, itemReq := range req.OrderItems {
|
||||
product, err := p.productRepo.GetByID(ctx, itemReq.ProductID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("product not found: %w", err)
|
||||
}
|
||||
|
||||
unitPrice := product.Price
|
||||
unitCost := product.Cost
|
||||
|
||||
if itemReq.ProductVariantID != nil {
|
||||
variant, err := p.productVariantRepo.GetByID(ctx, *itemReq.ProductVariantID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("product variant not found: %w", err)
|
||||
}
|
||||
|
||||
if variant.ProductID != itemReq.ProductID {
|
||||
return nil, fmt.Errorf("product variant does not belong to the specified product")
|
||||
}
|
||||
|
||||
unitPrice += variant.PriceModifier
|
||||
if variant.Cost > 0 {
|
||||
unitCost = variant.Cost
|
||||
}
|
||||
}
|
||||
|
||||
itemTotalPrice := float64(itemReq.Quantity) * unitPrice
|
||||
itemTotalCost := float64(itemReq.Quantity) * unitCost
|
||||
|
||||
subtotal += itemTotalPrice
|
||||
totalCost += itemTotalCost
|
||||
|
||||
orderItem := &entities.OrderItem{
|
||||
ProductID: itemReq.ProductID,
|
||||
ProductVariantID: itemReq.ProductVariantID,
|
||||
Quantity: itemReq.Quantity,
|
||||
UnitPrice: unitPrice, // Use price from database
|
||||
TotalPrice: itemTotalPrice,
|
||||
UnitCost: unitCost,
|
||||
TotalCost: itemTotalCost,
|
||||
Modifiers: entities.Modifiers(itemReq.Modifiers),
|
||||
Notes: itemReq.Notes,
|
||||
Metadata: entities.Metadata(itemReq.Metadata),
|
||||
Status: entities.OrderItemStatusPending,
|
||||
}
|
||||
|
||||
orderItems = append(orderItems, orderItem)
|
||||
}
|
||||
|
||||
taxAmount := subtotal * outlet.TaxRate
|
||||
totalAmount := subtotal + taxAmount
|
||||
|
||||
metadata := entities.Metadata(req.Metadata)
|
||||
if req.CustomerName != nil {
|
||||
if metadata == nil {
|
||||
metadata = make(entities.Metadata)
|
||||
}
|
||||
metadata["customer_name"] = *req.CustomerName
|
||||
}
|
||||
order := &entities.Order{
|
||||
OrganizationID: organizationID,
|
||||
OutletID: req.OutletID,
|
||||
UserID: req.UserID,
|
||||
CustomerID: req.CustomerID,
|
||||
OrderNumber: orderNumber,
|
||||
TableNumber: req.TableNumber,
|
||||
OrderType: entities.OrderType(req.OrderType),
|
||||
Status: entities.OrderStatusPending,
|
||||
Subtotal: subtotal,
|
||||
TaxAmount: taxAmount,
|
||||
DiscountAmount: 0,
|
||||
TotalAmount: totalAmount,
|
||||
TotalCost: totalCost,
|
||||
PaymentStatus: entities.PaymentStatusPending,
|
||||
IsVoid: false,
|
||||
IsRefund: false,
|
||||
Metadata: metadata,
|
||||
}
|
||||
|
||||
if err := p.orderRepo.Create(ctx, order); err != nil {
|
||||
return nil, fmt.Errorf("failed to create order: %w", err)
|
||||
}
|
||||
|
||||
for _, orderItem := range orderItems {
|
||||
orderItem.OrderID = order.ID
|
||||
if err := p.orderItemRepo.Create(ctx, orderItem); err != nil {
|
||||
return nil, fmt.Errorf("failed to create order item: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, order.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve created order: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.OrderEntityToResponse(orderWithRelations)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) {
|
||||
order, err := p.orderRepo.GetByID(ctx, orderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
if order.IsVoid {
|
||||
return nil, fmt.Errorf("cannot modify voided order")
|
||||
}
|
||||
|
||||
if order.PaymentStatus == entities.PaymentStatusCompleted {
|
||||
return nil, fmt.Errorf("cannot modify fully paid order")
|
||||
}
|
||||
|
||||
// Get outlet information for tax rate
|
||||
outlet, err := p.outletRepo.GetByID(ctx, order.OutletID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
var newSubtotal, newTotalCost float64
|
||||
var addedOrderItems []*entities.OrderItem
|
||||
|
||||
for _, itemReq := range req.OrderItems {
|
||||
product, err := p.productRepo.GetByID(ctx, itemReq.ProductID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("product not found: %w", err)
|
||||
}
|
||||
|
||||
// Use product price from database
|
||||
unitPrice := product.Price
|
||||
unitCost := product.Cost
|
||||
|
||||
// Handle product variant if specified
|
||||
if itemReq.ProductVariantID != nil {
|
||||
variant, err := p.productVariantRepo.GetByID(ctx, *itemReq.ProductVariantID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("product variant not found: %w", err)
|
||||
}
|
||||
|
||||
// Verify variant belongs to the product
|
||||
if variant.ProductID != itemReq.ProductID {
|
||||
return nil, fmt.Errorf("product variant does not belong to the specified product")
|
||||
}
|
||||
|
||||
// Apply price modifier
|
||||
unitPrice += variant.PriceModifier
|
||||
// Use variant cost if available, otherwise use product cost
|
||||
if variant.Cost > 0 {
|
||||
unitCost = variant.Cost
|
||||
}
|
||||
}
|
||||
|
||||
itemTotalPrice := float64(itemReq.Quantity) * unitPrice
|
||||
itemTotalCost := float64(itemReq.Quantity) * unitCost
|
||||
|
||||
newSubtotal += itemTotalPrice
|
||||
newTotalCost += itemTotalCost
|
||||
|
||||
orderItem := &entities.OrderItem{
|
||||
OrderID: orderID,
|
||||
ProductID: itemReq.ProductID,
|
||||
ProductVariantID: itemReq.ProductVariantID,
|
||||
Quantity: itemReq.Quantity,
|
||||
UnitPrice: unitPrice, // Use price from database
|
||||
TotalPrice: itemTotalPrice,
|
||||
UnitCost: unitCost,
|
||||
TotalCost: itemTotalCost,
|
||||
Modifiers: entities.Modifiers(itemReq.Modifiers),
|
||||
Notes: itemReq.Notes,
|
||||
Metadata: entities.Metadata(itemReq.Metadata),
|
||||
Status: entities.OrderItemStatusPending,
|
||||
}
|
||||
|
||||
addedOrderItems = append(addedOrderItems, orderItem)
|
||||
}
|
||||
|
||||
order.Subtotal += newSubtotal
|
||||
order.TotalCost += newTotalCost
|
||||
// Recalculate tax amount using outlet's tax rate
|
||||
order.TaxAmount = order.Subtotal * outlet.TaxRate
|
||||
order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount
|
||||
|
||||
if req.Metadata != nil {
|
||||
if order.Metadata == nil {
|
||||
order.Metadata = make(entities.Metadata)
|
||||
}
|
||||
for k, v := range req.Metadata {
|
||||
order.Metadata[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order: %w", err)
|
||||
}
|
||||
|
||||
var addedItemResponses []models.OrderItemResponse
|
||||
for _, orderItem := range addedOrderItems {
|
||||
if err := p.orderItemRepo.Create(ctx, orderItem); err != nil {
|
||||
return nil, fmt.Errorf("failed to create order item: %w", err)
|
||||
}
|
||||
|
||||
itemResponse := models.OrderItemResponse{
|
||||
ID: orderItem.ID,
|
||||
OrderID: orderItem.OrderID,
|
||||
ProductID: orderItem.ProductID,
|
||||
ProductVariantID: orderItem.ProductVariantID,
|
||||
Quantity: orderItem.Quantity,
|
||||
UnitPrice: orderItem.UnitPrice,
|
||||
TotalPrice: orderItem.TotalPrice,
|
||||
UnitCost: orderItem.UnitCost,
|
||||
TotalCost: orderItem.TotalCost,
|
||||
RefundAmount: orderItem.RefundAmount,
|
||||
RefundQuantity: orderItem.RefundQuantity,
|
||||
IsPartiallyRefunded: orderItem.IsPartiallyRefunded,
|
||||
IsFullyRefunded: orderItem.IsFullyRefunded,
|
||||
RefundReason: orderItem.RefundReason,
|
||||
RefundedAt: orderItem.RefundedAt,
|
||||
RefundedBy: orderItem.RefundedBy,
|
||||
Modifiers: []map[string]interface{}(orderItem.Modifiers),
|
||||
Notes: orderItem.Notes,
|
||||
Metadata: map[string]interface{}(orderItem.Metadata),
|
||||
Status: constants.OrderItemStatus(orderItem.Status),
|
||||
CreatedAt: orderItem.CreatedAt,
|
||||
UpdatedAt: orderItem.UpdatedAt,
|
||||
}
|
||||
addedItemResponses = append(addedItemResponses, itemResponse)
|
||||
}
|
||||
|
||||
orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, orderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve updated order: %w", err)
|
||||
}
|
||||
|
||||
updatedOrderResponse := mappers.OrderEntityToResponse(orderWithRelations)
|
||||
|
||||
return &models.AddToOrderResponse{
|
||||
OrderID: orderID,
|
||||
OrderNumber: order.OrderNumber,
|
||||
AddedItems: addedItemResponses,
|
||||
UpdatedOrder: *updatedOrderResponse,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) {
|
||||
// Get existing order
|
||||
order, err := p.orderRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
// Check if order can be modified
|
||||
if order.IsVoid {
|
||||
return nil, fmt.Errorf("cannot modify voided order")
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
if req.TableNumber != nil {
|
||||
order.TableNumber = req.TableNumber
|
||||
}
|
||||
if req.Status != nil {
|
||||
order.Status = entities.OrderStatus(*req.Status)
|
||||
}
|
||||
if req.DiscountAmount != nil {
|
||||
order.DiscountAmount = *req.DiscountAmount
|
||||
// Recalculate total amount
|
||||
order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount
|
||||
}
|
||||
if req.Metadata != nil {
|
||||
if order.Metadata == nil {
|
||||
order.Metadata = make(entities.Metadata)
|
||||
}
|
||||
for k, v := range req.Metadata {
|
||||
order.Metadata[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Update order
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order: %w", err)
|
||||
}
|
||||
|
||||
// Get updated order with relations
|
||||
orderWithRelations, err := p.orderRepo.GetWithRelations(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve updated order: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.OrderEntityToResponse(orderWithRelations)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error) {
|
||||
order, err := p.orderRepo.GetWithRelations(ctx, id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.OrderEntityToResponse(order)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) ListOrders(ctx context.Context, req *models.ListOrdersRequest) (*models.ListOrdersResponse, error) {
|
||||
filters := make(map[string]interface{})
|
||||
if req.OrganizationID != nil {
|
||||
filters["organization_id"] = *req.OrganizationID
|
||||
}
|
||||
if req.OutletID != nil {
|
||||
filters["outlet_id"] = *req.OutletID
|
||||
}
|
||||
if req.UserID != nil {
|
||||
filters["user_id"] = *req.UserID
|
||||
}
|
||||
if req.CustomerID != nil {
|
||||
filters["customer_id"] = *req.CustomerID
|
||||
}
|
||||
if req.OrderType != nil {
|
||||
filters["order_type"] = string(*req.OrderType)
|
||||
}
|
||||
if req.Status != nil {
|
||||
filters["status"] = string(*req.Status)
|
||||
}
|
||||
if req.PaymentStatus != nil {
|
||||
filters["payment_status"] = string(*req.PaymentStatus)
|
||||
}
|
||||
if req.IsVoid != nil {
|
||||
filters["is_void"] = *req.IsVoid
|
||||
}
|
||||
if req.IsRefund != nil {
|
||||
filters["is_refund"] = *req.IsRefund
|
||||
}
|
||||
if req.DateFrom != nil {
|
||||
filters["date_from"] = *req.DateFrom
|
||||
}
|
||||
if req.DateTo != nil {
|
||||
filters["date_to"] = *req.DateTo
|
||||
}
|
||||
if req.Search != "" {
|
||||
filters["search"] = req.Search
|
||||
}
|
||||
|
||||
offset := (req.Page - 1) * req.Limit
|
||||
orders, total, err := p.orderRepo.List(ctx, filters, req.Limit, offset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list orders: %w", err)
|
||||
}
|
||||
|
||||
// Convert to responses
|
||||
orderResponses := make([]models.OrderResponse, len(orders))
|
||||
for i, order := range orders {
|
||||
response := mappers.OrderEntityToResponse(order)
|
||||
if response != nil {
|
||||
orderResponses[i] = *response
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate total pages
|
||||
totalPages := int(total) / req.Limit
|
||||
if int(total)%req.Limit > 0 {
|
||||
totalPages++
|
||||
}
|
||||
|
||||
return &models.ListOrdersResponse{
|
||||
Orders: orderResponses,
|
||||
TotalCount: int(total),
|
||||
Page: req.Page,
|
||||
Limit: req.Limit,
|
||||
TotalPages: totalPages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) VoidOrder(ctx context.Context, req *models.VoidOrderRequest, voidedBy uuid.UUID) error {
|
||||
if req.OrderID != req.OrderID {
|
||||
return fmt.Errorf("order ID mismatch: path parameter does not match request body")
|
||||
}
|
||||
|
||||
order, err := p.orderRepo.GetByID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
if order.IsVoid {
|
||||
return fmt.Errorf("order is already voided")
|
||||
}
|
||||
|
||||
if order.PaymentStatus == entities.PaymentStatusCompleted {
|
||||
return fmt.Errorf("cannot void fully paid order")
|
||||
}
|
||||
|
||||
if req.Type == "ALL" {
|
||||
// Update order status to cancelled and mark as voided in a single transaction
|
||||
if err := p.orderRepo.VoidOrderWithStatus(ctx, req.OrderID, entities.OrderStatusCancelled, req.Reason, voidedBy); err != nil {
|
||||
return fmt.Errorf("failed to void order: %w", err)
|
||||
}
|
||||
} else if req.Type == "ITEM" {
|
||||
if len(req.Items) == 0 {
|
||||
return fmt.Errorf("items list is required when voiding specific items")
|
||||
}
|
||||
|
||||
var totalVoidedAmount float64
|
||||
var totalVoidedCost float64
|
||||
|
||||
for _, itemVoid := range req.Items {
|
||||
orderItemID := itemVoid.OrderItemID
|
||||
|
||||
orderItem, err := p.orderItemRepo.GetByID(ctx, orderItemID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("order item not found: %w", err)
|
||||
}
|
||||
|
||||
// Verify the order item belongs to this order
|
||||
if orderItem.OrderID != req.OrderID {
|
||||
return fmt.Errorf("order item does not belong to this order")
|
||||
}
|
||||
|
||||
// Validate void quantity
|
||||
if itemVoid.Quantity > orderItem.Quantity {
|
||||
return fmt.Errorf("void quantity cannot exceed original quantity for item %d", itemVoid.OrderItemID)
|
||||
}
|
||||
|
||||
// Calculate voided amounts
|
||||
voidedAmount := float64(itemVoid.Quantity) * orderItem.UnitPrice
|
||||
voidedCost := float64(itemVoid.Quantity) * orderItem.UnitCost
|
||||
|
||||
totalVoidedAmount += voidedAmount
|
||||
totalVoidedCost += voidedCost
|
||||
|
||||
// Void the order item
|
||||
if err := p.orderItemRepo.VoidOrderItem(ctx, orderItemID, itemVoid.Quantity, req.Reason, voidedBy); err != nil {
|
||||
return fmt.Errorf("failed to void order item %d: %w", itemVoid.OrderItemID, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Get outlet information for tax rate
|
||||
outlet, err := p.outletRepo.GetByID(ctx, order.OutletID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("outlet not found: %w", err)
|
||||
}
|
||||
|
||||
// Update order totals
|
||||
order.Subtotal -= totalVoidedAmount
|
||||
order.TotalCost -= totalVoidedCost
|
||||
order.TaxAmount = order.Subtotal * outlet.TaxRate // Recalculate tax using outlet's tax rate
|
||||
order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount
|
||||
|
||||
// Update the order
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return fmt.Errorf("failed to update order totals: %w", err)
|
||||
}
|
||||
|
||||
// Check if all items are voided, then void the entire order
|
||||
remainingItems, err := p.orderItemRepo.GetByOrderID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get remaining order items: %w", err)
|
||||
}
|
||||
|
||||
allItemsVoided := true
|
||||
for _, item := range remainingItems {
|
||||
if item.Quantity > 0 {
|
||||
allItemsVoided = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allItemsVoided {
|
||||
// Update order status to cancelled and mark as voided when all items are voided
|
||||
if err := p.orderRepo.VoidOrderWithStatus(ctx, req.OrderID, entities.OrderStatusCancelled, req.Reason, voidedBy); err != nil {
|
||||
return fmt.Errorf("failed to void order after all items voided: %w", err)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("invalid void type: must be 'ALL' or 'ITEM'")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) RefundOrder(ctx context.Context, id uuid.UUID, req *models.RefundOrderRequest, refundedBy uuid.UUID) error {
|
||||
order, err := p.orderRepo.GetWithRelations(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
// Check if order can be refunded
|
||||
if order.IsRefund {
|
||||
return fmt.Errorf("order is already refunded")
|
||||
}
|
||||
|
||||
if order.PaymentStatus != entities.PaymentStatusCompleted {
|
||||
return fmt.Errorf("order is not paid, cannot refund")
|
||||
}
|
||||
|
||||
reason := "No reason provided"
|
||||
if req.Reason != nil {
|
||||
reason = *req.Reason
|
||||
}
|
||||
|
||||
// Process refund based on request type
|
||||
if req.RefundAmount != nil {
|
||||
// Full or partial refund by amount
|
||||
if *req.RefundAmount > order.TotalAmount {
|
||||
return fmt.Errorf("refund amount cannot exceed order total")
|
||||
}
|
||||
|
||||
// Update order refund amount
|
||||
order.RefundAmount = *req.RefundAmount
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return fmt.Errorf("failed to update order refund amount: %w", err)
|
||||
}
|
||||
|
||||
// Mark order as refunded
|
||||
if err := p.orderRepo.RefundOrder(ctx, id, reason, refundedBy); err != nil {
|
||||
return fmt.Errorf("failed to mark order as refunded: %w", err)
|
||||
}
|
||||
|
||||
} else if len(req.OrderItems) > 0 {
|
||||
// Refund by specific items
|
||||
totalRefundAmount := float64(0)
|
||||
|
||||
for _, itemRefund := range req.OrderItems {
|
||||
// Get order item
|
||||
orderItem, err := p.orderItemRepo.GetByID(ctx, itemRefund.OrderItemID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("order item not found: %w", err)
|
||||
}
|
||||
|
||||
if orderItem.OrderID != id {
|
||||
return fmt.Errorf("order item does not belong to this order")
|
||||
}
|
||||
|
||||
// Calculate refund amount for this item
|
||||
refundQuantity := itemRefund.RefundQuantity
|
||||
if refundQuantity == 0 {
|
||||
refundQuantity = orderItem.Quantity
|
||||
}
|
||||
|
||||
if refundQuantity > orderItem.Quantity {
|
||||
return fmt.Errorf("refund quantity cannot exceed original quantity")
|
||||
}
|
||||
|
||||
refundAmount := float64(refundQuantity) * orderItem.UnitPrice
|
||||
if itemRefund.RefundAmount != nil {
|
||||
refundAmount = *itemRefund.RefundAmount
|
||||
}
|
||||
|
||||
// Process item refund
|
||||
itemReason := reason
|
||||
if itemRefund.Reason != nil {
|
||||
itemReason = *itemRefund.Reason
|
||||
}
|
||||
|
||||
if err := p.orderItemRepo.RefundOrderItem(ctx, itemRefund.OrderItemID, refundQuantity, refundAmount, itemReason, refundedBy); err != nil {
|
||||
return fmt.Errorf("failed to refund order item: %w", err)
|
||||
}
|
||||
|
||||
totalRefundAmount += refundAmount
|
||||
}
|
||||
|
||||
// Update order refund amount
|
||||
order.RefundAmount = totalRefundAmount
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return fmt.Errorf("failed to update order refund amount: %w", err)
|
||||
}
|
||||
|
||||
// Mark order as refunded
|
||||
if err := p.orderRepo.RefundOrder(ctx, id, reason, refundedBy); err != nil {
|
||||
return fmt.Errorf("failed to mark order as refunded: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) CreatePayment(ctx context.Context, req *models.CreatePaymentRequest) (*models.PaymentResponse, error) {
|
||||
order, err := p.orderRepo.GetByID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
if order.IsVoid {
|
||||
return nil, fmt.Errorf("cannot process payment for voided order")
|
||||
}
|
||||
|
||||
if order.PaymentStatus == entities.PaymentStatusCompleted {
|
||||
return nil, fmt.Errorf("order is already fully paid")
|
||||
}
|
||||
|
||||
_, err = p.paymentMethodRepo.GetByID(ctx, req.PaymentMethodID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("payment method not found: %w", err)
|
||||
}
|
||||
|
||||
totalPaid, err := p.paymentRepo.GetTotalPaidByOrderID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get total paid: %w", err)
|
||||
}
|
||||
|
||||
remainingAmount := order.TotalAmount - totalPaid
|
||||
if req.Amount > remainingAmount {
|
||||
return nil, fmt.Errorf("payment amount exceeds remaining balance")
|
||||
}
|
||||
|
||||
payment := &entities.Payment{
|
||||
OrderID: req.OrderID,
|
||||
PaymentMethodID: req.PaymentMethodID,
|
||||
Amount: req.Amount,
|
||||
Status: entities.PaymentTransactionStatusCompleted,
|
||||
TransactionID: req.TransactionID,
|
||||
SplitNumber: req.SplitNumber,
|
||||
SplitTotal: req.SplitTotal,
|
||||
SplitDescription: req.SplitDescription,
|
||||
Metadata: entities.Metadata(req.Metadata),
|
||||
}
|
||||
|
||||
if err := p.paymentRepo.Create(ctx, payment); err != nil {
|
||||
return nil, fmt.Errorf("failed to create payment: %w", err)
|
||||
}
|
||||
|
||||
if len(req.PaymentOrderItems) > 0 {
|
||||
for _, itemPayment := range req.PaymentOrderItems {
|
||||
paymentOrderItem := &entities.PaymentOrderItem{
|
||||
PaymentID: payment.ID,
|
||||
OrderItemID: itemPayment.OrderItemID,
|
||||
Amount: itemPayment.Amount,
|
||||
}
|
||||
|
||||
fmt.Println(paymentOrderItem)
|
||||
// TODO: Create payment order item in database
|
||||
// This would require a PaymentOrderItemRepository
|
||||
}
|
||||
}
|
||||
|
||||
// Update order payment status if fully paid
|
||||
newTotalPaid := totalPaid + req.Amount
|
||||
orderJustCompleted := false
|
||||
if newTotalPaid >= order.TotalAmount {
|
||||
if order.PaymentStatus != entities.PaymentStatusCompleted {
|
||||
orderJustCompleted = true
|
||||
}
|
||||
if err := p.orderRepo.UpdatePaymentStatus(ctx, req.OrderID, entities.PaymentStatusCompleted); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order payment status: %w", err)
|
||||
}
|
||||
// Set order status to completed when fully paid
|
||||
if err := p.orderRepo.UpdateStatus(ctx, req.OrderID, entities.OrderStatusCompleted); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order status: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := p.orderRepo.UpdatePaymentStatus(ctx, req.OrderID, entities.PaymentStatusPartiallyRefunded); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order payment status: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if orderJustCompleted {
|
||||
orderItems, err := p.orderItemRepo.GetByOrderID(ctx, req.OrderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get order items for inventory adjustment: %w", err)
|
||||
}
|
||||
for _, item := range orderItems {
|
||||
if _, err := p.inventoryRepo.AdjustQuantity(ctx, item.ProductID, order.OutletID, -item.Quantity); err != nil {
|
||||
return nil, fmt.Errorf("failed to adjust inventory for product %s: %w", item.ProductID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get payment with relations for response
|
||||
paymentWithRelations, err := p.paymentRepo.GetByID(ctx, payment.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve created payment: %w", err)
|
||||
}
|
||||
|
||||
response := mappers.PaymentEntityToResponse(paymentWithRelations)
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error {
|
||||
// Get payment
|
||||
payment, err := p.paymentRepo.GetByID(ctx, paymentID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("payment not found: %w", err)
|
||||
}
|
||||
|
||||
// Check if payment can be refunded
|
||||
if payment.Status != entities.PaymentTransactionStatusCompleted {
|
||||
return fmt.Errorf("payment is not completed, cannot refund")
|
||||
}
|
||||
|
||||
if refundAmount > payment.Amount {
|
||||
return fmt.Errorf("refund amount cannot exceed payment amount")
|
||||
}
|
||||
|
||||
// Process refund
|
||||
if err := p.paymentRepo.RefundPayment(ctx, paymentID, refundAmount, reason, refundedBy); err != nil {
|
||||
return fmt.Errorf("failed to refund payment: %w", err)
|
||||
}
|
||||
|
||||
// Update order refund amount
|
||||
order, err := p.orderRepo.GetByID(ctx, payment.OrderID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get order: %w", err)
|
||||
}
|
||||
|
||||
order.RefundAmount += refundAmount
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return fmt.Errorf("failed to update order refund amount: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *OrderProcessorImpl) SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error) {
|
||||
// Get the order
|
||||
order, err := p.orderRepo.GetByID(ctx, orderID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("order not found: %w", err)
|
||||
}
|
||||
|
||||
// Verify order belongs to the organization
|
||||
if order.OrganizationID != organizationID {
|
||||
return nil, fmt.Errorf("order does not belong to the organization")
|
||||
}
|
||||
|
||||
// Check if order status is pending (only pending orders can have customer set)
|
||||
if order.Status != entities.OrderStatusPending {
|
||||
return nil, fmt.Errorf("customer can only be set for pending orders")
|
||||
}
|
||||
|
||||
// Verify customer exists and belongs to the organization
|
||||
customer, err := p.customerRepo.GetByIDAndOrganization(ctx, req.CustomerID, organizationID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("customer not found or does not belong to the organization: %w", err)
|
||||
}
|
||||
|
||||
// Update order with customer ID
|
||||
order.CustomerID = &req.CustomerID
|
||||
if err := p.orderRepo.Update(ctx, order); err != nil {
|
||||
return nil, fmt.Errorf("failed to update order with customer: %w", err)
|
||||
}
|
||||
|
||||
response := &models.SetOrderCustomerResponse{
|
||||
OrderID: orderID,
|
||||
CustomerID: req.CustomerID,
|
||||
Message: fmt.Sprintf("Customer '%s' successfully set for order", customer.Name),
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
Reference in New Issue
Block a user