feat(loyalty): show what an order earned on the order and receipt

Adds points_earned and coins_earned to the order response
(docs/prd-point-coin.md F3, PC-205). The POS prints the receipt from this
response, so the receipt gets them too.

The values are the sums of the order's EARN rows, read in one query for a
list of orders. They are filled for create, add items, update, detail and
list, and are 0 for an order that earned nothing. UpdateOrder earns before
building its response, so a payment completed there already shows the
earning. A failure to read them is logged and leaves them at 0 rather than
failing the order read. The self-order session listing reads orders
directly from the repository and still shows 0.

The two order hooks are merged into one OrderLoyalty interface (paid,
refunded, earned by orders) with a single SetLoyalty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
efrilm
2026-09-30 10:50:37 +07:00
co-authored by Claude Opus 5.5
parent eb5b63677f
commit fc97c78300
10 changed files with 194 additions and 53 deletions
@@ -214,6 +214,14 @@ func TestEarningProcessor_AgainstPostgres(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, map[string]int64{"POINT": 400, "COIN": 1}, earnRows(missed))
// What the order screen and receipt show is what the ledger has.
shown, err := earning.EarnedByOrders(ctx, []uuid.UUID{paid, split, missed, uuid.New()})
require.NoError(t, err)
assert.Equal(t, OrderEarned{Points: 875, Coins: 3}, shown[paid])
assert.Equal(t, OrderEarned{Points: 600, Coins: 2}, shown[split])
assert.Equal(t, OrderEarned{Points: 400, Coins: 1}, shown[missed])
assert.Len(t, shown, 3, "an order without earning is absent, so it shows 0")
balance := struct{ PointBalance, CoinBalance int64 }{}
require.NoError(t, db.Raw(`SELECT point_balance, coin_balance FROM customer_wallets WHERE customer_id = ?`, regular).Scan(&balance).Error)
assert.Equal(t, int64(875+250+600+400), balance.PointBalance)