808 lines
27 KiB
Go
Raw Normal View History

2025-07-18 20:10:29 +07:00
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"
2025-07-30 23:18:20 +07:00
"apskel-pos-be/internal/repository"
2025-07-18 20:10:29 +07:00
"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)
2025-07-30 23:18:20 +07:00
CreatePaymentWithInventoryMovement(ctx context.Context, req *models.CreatePaymentRequest, order *entities.Order, totalPaid float64) (*entities.Payment, error)
RefundPaymentWithInventoryMovement(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID, order *entities.Payment) error
2025-07-18 20:10:29 +07:00
}
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 {
2025-07-30 23:18:20 +07:00
orderRepo OrderRepository
orderItemRepo OrderItemRepository
paymentRepo PaymentRepository
productRepo ProductRepository
paymentMethodRepo PaymentMethodRepository
inventoryRepo repository.InventoryRepository
inventoryMovementRepo repository.InventoryMovementRepository
productVariantRepo ProductVariantRepository
outletRepo OutletRepository
customerRepo CustomerRepository
2025-07-18 20:10:29 +07:00
}
func NewOrderProcessorImpl(
orderRepo OrderRepository,
orderItemRepo OrderItemRepository,
paymentRepo PaymentRepository,
productRepo ProductRepository,
paymentMethodRepo PaymentMethodRepository,
2025-07-30 23:18:20 +07:00
inventoryRepo repository.InventoryRepository,
inventoryMovementRepo repository.InventoryMovementRepository,
2025-07-18 20:10:29 +07:00
productVariantRepo ProductVariantRepository,
outletRepo OutletRepository,
customerRepo CustomerRepository,
) *OrderProcessorImpl {
return &OrderProcessorImpl{
2025-07-30 23:18:20 +07:00
orderRepo: orderRepo,
orderItemRepo: orderItemRepo,
paymentRepo: paymentRepo,
productRepo: productRepo,
paymentMethodRepo: paymentMethodRepo,
inventoryRepo: inventoryRepo,
inventoryMovementRepo: inventoryMovementRepo,
productVariantRepo: productVariantRepo,
outletRepo: outletRepo,
customerRepo: customerRepo,
2025-07-18 20:10:29 +07:00
}
}
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" {
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)
}
if orderItem.OrderID != req.OrderID {
return fmt.Errorf("order item does not belong to this order")
}
if itemVoid.Quantity > orderItem.Quantity {
return fmt.Errorf("void quantity cannot exceed original quantity for item %d", itemVoid.OrderItemID)
}
voidedAmount := float64(itemVoid.Quantity) * orderItem.UnitPrice
voidedCost := float64(itemVoid.Quantity) * orderItem.UnitCost
totalVoidedAmount += voidedAmount
totalVoidedCost += voidedCost
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)
}
}
outlet, err := p.outletRepo.GetByID(ctx, order.OutletID)
if err != nil {
return fmt.Errorf("outlet not found: %w", err)
}
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
if err := p.orderRepo.Update(ctx, order); err != nil {
return fmt.Errorf("failed to update order totals: %w", err)
}
remainingItems, err := p.orderItemRepo.GetByOrderID(ctx, req.OrderID)
if err != nil {
return fmt.Errorf("failed to get remaining order items: %w", err)
}
2025-08-06 00:02:49 +07:00
hasActiveItems := false
2025-07-18 20:10:29 +07:00
for _, item := range remainingItems {
2025-08-06 00:02:49 +07:00
if item.Status != entities.OrderItemStatusCancelled {
hasActiveItems = true
2025-07-18 20:10:29 +07:00
break
}
}
2025-08-06 00:02:49 +07:00
if !hasActiveItems {
2025-07-18 20:10:29 +07:00
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)
}
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")
}
2025-07-30 23:18:20 +07:00
payment, err := p.paymentRepo.CreatePaymentWithInventoryMovement(ctx, req, order, totalPaid)
if err != nil {
return nil, err
2025-07-18 20:10:29 +07:00
}
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
}
2025-07-30 23:18:20 +07:00
func stringPtr(s string) *string {
return &s
}
2025-07-18 20:10:29 +07:00
func (p *OrderProcessorImpl) RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error {
payment, err := p.paymentRepo.GetByID(ctx, paymentID)
if err != nil {
return fmt.Errorf("payment not found: %w", err)
}
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")
}
2025-07-30 23:18:20 +07:00
return p.paymentRepo.RefundPaymentWithInventoryMovement(ctx, paymentID, refundAmount, reason, refundedBy, payment)
2025-07-18 20:10:29 +07:00
}
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
}