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 }