feat(customer): order history and detail for the customer app
Adds GET /customer/orders and GET /customer/orders/:id for the customer app: the orders linked to the logged-in customer across their organization's outlets, newest first, paginated (limit 1-100, default 20). The list shows the order number, outlet, type, status, total, item count, void/refund flags and the EnakPoint and EnakCoin it earned. The detail adds the amounts, the items with product and variant names, weight and unit for weighed lines, modifiers and notes, and the payments with the method and, for EnakPoint, the points used. Costs, cashier and other internal fields are left out. Another customer's order answers 404 like one that does not exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
c1ec6a3dfd
commit
a24d5f0561
@@ -0,0 +1,145 @@
|
||||
package processor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"apskel-pos-be/internal/models"
|
||||
"apskel-pos-be/internal/repository"
|
||||
)
|
||||
|
||||
type customerOrderRepoFake struct {
|
||||
owner map[uuid.UUID]uuid.UUID // order -> customer
|
||||
orders []repository.CustomerOrderRow
|
||||
items map[uuid.UUID][]repository.CustomerOrderItemRow
|
||||
payments map[uuid.UUID][]repository.CustomerOrderPaymentRow
|
||||
offset int
|
||||
limit int
|
||||
}
|
||||
|
||||
func (f *customerOrderRepoFake) ListOrders(_ context.Context, customerID uuid.UUID, offset, limit int) ([]repository.CustomerOrderRow, int64, error) {
|
||||
f.offset, f.limit = offset, limit
|
||||
var mine []repository.CustomerOrderRow
|
||||
for _, o := range f.orders {
|
||||
if f.owner[o.ID] == customerID {
|
||||
mine = append(mine, o)
|
||||
}
|
||||
}
|
||||
return mine, int64(len(mine)), nil
|
||||
}
|
||||
|
||||
func (f *customerOrderRepoFake) GetOrder(_ context.Context, customerID, orderID uuid.UUID) (*repository.CustomerOrderRow, error) {
|
||||
for _, o := range f.orders {
|
||||
if o.ID == orderID && f.owner[o.ID] == customerID {
|
||||
c := o
|
||||
return &c, nil
|
||||
}
|
||||
}
|
||||
return nil, repository.ErrCustomerOrderNotFound
|
||||
}
|
||||
|
||||
func (f *customerOrderRepoFake) ListItems(_ context.Context, orderID uuid.UUID) ([]repository.CustomerOrderItemRow, error) {
|
||||
return f.items[orderID], nil
|
||||
}
|
||||
|
||||
func (f *customerOrderRepoFake) ListPayments(_ context.Context, orderID uuid.UUID) ([]repository.CustomerOrderPaymentRow, error) {
|
||||
return f.payments[orderID], nil
|
||||
}
|
||||
|
||||
type earnedFake map[uuid.UUID]OrderEarned
|
||||
|
||||
func (f earnedFake) EarnedByOrders(_ context.Context, ids []uuid.UUID) (map[uuid.UUID]OrderEarned, error) {
|
||||
out := map[uuid.UUID]OrderEarned{}
|
||||
for _, id := range ids {
|
||||
if e, ok := f[id]; ok {
|
||||
out[id] = e
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func newCustomerOrderTest() (*customerOrderRepoFake, uuid.UUID, uuid.UUID, uuid.UUID) {
|
||||
customer, other := uuid.New(), uuid.New()
|
||||
mine, theirs := uuid.New(), uuid.New()
|
||||
repo := &customerOrderRepoFake{
|
||||
owner: map[uuid.UUID]uuid.UUID{mine: customer, theirs: other},
|
||||
orders: []repository.CustomerOrderRow{
|
||||
{ID: mine, OrderNumber: "ORD-1", OutletName: "Gokuna 1", Status: "completed", PaymentStatus: "completed", Subtotal: 90000, TotalAmount: 99000, ItemCount: 2, CreatedAt: time.Now()},
|
||||
{ID: theirs, OrderNumber: "ORD-2"},
|
||||
},
|
||||
}
|
||||
return repo, customer, mine, theirs
|
||||
}
|
||||
|
||||
func TestCustomerOrders_ListShowsOnlyTheCustomersOrdersWithEarning(t *testing.T) {
|
||||
repo, customer, mine, _ := newCustomerOrderTest()
|
||||
p := NewCustomerOrderProcessor(repo, earnedFake{mine: {Points: 900, Coins: 3}})
|
||||
|
||||
got, err := p.List(context.Background(), customer, models.ListCustomerOrdersQuery{Page: 2, Limit: 10})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, got.Data, 1)
|
||||
assert.Equal(t, "ORD-1", got.Data[0].OrderNumber)
|
||||
assert.Equal(t, int64(900), got.Data[0].PointsEarned)
|
||||
assert.Equal(t, int64(3), got.Data[0].CoinsEarned)
|
||||
assert.Equal(t, 10, repo.offset, "page 2 of 10 skips the first 10")
|
||||
assert.Equal(t, 10, repo.limit)
|
||||
assert.Equal(t, 2, got.Pagination.Page)
|
||||
}
|
||||
|
||||
func TestCustomerOrders_ListDefaultsAndLimits(t *testing.T) {
|
||||
repo, customer, _, _ := newCustomerOrderTest()
|
||||
p := NewCustomerOrderProcessor(repo, earnedFake{})
|
||||
|
||||
got, err := p.List(context.Background(), customer, models.ListCustomerOrdersQuery{})
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 1, got.Pagination.Page)
|
||||
assert.Equal(t, 20, got.Pagination.Limit)
|
||||
|
||||
for _, q := range []models.ListCustomerOrdersQuery{{Page: -1}, {Limit: 101}, {Limit: -5}} {
|
||||
_, err := p.List(context.Background(), customer, q)
|
||||
assert.ErrorIs(t, err, ErrInvalidWalletQuery, "%+v", q)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCustomerOrders_DetailHasItemsPaymentsAndEarning(t *testing.T) {
|
||||
repo, customer, mine, _ := newCustomerOrderTest()
|
||||
variant, unit := "Large", "ons"
|
||||
weight := 4.2
|
||||
points, value := int64(12500), 1.0
|
||||
repo.items = map[uuid.UUID][]repository.CustomerOrderItemRow{
|
||||
mine: {
|
||||
{ProductName: "Kopi Susu", VariantName: &variant, Quantity: 2, UnitPrice: 25000, TotalPrice: 50000, Status: "completed"},
|
||||
{ProductName: "Ikan Tude", Quantity: 1, Weight: &weight, UnitName: &unit, UnitPrice: 4500, TotalPrice: 18900, Status: "completed"},
|
||||
},
|
||||
}
|
||||
repo.payments = map[uuid.UUID][]repository.CustomerOrderPaymentRow{
|
||||
mine: {
|
||||
{MethodName: "EnakPoint", MethodType: "point", Amount: 12500, Status: "completed", PointsUsed: &points, PointValue: &value},
|
||||
{MethodName: "Cash", MethodType: "cash", Amount: 86500, Status: "completed"},
|
||||
},
|
||||
}
|
||||
p := NewCustomerOrderProcessor(repo, earnedFake{mine: {Points: 865}})
|
||||
|
||||
got, err := p.Detail(context.Background(), customer, mine)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "Gokuna 1", got.OutletName)
|
||||
assert.Equal(t, float64(90000), got.Subtotal)
|
||||
assert.Equal(t, int64(865), got.PointsEarned)
|
||||
require.Len(t, got.Items, 2)
|
||||
assert.Equal(t, "Large", *got.Items[0].VariantName)
|
||||
assert.Equal(t, []map[string]interface{}{}, got.Items[0].Modifiers, "no modifiers is an empty list, not null")
|
||||
assert.Equal(t, 4.2, *got.Items[1].Weight)
|
||||
require.Len(t, got.Payments, 2)
|
||||
assert.Equal(t, int64(12500), *got.Payments[0].PointsUsed)
|
||||
}
|
||||
|
||||
func TestCustomerOrders_AnotherCustomersOrderIsNotFound(t *testing.T) {
|
||||
repo, customer, _, theirs := newCustomerOrderTest()
|
||||
_, err := NewCustomerOrderProcessor(repo, earnedFake{}).Detail(context.Background(), customer, theirs)
|
||||
assert.ErrorIs(t, err, repository.ErrCustomerOrderNotFound)
|
||||
}
|
||||
Reference in New Issue
Block a user