Files
apskel-pos-backend/internal/processor/order_paid_hook_test.go
T

182 lines
5.8 KiB
Go
Raw Normal View History

package processor
import (
"context"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"apskel-pos-be/internal/entities"
"apskel-pos-be/internal/models"
"apskel-pos-be/internal/repository"
)
// These fakes embed the interface they stand for and implement only what the paths
// under test call; anything else would panic, which would show a path doing more than
// expected.
type hookOrderRepo struct {
OrderRepository
order *entities.Order
statusUpdates int
hookCallsAtPay int // hook calls seen when the status was written
hook *orderPaidHookFake
}
func (r *hookOrderRepo) GetByID(context.Context, uuid.UUID) (*entities.Order, error) {
o := *r.order
return &o, nil
}
func (r *hookOrderRepo) GetWithRelations(context.Context, uuid.UUID) (*entities.Order, error) {
o := *r.order
return &o, nil
}
func (r *hookOrderRepo) UpdateStatusSuccess(_ context.Context, _ uuid.UUID, status entities.OrderStatus, payment entities.PaymentStatus) error {
r.statusUpdates++
r.hookCallsAtPay = len(r.hook.calls)
r.order.Status, r.order.PaymentStatus = status, payment
return nil
}
type hookPaymentRepo struct {
PaymentRepository
created []*entities.Payment
}
func (r *hookPaymentRepo) GetTotalPaidByOrderID(context.Context, uuid.UUID) (float64, error) {
return 0, nil
}
func (r *hookPaymentRepo) Create(_ context.Context, p *entities.Payment) error {
p.ID = uuid.New()
r.created = append(r.created, p)
return nil
}
func (r *hookPaymentRepo) GetByID(_ context.Context, id uuid.UUID) (*entities.Payment, error) {
for _, p := range r.created {
if p.ID == id {
return p, nil
}
}
return nil, nil
}
func (r *hookPaymentRepo) GetByOrderID(context.Context, uuid.UUID) ([]*entities.Payment, error) {
return r.created, nil
}
type hookPaymentMethodRepo struct{}
func (hookPaymentMethodRepo) GetByID(_ context.Context, id uuid.UUID) (*entities.PaymentMethod, error) {
return &entities.PaymentMethod{ID: id}, nil
}
type hookOrderItemRepo struct{ OrderItemRepository }
func (hookOrderItemRepo) GetByOrderID(context.Context, uuid.UUID) ([]*entities.OrderItem, error) {
return nil, nil
}
// splitFake settles the order on the payment that covers what is left, as the real
// split bill processor does.
type splitFake struct{ settle bool }
func (f splitFake) split(order *entities.Order) (*models.SplitBillResponse, error) {
if f.settle {
order.PaymentStatus = entities.PaymentStatusCompleted
} else {
order.PaymentStatus = entities.PaymentStatusPartial
}
return &models.SplitBillResponse{OrderID: order.ID}, nil
}
func (f splitFake) SplitByAmount(_ context.Context, _ *models.SplitBillRequest, order *entities.Order, _ *entities.PaymentMethod, _ *entities.Customer) (*models.SplitBillResponse, error) {
return f.split(order)
}
func (f splitFake) SplitByItem(_ context.Context, _ *models.SplitBillRequest, order *entities.Order, _ *entities.PaymentMethod, _ *entities.Customer) (*models.SplitBillResponse, error) {
return f.split(order)
}
type orderPaidHookFake struct {
calls []uuid.UUID
ctxs []context.Context
}
func (h *orderPaidHookFake) OnOrderPaid(ctx context.Context, orderID uuid.UUID) {
h.calls = append(h.calls, orderID)
h.ctxs = append(h.ctxs, ctx)
}
func newHookedOrderProcessor(split SplitBillProcessor) (*OrderProcessorImpl, *hookOrderRepo, *orderPaidHookFake) {
hook := &orderPaidHookFake{}
orders := &hookOrderRepo{
order: &entities.Order{ID: uuid.New(), OrganizationID: uuid.New(), OutletID: uuid.New(), TotalAmount: 100000, PaymentStatus: entities.PaymentStatusPending},
hook: hook,
}
p := &OrderProcessorImpl{
orderRepo: orders,
orderItemRepo: hookOrderItemRepo{},
paymentRepo: &hookPaymentRepo{},
paymentMethodRepo: hookPaymentMethodRepo{},
splitBillProcessor: split,
txManager: repository.NewTxManager(nil),
}
p.SetOrderPaidHook(hook)
return p, orders, hook
}
func TestOrderPaidHook_CreatePayment(t *testing.T) {
p, orders, hook := newHookedOrderProcessor(nil)
ctx, cancel := context.WithCancel(context.Background())
_, err := p.CreatePayment(ctx, &models.CreatePaymentRequest{OrderID: orders.order.ID, PaymentMethodID: uuid.New(), Amount: 100000})
require.NoError(t, err)
assert.Equal(t, []uuid.UUID{orders.order.ID}, hook.calls)
assert.Zero(t, orders.hookCallsAtPay, "the hook runs after the payment, not inside its transaction")
// The hook's context outlives the request.
cancel()
assert.NoError(t, hook.ctxs[0].Err())
}
func TestOrderPaidHook_UpdateOrder(t *testing.T) {
p, orders, hook := newHookedOrderProcessor(nil)
_, err := p.UpdateOrder(context.Background(), orders.order.ID, &models.UpdateOrderRequest{})
require.NoError(t, err)
assert.Equal(t, []uuid.UUID{orders.order.ID}, hook.calls)
assert.Equal(t, 1, orders.statusUpdates)
assert.Zero(t, orders.hookCallsAtPay)
}
func TestOrderPaidHook_SplitBillOnlyOnTheSettlingPayment(t *testing.T) {
for _, splitType := range []string{"AMOUNT", "ITEM"} {
t.Run(splitType, func(t *testing.T) {
req := &models.SplitBillRequest{Type: splitType, PaymentMethodID: uuid.New()}
p, orders, hook := newHookedOrderProcessor(splitFake{settle: false})
req.OrderID = orders.order.ID
_, err := p.SplitBill(context.Background(), req)
require.NoError(t, err)
assert.Empty(t, hook.calls, "a partial split payment does not make the order paid")
p, orders, hook = newHookedOrderProcessor(splitFake{settle: true})
req.OrderID = orders.order.ID
_, err = p.SplitBill(context.Background(), req)
require.NoError(t, err)
assert.Equal(t, []uuid.UUID{orders.order.ID}, hook.calls)
})
}
}
func TestOrderPaidHook_NoHookIsFine(t *testing.T) {
p, orders, _ := newHookedOrderProcessor(nil)
p.SetOrderPaidHook(nil)
_, err := p.UpdateOrder(context.Background(), orders.order.ID, &models.UpdateOrderRequest{})
assert.NoError(t, err)
}