2026-09-30 10:40:05 +07:00
|
|
|
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 {
|
2026-09-30 10:50:37 +07:00
|
|
|
calls []uuid.UUID
|
|
|
|
|
ctxs []context.Context
|
|
|
|
|
refunded []uuid.UUID
|
|
|
|
|
earned map[uuid.UUID]OrderEarned
|
2026-09-30 10:40:05 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *orderPaidHookFake) OnOrderPaid(ctx context.Context, orderID uuid.UUID) {
|
|
|
|
|
h.calls = append(h.calls, orderID)
|
|
|
|
|
h.ctxs = append(h.ctxs, ctx)
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 10:50:37 +07:00
|
|
|
func (h *orderPaidHookFake) OnOrderRefunded(_ context.Context, orderID uuid.UUID) {
|
|
|
|
|
h.refunded = append(h.refunded, orderID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *orderPaidHookFake) EarnedByOrders(context.Context, []uuid.UUID) (map[uuid.UUID]OrderEarned, error) {
|
|
|
|
|
return h.earned, nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-30 10:40:05 +07:00
|
|
|
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),
|
|
|
|
|
}
|
2026-09-30 10:50:37 +07:00
|
|
|
p.SetLoyalty(hook)
|
2026-09-30 10:40:05 +07:00
|
|
|
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)
|
2026-09-30 10:50:37 +07:00
|
|
|
p.SetLoyalty(nil)
|
2026-09-30 10:40:05 +07:00
|
|
|
_, err := p.UpdateOrder(context.Background(), orders.order.ID, &models.UpdateOrderRequest{})
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
}
|
2026-09-30 10:45:33 +07:00
|
|
|
|
|
|
|
|
func (r *hookOrderRepo) VoidOrderWithStatus(_ context.Context, _ uuid.UUID, status entities.OrderStatus, _ string, _ uuid.UUID) error {
|
|
|
|
|
r.order.Status, r.order.IsVoid = status, true
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *hookOrderRepo) Update(_ context.Context, o *entities.Order) error {
|
|
|
|
|
r.order.RefundAmount = o.RefundAmount
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *hookOrderRepo) RefundOrder(context.Context, uuid.UUID, string, uuid.UUID) error {
|
|
|
|
|
r.order.IsRefund = true
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *hookPaymentRepo) RefundPayment(_ context.Context, id uuid.UUID, amount float64, _ string, _ uuid.UUID) error {
|
|
|
|
|
for _, p := range r.created {
|
|
|
|
|
if p.ID == id {
|
|
|
|
|
p.RefundAmount = amount
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOrderRefundedHook(t *testing.T) {
|
|
|
|
|
t.Run("void", func(t *testing.T) {
|
2026-09-30 10:50:37 +07:00
|
|
|
p, orders, hook := newHookedOrderProcessor(nil)
|
2026-09-30 10:45:33 +07:00
|
|
|
require.NoError(t, p.VoidOrder(context.Background(), &models.VoidOrderRequest{OrderID: orders.order.ID, Type: "ALL", Reason: "salah input"}, uuid.New()))
|
2026-09-30 10:50:37 +07:00
|
|
|
assert.Equal(t, []uuid.UUID{orders.order.ID}, hook.refunded)
|
2026-09-30 10:45:33 +07:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("refund order", func(t *testing.T) {
|
2026-09-30 10:50:37 +07:00
|
|
|
p, orders, hook := newHookedOrderProcessor(nil)
|
2026-09-30 10:45:33 +07:00
|
|
|
orders.order.PaymentStatus = entities.PaymentStatusCompleted
|
|
|
|
|
amount := 25000.0
|
|
|
|
|
require.NoError(t, p.RefundOrder(context.Background(), orders.order.ID, &models.RefundOrderRequest{RefundAmount: &amount}, uuid.New()))
|
2026-09-30 10:50:37 +07:00
|
|
|
assert.Equal(t, []uuid.UUID{orders.order.ID}, hook.refunded)
|
2026-09-30 10:45:33 +07:00
|
|
|
})
|
|
|
|
|
|
|
|
|
|
t.Run("refund payment", func(t *testing.T) {
|
2026-09-30 10:50:37 +07:00
|
|
|
p, orders, hook := newHookedOrderProcessor(nil)
|
2026-09-30 10:45:33 +07:00
|
|
|
payments := p.paymentRepo.(*hookPaymentRepo)
|
|
|
|
|
payment := &entities.Payment{ID: uuid.New(), OrderID: orders.order.ID, Amount: 50000, Status: entities.PaymentTransactionStatusCompleted}
|
|
|
|
|
payments.created = append(payments.created, payment)
|
|
|
|
|
require.NoError(t, p.RefundPayment(context.Background(), payment.ID, 20000, "kembali", uuid.New()))
|
2026-09-30 10:50:37 +07:00
|
|
|
assert.Equal(t, []uuid.UUID{orders.order.ID}, hook.refunded)
|
2026-09-30 10:45:33 +07:00
|
|
|
|
|
|
|
|
// A refund that fails does not run the hook.
|
|
|
|
|
assert.Error(t, p.RefundPayment(context.Background(), payment.ID, 60000, "terlalu besar", uuid.New()))
|
2026-09-30 10:50:37 +07:00
|
|
|
assert.Len(t, hook.refunded, 1)
|
2026-09-30 10:45:33 +07:00
|
|
|
})
|
|
|
|
|
}
|
2026-09-30 10:50:37 +07:00
|
|
|
|
|
|
|
|
func (r *hookOrderRepo) List(context.Context, map[string]interface{}, int, int) ([]*entities.Order, int64, error) {
|
|
|
|
|
o := *r.order
|
|
|
|
|
other := entities.Order{ID: uuid.New()}
|
|
|
|
|
return []*entities.Order{&o, &other}, 2, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestOrderResponsesCarryEarnings(t *testing.T) {
|
|
|
|
|
p, orders, hook := newHookedOrderProcessor(nil)
|
|
|
|
|
hook.earned = map[uuid.UUID]OrderEarned{orders.order.ID: {Points: 875, Coins: 3}}
|
|
|
|
|
|
|
|
|
|
got, err := p.GetOrderByID(context.Background(), orders.order.ID)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, int64(875), got.PointsEarned)
|
|
|
|
|
assert.Equal(t, int64(3), got.CoinsEarned)
|
|
|
|
|
|
|
|
|
|
list, err := p.ListOrders(context.Background(), &models.ListOrdersRequest{Page: 1, Limit: 10})
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
require.Len(t, list.Orders, 2)
|
|
|
|
|
assert.Equal(t, int64(875), list.Orders[0].PointsEarned)
|
|
|
|
|
assert.Equal(t, int64(0), list.Orders[1].PointsEarned, "an order that earned nothing shows 0")
|
|
|
|
|
|
|
|
|
|
// Without the loyalty program the fields stay at 0.
|
|
|
|
|
p.SetLoyalty(nil)
|
|
|
|
|
got, err = p.GetOrderByID(context.Background(), orders.order.ID)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
assert.Equal(t, int64(0), got.PointsEarned)
|
|
|
|
|
}
|