Adds POST /customer/orders/:id/pay-with-points (docs/prd-point-coin.md F9, PC-306) for the customer app and self-order. It uses the same payment path as the cashier, approved by the customer's PIN instead of a code: the session alone is not enough (K8), and a wrong PIN takes nothing and counts toward the lock. A customer can pay only their own order; any other order, and one that does not exist, answer 404 alike, so the endpoint does not reveal other customers' orders. The method is the organization's EnakPoint method, no cashier is recorded, and settling the order triggers earning through the same onOrderPaid hook as every other payment. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
330 lines
9.9 KiB
Go
330 lines
9.9 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"apskel-pos-be/internal/entities"
|
|
"apskel-pos-be/internal/models"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/mock"
|
|
)
|
|
|
|
// Mock implementations for testing
|
|
type MockOrderProcessor struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *MockOrderProcessor) CreateOrder(ctx context.Context, req *models.CreateOrderRequest, organizationID uuid.UUID) (*models.OrderResponse, error) {
|
|
args := m.Called(ctx, req, organizationID)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*models.OrderResponse), args.Error(1)
|
|
}
|
|
|
|
func (m *MockOrderProcessor) AddToOrder(ctx context.Context, orderID uuid.UUID, req *models.AddToOrderRequest) (*models.AddToOrderResponse, error) {
|
|
args := m.Called(ctx, orderID, req)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*models.AddToOrderResponse), args.Error(1)
|
|
}
|
|
|
|
func (m *MockOrderProcessor) UpdateOrder(ctx context.Context, id uuid.UUID, req *models.UpdateOrderRequest) (*models.OrderResponse, error) {
|
|
args := m.Called(ctx, id, req)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*models.OrderResponse), args.Error(1)
|
|
}
|
|
|
|
func (m *MockOrderProcessor) PayWithPointsInApp(ctx context.Context, customerID, orderID uuid.UUID, points int64, pin string, info models.CustomerPinRequestInfo) (*models.PaymentResponse, error) {
|
|
args := m.Called(ctx, customerID, orderID, points, pin, info)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*models.PaymentResponse), args.Error(1)
|
|
}
|
|
|
|
func (m *MockOrderProcessor) GetOrderByID(ctx context.Context, id uuid.UUID) (*models.OrderResponse, error) {
|
|
args := m.Called(ctx, id)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*models.OrderResponse), args.Error(1)
|
|
}
|
|
|
|
func (m *MockOrderProcessor) ListOrders(ctx context.Context, req *models.ListOrdersRequest) (*models.ListOrdersResponse, error) {
|
|
args := m.Called(ctx, req)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*models.ListOrdersResponse), args.Error(1)
|
|
}
|
|
|
|
func (m *MockOrderProcessor) VoidOrder(ctx context.Context, req *models.VoidOrderRequest, voidedBy uuid.UUID) error {
|
|
args := m.Called(ctx, req, voidedBy)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockOrderProcessor) RefundOrder(ctx context.Context, id uuid.UUID, req *models.RefundOrderRequest, refundedBy uuid.UUID) error {
|
|
args := m.Called(ctx, id, req, refundedBy)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockOrderProcessor) CreatePayment(ctx context.Context, req *models.CreatePaymentRequest) (*models.PaymentResponse, error) {
|
|
args := m.Called(ctx, req)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*models.PaymentResponse), args.Error(1)
|
|
}
|
|
|
|
func (m *MockOrderProcessor) RefundPayment(ctx context.Context, paymentID uuid.UUID, refundAmount float64, reason string, refundedBy uuid.UUID) error {
|
|
args := m.Called(ctx, paymentID, refundAmount, reason, refundedBy)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockOrderProcessor) SetOrderCustomer(ctx context.Context, orderID uuid.UUID, req *models.SetOrderCustomerRequest, organizationID uuid.UUID) (*models.SetOrderCustomerResponse, error) {
|
|
args := m.Called(ctx, orderID, req, organizationID)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*models.SetOrderCustomerResponse), args.Error(1)
|
|
}
|
|
|
|
func (m *MockOrderProcessor) SplitBill(ctx context.Context, req *models.SplitBillRequest) (*models.SplitBillResponse, error) {
|
|
args := m.Called(ctx, req)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*models.SplitBillResponse), args.Error(1)
|
|
}
|
|
|
|
type MockTableRepository struct {
|
|
mock.Mock
|
|
}
|
|
|
|
func (m *MockTableRepository) Create(ctx context.Context, table *entities.Table) error {
|
|
args := m.Called(ctx, table)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockTableRepository) GetByID(ctx context.Context, id uuid.UUID) (*entities.Table, error) {
|
|
args := m.Called(ctx, id)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*entities.Table), args.Error(1)
|
|
}
|
|
|
|
func (m *MockTableRepository) GetByToken(ctx context.Context, token string) (*entities.Table, error) {
|
|
args := m.Called(ctx, token)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*entities.Table), args.Error(1)
|
|
}
|
|
|
|
func (m *MockTableRepository) GetByOutletID(ctx context.Context, outletID uuid.UUID) ([]entities.Table, error) {
|
|
args := m.Called(ctx, outletID)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).([]entities.Table), args.Error(1)
|
|
}
|
|
|
|
func (m *MockTableRepository) GetByOrganizationID(ctx context.Context, organizationID uuid.UUID) ([]entities.Table, error) {
|
|
args := m.Called(ctx, organizationID)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).([]entities.Table), args.Error(1)
|
|
}
|
|
|
|
func (m *MockTableRepository) Update(ctx context.Context, table *entities.Table) error {
|
|
args := m.Called(ctx, table)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockTableRepository) Delete(ctx context.Context, id uuid.UUID) error {
|
|
args := m.Called(ctx, id)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockTableRepository) List(ctx context.Context, organizationID, outletID *uuid.UUID, status *string, isActive *bool, search string, page, limit int) ([]entities.Table, int64, error) {
|
|
args := m.Called(ctx, organizationID, outletID, status, isActive, search, page, limit)
|
|
if args.Get(0) == nil {
|
|
return nil, 0, args.Error(2)
|
|
}
|
|
return args.Get(0).([]entities.Table), args.Get(1).(int64), args.Error(2)
|
|
}
|
|
|
|
func (m *MockTableRepository) GetAvailableTables(ctx context.Context, outletID uuid.UUID) ([]entities.Table, error) {
|
|
args := m.Called(ctx, outletID)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).([]entities.Table), args.Error(1)
|
|
}
|
|
|
|
func (m *MockTableRepository) GetOccupiedTables(ctx context.Context, outletID uuid.UUID) ([]entities.Table, error) {
|
|
args := m.Called(ctx, outletID)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).([]entities.Table), args.Error(1)
|
|
}
|
|
|
|
func (m *MockTableRepository) OccupyTable(ctx context.Context, tableID, orderID uuid.UUID, startTime *time.Time) error {
|
|
args := m.Called(ctx, tableID, orderID, startTime)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockTableRepository) ReleaseTable(ctx context.Context, tableID uuid.UUID, paymentAmount float64) error {
|
|
args := m.Called(ctx, tableID, paymentAmount)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func (m *MockTableRepository) GetByOrderID(ctx context.Context, orderID uuid.UUID) (*entities.Table, error) {
|
|
args := m.Called(ctx, orderID)
|
|
if args.Get(0) == nil {
|
|
return nil, args.Error(1)
|
|
}
|
|
return args.Get(0).(*entities.Table), args.Error(1)
|
|
}
|
|
|
|
func (m *MockTableRepository) UpdateToken(ctx context.Context, tableID uuid.UUID, token string) error {
|
|
args := m.Called(ctx, tableID, token)
|
|
return args.Error(0)
|
|
}
|
|
|
|
func TestCreateOrderWithTableOccupation(t *testing.T) {
|
|
// Setup
|
|
ctx := context.Background()
|
|
organizationID := uuid.New()
|
|
outletID := uuid.New()
|
|
userID := uuid.New()
|
|
tableID := uuid.New()
|
|
orderID := uuid.New()
|
|
|
|
// Create mock table
|
|
mockTable := &entities.Table{
|
|
ID: tableID,
|
|
OrganizationID: organizationID,
|
|
OutletID: outletID,
|
|
TableName: "Table 1",
|
|
Status: "available",
|
|
IsActive: true,
|
|
}
|
|
|
|
// Create mock order response
|
|
mockOrderResponse := &models.OrderResponse{
|
|
ID: orderID,
|
|
// Add other required fields
|
|
}
|
|
|
|
// Create mock repositories
|
|
mockTableRepo := &MockTableRepository{}
|
|
mockOrderProcessor := &MockOrderProcessor{}
|
|
|
|
// Set up expectations
|
|
mockTableRepo.On("GetByID", ctx, tableID).Return(mockTable, nil)
|
|
mockOrderProcessor.On("CreateOrder", ctx, mock.AnythingOfType("*models.CreateOrderRequest"), organizationID).Return(mockOrderResponse, nil)
|
|
mockTableRepo.On("OccupyTable", ctx, tableID, orderID, mock.AnythingOfType("*time.Time")).Return(nil)
|
|
|
|
// Create service with mock dependencies
|
|
service := &OrderServiceImpl{
|
|
orderProcessor: mockOrderProcessor,
|
|
tableRepo: mockTableRepo,
|
|
}
|
|
|
|
// Create test request
|
|
req := &models.CreateOrderRequest{
|
|
OutletID: outletID,
|
|
UserID: userID,
|
|
TableID: &tableID,
|
|
OrderType: "dine_in",
|
|
OrderItems: []models.CreateOrderItemRequest{
|
|
{
|
|
ProductID: uuid.New(),
|
|
Quantity: 1,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test order creation with table occupation
|
|
response, err := service.CreateOrder(ctx, req, organizationID)
|
|
|
|
// Assertions
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, response)
|
|
assert.Equal(t, orderID, response.ID)
|
|
|
|
// Verify mock calls
|
|
mockTableRepo.AssertExpectations(t)
|
|
mockOrderProcessor.AssertExpectations(t)
|
|
}
|
|
|
|
func TestCreateOrderTableValidationFailure(t *testing.T) {
|
|
// Setup
|
|
ctx := context.Background()
|
|
organizationID := uuid.New()
|
|
outletID := uuid.New()
|
|
userID := uuid.New()
|
|
tableID := uuid.New()
|
|
|
|
// Create mock table that's already occupied
|
|
mockTable := &entities.Table{
|
|
ID: tableID,
|
|
OrganizationID: organizationID,
|
|
OutletID: outletID,
|
|
TableName: "Table 1",
|
|
Status: "occupied", // Already occupied
|
|
IsActive: true,
|
|
}
|
|
|
|
// Create mock repositories
|
|
mockTableRepo := &MockTableRepository{}
|
|
mockOrderProcessor := &MockOrderProcessor{}
|
|
|
|
// Set up expectations
|
|
mockTableRepo.On("GetByID", ctx, tableID).Return(mockTable, nil)
|
|
|
|
// Create service with mock dependencies
|
|
service := &OrderServiceImpl{
|
|
orderProcessor: mockOrderProcessor,
|
|
tableRepo: mockTableRepo,
|
|
}
|
|
|
|
// Create test request
|
|
req := &models.CreateOrderRequest{
|
|
OutletID: outletID,
|
|
UserID: userID,
|
|
TableID: &tableID,
|
|
OrderType: "dine_in",
|
|
OrderItems: []models.CreateOrderItemRequest{
|
|
{
|
|
ProductID: uuid.New(),
|
|
Quantity: 1,
|
|
},
|
|
},
|
|
}
|
|
|
|
// Test order creation with occupied table
|
|
response, err := service.CreateOrder(ctx, req, organizationID)
|
|
|
|
// Assertions
|
|
assert.Error(t, err)
|
|
assert.Nil(t, response)
|
|
assert.Contains(t, err.Error(), "table is not available for occupation")
|
|
|
|
// Verify mock calls
|
|
mockTableRepo.AssertExpectations(t)
|
|
mockOrderProcessor.AssertNotCalled(t, "CreateOrder")
|
|
}
|