EnakPoint can only be redeemed for vouchers now: it can no longer pay for orders and is never cashed out (docs/enakgame-prd.md §3.2, EG-001, EG-002). No order was ever paid with EnakPoint, so there is no data to move. Removed: - POST /customer/wallet/payment-code, POST /customer/orders/:id/pay-with-points and GET /orders/:id/point-payment/preview, with their processors, repositories, services, handlers and tests. - The point payment method type: paying, splitting and refunding with it, the outlet filter on the method list, and the system-method guard. - points and payment_code on CreatePayment; points_used and point_value on payments; accepts_point_payment on the customer outlets. - The outlet point_payment settings. A PUT that still sends them is rejected as an unknown field. - The EnakPoint split in the payment method analytics. - PAYMENT and PAYMENT_REFUND from the wallet type rules. Tests that used them as a generic EnakPoint debit use REWARD_REDEEM. - The EnakPoint-paid part from the earning basis, which is subtotal − discount again. Migration 000102 drops the trigger, the point methods and their index, the payments columns, and the outlet settings, and restores the method type CHECK without point. payments.payment_method_id is ON DELETE RESTRICT, so it fails rather than lose a payment made with EnakPoint. The integration docs list the removed endpoints and fields, and the EnakPoint & EnakCoin PRD and tasks note what is superseded. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
156 lines
5.5 KiB
Go
156 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
|
|
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.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
|
|
}
|