Add void and printer type

This commit is contained in:
Aditya Siregar 2025-08-06 00:02:49 +07:00
parent 11d814ab2f
commit 44dc3fa61c
8 changed files with 67 additions and 37 deletions

4
go.mod
View File

@ -54,7 +54,7 @@ require (
golang.org/x/arch v0.7.0 // indirect golang.org/x/arch v0.7.0 // indirect
golang.org/x/net v0.30.0 // indirect golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.26.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 google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
@ -68,5 +68,5 @@ require (
go.uber.org/zap v1.21.0 go.uber.org/zap v1.21.0
golang.org/x/crypto v0.28.0 golang.org/x/crypto v0.28.0
gorm.io/driver/postgres v1.5.0 gorm.io/driver/postgres v1.5.0
gorm.io/gorm v1.24.7-0.20230306060331-85eaf9eeda11 gorm.io/gorm v1.30.0
) )

7
go.sum
View File

@ -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.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.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.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= 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-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-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/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= 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 h1:u2FXTy14l45qc3UeCJ7QaAXZmZfDDv0YrthvmRq1l0U=
gorm.io/driver/postgres v1.5.0/go.mod h1:FUZXzO+5Uqg5zzwzv4KK49R8lvGIyscBOqYrtI1Ce9A= 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.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-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-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

View File

@ -72,6 +72,7 @@ type OrderResponse struct {
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
OrderItems []OrderItemResponse `json:"order_items,omitempty"` OrderItems []OrderItemResponse `json:"order_items,omitempty"`
IsRefund bool `json:"is_refund"`
} }
type OrderItemResponse struct { type OrderItemResponse struct {
@ -90,6 +91,7 @@ type OrderItemResponse struct {
Status string `json:"status"` Status string `json:"status"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
PrinterType string `json:"printer_type"`
} }
type ListOrdersQuery struct { type ListOrdersQuery struct {

View File

@ -91,14 +91,13 @@ func OrderItemEntityToResponse(item *entities.OrderItem) *models.OrderItemRespon
Status: constants.OrderItemStatus(item.Status), Status: constants.OrderItemStatus(item.Status),
CreatedAt: item.CreatedAt, CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt, UpdatedAt: item.UpdatedAt,
PrinterType: item.Product.PrinterType,
} }
// Set product name if product is preloaded
if item.Product.ID != uuid.Nil { if item.Product.ID != uuid.Nil {
response.ProductName = item.Product.Name response.ProductName = item.Product.Name
} }
// Set product variant name if product variant is preloaded
if item.ProductVariant != nil { if item.ProductVariant != nil {
response.ProductVariantName = &item.ProductVariant.Name response.ProductVariantName = &item.ProductVariant.Name
} }

View File

@ -199,6 +199,7 @@ type OrderItemResponse struct {
Status constants.OrderItemStatus Status constants.OrderItemStatus
CreatedAt time.Time CreatedAt time.Time
UpdatedAt time.Time UpdatedAt time.Time
PrinterType string
} }
type PaymentOrderItemResponse struct { type PaymentOrderItemResponse struct {

View File

@ -532,7 +532,6 @@ func (p *OrderProcessorImpl) VoidOrder(ctx context.Context, req *models.VoidOrde
} }
if req.Type == "ALL" { 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 { if err := p.orderRepo.VoidOrderWithStatus(ctx, req.OrderID, entities.OrderStatusCancelled, req.Reason, voidedBy); err != nil {
return fmt.Errorf("failed to void order: %w", err) 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) return fmt.Errorf("order item not found: %w", err)
} }
// Verify the order item belongs to this order
if orderItem.OrderID != req.OrderID { if orderItem.OrderID != req.OrderID {
return fmt.Errorf("order item does not belong to this order") return fmt.Errorf("order item does not belong to this order")
} }
// Validate void quantity
if itemVoid.Quantity > orderItem.Quantity { if itemVoid.Quantity > orderItem.Quantity {
return fmt.Errorf("void quantity cannot exceed original quantity for item %d", itemVoid.OrderItemID) return fmt.Errorf("void quantity cannot exceed original quantity for item %d", itemVoid.OrderItemID)
} }
// Calculate voided amounts
voidedAmount := float64(itemVoid.Quantity) * orderItem.UnitPrice voidedAmount := float64(itemVoid.Quantity) * orderItem.UnitPrice
voidedCost := float64(itemVoid.Quantity) * orderItem.UnitCost voidedCost := float64(itemVoid.Quantity) * orderItem.UnitCost
totalVoidedAmount += voidedAmount totalVoidedAmount += voidedAmount
totalVoidedCost += voidedCost totalVoidedCost += voidedCost
// Void the order item
if err := p.orderItemRepo.VoidOrderItem(ctx, orderItemID, itemVoid.Quantity, req.Reason, voidedBy); err != nil { 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) 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) outlet, err := p.outletRepo.GetByID(ctx, order.OutletID)
if err != nil { if err != nil {
return fmt.Errorf("outlet not found: %w", err) return fmt.Errorf("outlet not found: %w", err)
} }
// Update order totals
order.Subtotal -= totalVoidedAmount order.Subtotal -= totalVoidedAmount
order.TotalCost -= totalVoidedCost order.TotalCost -= totalVoidedCost
order.TaxAmount = order.Subtotal * outlet.TaxRate // Recalculate tax using outlet's tax rate order.TaxAmount = order.Subtotal * outlet.TaxRate // Recalculate tax using outlet's tax rate
order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount order.TotalAmount = order.Subtotal + order.TaxAmount - order.DiscountAmount
// Update the order
if err := p.orderRepo.Update(ctx, order); err != nil { if err := p.orderRepo.Update(ctx, order); err != nil {
return fmt.Errorf("failed to update order totals: %w", err) 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) remainingItems, err := p.orderItemRepo.GetByOrderID(ctx, req.OrderID)
if err != nil { if err != nil {
return fmt.Errorf("failed to get remaining order items: %w", err) return fmt.Errorf("failed to get remaining order items: %w", err)
} }
allItemsVoided := true hasActiveItems := false
for _, item := range remainingItems { for _, item := range remainingItems {
if item.Quantity > 0 { if item.Status != entities.OrderItemStatusCancelled {
allItemsVoided = false hasActiveItems = true
break break
} }
} }
if allItemsVoided { if !hasActiveItems {
// 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 { 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) return fmt.Errorf("failed to void order after all items voided: %w", err)
} }

View File

@ -2,6 +2,7 @@ package repository
import ( import (
"context" "context"
"fmt"
"time" "time"
"apskel-pos-be/internal/entities" "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 { func (r *OrderItemRepositoryImpl) VoidOrderItem(ctx context.Context, id uuid.UUID, voidQuantity int, reason string, voidedBy uuid.UUID) error {
now := time.Now() now := time.Now()
// Get current order item
var orderItem entities.OrderItem var orderItem entities.OrderItem
if err := r.db.WithContext(ctx).First(&orderItem, "id = ?", id).Error; err != nil { if err := r.db.WithContext(ctx).First(&orderItem, "id = ?", id).Error; err != nil {
return err return err
} }
// Calculate new voided quantity if voidQuantity >= orderItem.Quantity {
newVoidedQuantity := orderItem.RefundQuantity + voidQuantity // Using refund_quantity field for voided quantity voidedAmount := float64(voidQuantity) * orderItem.UnitPrice
// Determine if fully or partially voided updates := map[string]interface{}{
isFullyVoided := newVoidedQuantity >= orderItem.Quantity "refund_quantity": voidQuantity,
isPartiallyVoided := newVoidedQuantity > 0 && newVoidedQuantity < orderItem.Quantity "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 return r.db.WithContext(ctx).Model(&entities.OrderItem{}).
voidedAmount := float64(voidQuantity) * orderItem.UnitPrice 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{}{ updates := map[string]interface{}{
"refund_quantity": newVoidedQuantity, // Reusing refund_quantity field for voided quantity "quantity": remainingQuantity,
"refund_amount": orderItem.RefundAmount + voidedAmount, "total_price": remainingTotalPrice,
"is_partially_refunded": isPartiallyVoided, // Reusing refunded flags for voided status "total_cost": remainingTotalCost,
"is_fully_refunded": isFullyVoided, "updated_at": now,
"refund_reason": reason,
"refunded_at": now,
"refunded_by": voidedBy,
"status": entities.OrderItemStatusCancelled, // Mark as cancelled when voided
} }
return r.db.WithContext(ctx).Model(&entities.OrderItem{}). return r.db.WithContext(ctx).Model(&entities.OrderItem{}).

View File

@ -108,6 +108,7 @@ func OrderModelToContract(resp *models.OrderResponse) *contract.OrderResponse {
Status: string(item.Status), Status: string(item.Status),
CreatedAt: item.CreatedAt, CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt, UpdatedAt: item.UpdatedAt,
PrinterType: item.PrinterType,
} }
} }
return &contract.OrderResponse{ return &contract.OrderResponse{
@ -127,6 +128,7 @@ func OrderModelToContract(resp *models.OrderResponse) *contract.OrderResponse {
CreatedAt: resp.CreatedAt, CreatedAt: resp.CreatedAt,
UpdatedAt: resp.UpdatedAt, UpdatedAt: resp.UpdatedAt,
OrderItems: items, OrderItems: items,
IsRefund: resp.IsRefund,
} }
} }