Adds GET /customer/orders and GET /customer/orders/:id for the customer app: the orders linked to the logged-in customer across their organization's outlets, newest first, paginated (limit 1-100, default 20). The list shows the order number, outlet, type, status, total, item count, void/refund flags and the EnakPoint and EnakCoin it earned. The detail adds the amounts, the items with product and variant names, weight and unit for weighed lines, modifiers and notes, and the payments with the method and, for EnakPoint, the points used. Costs, cashier and other internal fields are left out. Another customer's order answers 404 like one that does not exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
158 lines
5.5 KiB
Go
158 lines
5.5 KiB
Go
package repository
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"gorm.io/gorm"
|
|
|
|
"apskel-pos-be/internal/entities"
|
|
)
|
|
|
|
// ErrCustomerOrderNotFound means the order does not exist or belongs to another
|
|
// customer; the two are not told apart.
|
|
var ErrCustomerOrderNotFound = errors.New("order not found")
|
|
|
|
// CustomerOrderRow is one order as the customer app lists it.
|
|
type CustomerOrderRow struct {
|
|
ID uuid.UUID
|
|
OrderNumber string
|
|
OutletID uuid.UUID
|
|
OutletName string
|
|
OrderType string
|
|
TableNumber *string
|
|
Status string
|
|
PaymentStatus string
|
|
Subtotal float64
|
|
DiscountAmount float64
|
|
TaxAmount float64
|
|
TotalAmount float64
|
|
RefundAmount float64
|
|
IsVoid bool
|
|
IsRefund bool
|
|
ItemCount int64
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// CustomerOrderItemRow is one line of an order, without costs.
|
|
type CustomerOrderItemRow struct {
|
|
ID uuid.UUID
|
|
ProductID uuid.UUID
|
|
ProductName string
|
|
VariantName *string
|
|
Quantity int
|
|
Weight *float64
|
|
UnitName *string
|
|
UnitPrice float64
|
|
TotalPrice float64
|
|
RefundQuantity int
|
|
Modifiers entities.Modifiers `gorm:"type:jsonb"`
|
|
Notes *string
|
|
Status string
|
|
}
|
|
|
|
// CustomerOrderPaymentRow is one payment of an order.
|
|
type CustomerOrderPaymentRow struct {
|
|
ID uuid.UUID
|
|
MethodName string
|
|
MethodType string
|
|
Amount float64
|
|
Status string
|
|
RefundAmount float64
|
|
PointsUsed *int64
|
|
PointValue *float64
|
|
CreatedAt time.Time
|
|
}
|
|
|
|
// CustomerOrderRepository reads a customer's own orders for the customer app. Every
|
|
// read is scoped to the customer, so one customer can never see another's order.
|
|
type CustomerOrderRepository interface {
|
|
// ListOrders returns a page of the customer's orders, newest first, and the total.
|
|
ListOrders(ctx context.Context, customerID uuid.UUID, offset, limit int) ([]CustomerOrderRow, int64, error)
|
|
// GetOrder returns ErrCustomerOrderNotFound unless the order is the customer's.
|
|
GetOrder(ctx context.Context, customerID, orderID uuid.UUID) (*CustomerOrderRow, error)
|
|
ListItems(ctx context.Context, orderID uuid.UUID) ([]CustomerOrderItemRow, error)
|
|
ListPayments(ctx context.Context, orderID uuid.UUID) ([]CustomerOrderPaymentRow, error)
|
|
}
|
|
|
|
type customerOrderRepository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
func NewCustomerOrderRepository(db *gorm.DB) CustomerOrderRepository {
|
|
return &customerOrderRepository{db: db}
|
|
}
|
|
|
|
const customerOrderColumns = `o.id, o.order_number, o.outlet_id, COALESCE(ol.name, '') AS outlet_name,
|
|
o.order_type, o.table_number, o.status, o.payment_status, o.subtotal, o.discount_amount,
|
|
o.tax_amount, o.total_amount, o.refund_amount, o.is_void, o.is_refund,
|
|
(SELECT COUNT(*) FROM order_items oi WHERE oi.order_id = o.id) AS item_count, o.created_at`
|
|
|
|
func (r *customerOrderRepository) ListOrders(ctx context.Context, customerID uuid.UUID, offset, limit int) ([]CustomerOrderRow, int64, error) {
|
|
db := DBFromContext(ctx, r.db).WithContext(ctx)
|
|
var total int64
|
|
if err := db.Table("orders").Where("customer_id = ?", customerID).Count(&total).Error; err != nil {
|
|
return nil, 0, fmt.Errorf("failed to count customer orders: %w", err)
|
|
}
|
|
var rows []CustomerOrderRow
|
|
err := db.Raw(`SELECT `+customerOrderColumns+`
|
|
FROM orders o LEFT JOIN outlets ol ON ol.id = o.outlet_id
|
|
WHERE o.customer_id = ?
|
|
ORDER BY o.created_at DESC, o.id
|
|
LIMIT ? OFFSET ?`, customerID, limit, offset).Scan(&rows).Error
|
|
if err != nil {
|
|
return nil, 0, fmt.Errorf("failed to list customer orders: %w", err)
|
|
}
|
|
return rows, total, nil
|
|
}
|
|
|
|
func (r *customerOrderRepository) GetOrder(ctx context.Context, customerID, orderID uuid.UUID) (*CustomerOrderRow, error) {
|
|
var rows []CustomerOrderRow
|
|
err := DBFromContext(ctx, r.db).WithContext(ctx).Raw(`SELECT `+customerOrderColumns+`
|
|
FROM orders o LEFT JOIN outlets ol ON ol.id = o.outlet_id
|
|
WHERE o.id = ? AND o.customer_id = ?`, orderID, customerID).Scan(&rows).Error
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to get customer order: %w", err)
|
|
}
|
|
if len(rows) == 0 {
|
|
return nil, ErrCustomerOrderNotFound
|
|
}
|
|
return &rows[0], nil
|
|
}
|
|
|
|
func (r *customerOrderRepository) ListItems(ctx context.Context, orderID uuid.UUID) ([]CustomerOrderItemRow, error) {
|
|
var rows []CustomerOrderItemRow
|
|
err := DBFromContext(ctx, r.db).WithContext(ctx).Raw(`
|
|
SELECT oi.id, oi.product_id, COALESCE(p.name, '') AS product_name, pv.name AS variant_name,
|
|
oi.quantity, oi.weight, COALESCE(u.abbreviation, u.name) AS unit_name,
|
|
oi.unit_price, oi.total_price, oi.refund_quantity, oi.modifiers, oi.notes, oi.status
|
|
FROM order_items oi
|
|
LEFT JOIN products p ON p.id = oi.product_id
|
|
LEFT JOIN product_variants pv ON pv.id = oi.product_variant_id
|
|
LEFT JOIN units u ON u.id = oi.unit_id
|
|
WHERE oi.order_id = ?
|
|
ORDER BY oi.created_at, oi.id`, orderID).Scan(&rows).Error
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list order items: %w", err)
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func (r *customerOrderRepository) ListPayments(ctx context.Context, orderID uuid.UUID) ([]CustomerOrderPaymentRow, error) {
|
|
var rows []CustomerOrderPaymentRow
|
|
err := DBFromContext(ctx, r.db).WithContext(ctx).Raw(`
|
|
SELECT pay.id, COALESCE(pm.name, '') AS method_name, COALESCE(pm.type, '') AS method_type,
|
|
pay.amount, pay.status, pay.refund_amount, pay.points_used, pay.point_value, pay.created_at
|
|
FROM payments pay
|
|
LEFT JOIN payment_methods pm ON pm.id = pay.payment_method_id
|
|
WHERE pay.order_id = ?
|
|
ORDER BY pay.created_at, pay.id`, orderID).Scan(&rows).Error
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to list order payments: %w", err)
|
|
}
|
|
return rows, nil
|
|
}
|