diff --git a/go.mod b/go.mod index 0604cbf..3539fbb 100644 --- a/go.mod +++ b/go.mod @@ -54,7 +54,7 @@ require ( golang.org/x/arch v0.7.0 // indirect golang.org/x/net v0.30.0 // indirect golang.org/x/sys v0.26.0 // indirect - golang.org/x/text v0.19.0 // indirect + golang.org/x/text v0.20.0 // indirect google.golang.org/protobuf v1.32.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -68,5 +68,5 @@ require ( go.uber.org/zap v1.21.0 golang.org/x/crypto v0.28.0 gorm.io/driver/postgres v1.5.0 - gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 + gorm.io/gorm v1.30.0 ) diff --git a/go.sum b/go.sum index 2c52301..6cc295c 100644 --- a/go.sum +++ b/go.sum @@ -442,8 +442,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= -golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug= +golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -607,8 +607,9 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/postgres v1.5.0 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U= gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= -gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 h1:9qNbmu21nNThCNnF5i2R3kw2aL27U8ZwbzccNjOmW0g= gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= +gorm.io/gorm v1.30.0 h1:qbT5aPv1UH8gI99OsRlvDToLxW5zR7FzS9acZDOZcgs= +gorm.io/gorm v1.30.0/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/internal/contract/order_contract.go b/internal/contract/order_contract.go index 1b16142..4279e9f 100644 --- a/internal/contract/order_contract.go +++ b/internal/contract/order_contract.go @@ -72,6 +72,7 @@ type OrderResponse struct { CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` OrderItems []OrderItemResponse `json:"order_items,omitempty"` + IsRefund bool `json:"is_refund"` } type OrderItemResponse struct { @@ -90,6 +91,7 @@ type OrderItemResponse struct { Status string `json:"status"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` + PrinterType string `json:"printer_type"` } type ListOrdersQuery struct { diff --git a/internal/mappers/order_mapper.go b/internal/mappers/order_mapper.go index 7e72b16..601e8bb 100644 --- a/internal/mappers/order_mapper.go +++ b/internal/mappers/order_mapper.go @@ -91,14 +91,13 @@ func OrderItemEntityToResponse(item *entities.OrderItem) *models.OrderItemRespon Status: constants.OrderItemStatus(item.Status), CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, + PrinterType: item.Product.PrinterType, } - // Set product name if product is preloaded if item.Product.ID != uuid.Nil { response.ProductName = item.Product.Name } - // Set product variant name if product variant is preloaded if item.ProductVariant != nil { response.ProductVariantName = &item.ProductVariant.Name } diff --git a/internal/models/order.go b/internal/models/order.go index 9e1ce87..a221810 100644 --- a/internal/models/order.go +++ b/internal/models/order.go @@ -199,6 +199,7 @@ type OrderItemResponse struct { Status constants.OrderItemStatus CreatedAt time.Time UpdatedAt time.Time + PrinterType string } type PaymentOrderItemResponse struct { diff --git a/internal/processor/order_processor.go b/internal/processor/order_processor.go index 403fd77..a9d750a 100644 --- a/internal/processor/order_processor.go +++ b/internal/processor/order_processor.go @@ -532,7 +532,6 @@ func (p *OrderProcessorImpl) VoidOrder(ctx context.Context, req *models.VoidOrde } 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) } @@ -552,62 +551,53 @@ func (p *OrderProcessorImpl) VoidOrder(ctx context.Context, req *models.VoidOrde 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 + hasActiveItems := false for _, item := range remainingItems { - if item.Quantity > 0 { - allItemsVoided = false + if item.Status != entities.OrderItemStatusCancelled { + hasActiveItems = true break } } - if allItemsVoided { - // Update order status to cancelled and mark as voided when all items are voided + if !hasActiveItems { 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) } diff --git a/internal/repository/order_item_repository.go b/internal/repository/order_item_repository.go index 04ddb59..d983b6b 100644 --- a/internal/repository/order_item_repository.go +++ b/internal/repository/order_item_repository.go @@ -2,6 +2,7 @@ package repository import ( "context" + "fmt" "time" "apskel-pos-be/internal/entities" @@ -103,31 +104,65 @@ func (r *OrderItemRepositoryImpl) UpdateStatus(ctx context.Context, id uuid.UUID func (r *OrderItemRepositoryImpl) VoidOrderItem(ctx context.Context, id uuid.UUID, voidQuantity int, reason string, voidedBy uuid.UUID) error { now := time.Now() - // Get current order item var orderItem entities.OrderItem if err := r.db.WithContext(ctx).First(&orderItem, "id = ?", id).Error; err != nil { return err } - // Calculate new voided quantity - newVoidedQuantity := orderItem.RefundQuantity + voidQuantity // Using refund_quantity field for voided quantity + if voidQuantity >= orderItem.Quantity { + voidedAmount := float64(voidQuantity) * orderItem.UnitPrice - // Determine if fully or partially voided - isFullyVoided := newVoidedQuantity >= orderItem.Quantity - isPartiallyVoided := newVoidedQuantity > 0 && newVoidedQuantity < orderItem.Quantity + updates := map[string]interface{}{ + "refund_quantity": voidQuantity, + "refund_amount": voidedAmount, + "is_partially_refunded": false, + "is_fully_refunded": true, + "refund_reason": reason, + "refunded_at": now, + "refunded_by": voidedBy, + "status": entities.OrderItemStatusCancelled, + } - // Calculate voided amount - voidedAmount := float64(voidQuantity) * orderItem.UnitPrice + return r.db.WithContext(ctx).Model(&entities.OrderItem{}). + Where("id = ?", id). + Updates(updates).Error + } + + voidedOrderItem := entities.OrderItem{ + OrderID: orderItem.OrderID, + ProductID: orderItem.ProductID, + ProductVariantID: orderItem.ProductVariantID, + Quantity: voidQuantity, + UnitPrice: orderItem.UnitPrice, + TotalPrice: float64(voidQuantity) * orderItem.UnitPrice, + UnitCost: orderItem.UnitCost, + TotalCost: float64(voidQuantity) * orderItem.UnitCost, + RefundAmount: float64(voidQuantity) * orderItem.UnitPrice, + RefundQuantity: voidQuantity, + IsPartiallyRefunded: false, + IsFullyRefunded: true, + RefundReason: &reason, + RefundedAt: &now, + RefundedBy: &voidedBy, + Modifiers: orderItem.Modifiers, + Notes: orderItem.Notes, + Metadata: orderItem.Metadata, + Status: entities.OrderItemStatusCancelled, + } + + if err := r.db.WithContext(ctx).Create(&voidedOrderItem).Error; err != nil { + return fmt.Errorf("failed to create voided order item: %w", err) + } + + remainingQuantity := orderItem.Quantity - voidQuantity + remainingTotalPrice := float64(remainingQuantity) * orderItem.UnitPrice + remainingTotalCost := float64(remainingQuantity) * orderItem.UnitCost updates := map[string]interface{}{ - "refund_quantity": newVoidedQuantity, // Reusing refund_quantity field for voided quantity - "refund_amount": orderItem.RefundAmount + voidedAmount, - "is_partially_refunded": isPartiallyVoided, // Reusing refunded flags for voided status - "is_fully_refunded": isFullyVoided, - "refund_reason": reason, - "refunded_at": now, - "refunded_by": voidedBy, - "status": entities.OrderItemStatusCancelled, // Mark as cancelled when voided + "quantity": remainingQuantity, + "total_price": remainingTotalPrice, + "total_cost": remainingTotalCost, + "updated_at": now, } return r.db.WithContext(ctx).Model(&entities.OrderItem{}). diff --git a/internal/transformer/order_transformer.go b/internal/transformer/order_transformer.go index eff4dc2..288cb50 100644 --- a/internal/transformer/order_transformer.go +++ b/internal/transformer/order_transformer.go @@ -108,6 +108,7 @@ func OrderModelToContract(resp *models.OrderResponse) *contract.OrderResponse { Status: string(item.Status), CreatedAt: item.CreatedAt, UpdatedAt: item.UpdatedAt, + PrinterType: item.PrinterType, } } return &contract.OrderResponse{ @@ -127,6 +128,7 @@ func OrderModelToContract(resp *models.OrderResponse) *contract.OrderResponse { CreatedAt: resp.CreatedAt, UpdatedAt: resp.UpdatedAt, OrderItems: items, + IsRefund: resp.IsRefund, } }