taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 6605b528aad05bc7e58ec4d2dca47c8300522e5a
parent 4937b40a71699a21006eeb08be36e177ec7a4a71
Author: Florian Dold <dold@taler.net>
Date:   Tue, 25 Aug 2026 23:10:08 +0200

merchant web UI: distinguish paid and settled order rows

Diffstat:
Mpackages/taler-merchant-webui/src/api/hooks.test.ts | 24++++++++++++++++++------
Mpackages/taler-merchant-webui/src/api/hooks/useOrders.ts | 3++-
Mpackages/taler-merchant-webui/src/tutorial/tutorialData.tsx | 19+++++++++++++------
Mpackages/taler-util/src/types-taler-merchant.test.ts | 25+++++++++++++++++++++++++
Mpackages/taler-util/src/types-taler-merchant.ts | 5+++++
5 files changed, 63 insertions(+), 13 deletions(-)

diff --git a/packages/taler-merchant-webui/src/api/hooks.test.ts b/packages/taler-merchant-webui/src/api/hooks.test.ts @@ -50,8 +50,8 @@ test("isNonZeroAmount identifies zero and non-zero string and object amounts", ( // A list entry and a status response are different shapes, and `mapOrderStatus` // runs over both. These fixtures are the real ones: every required field is // present, and none carries a field its endpoint does not send. Older fixtures -// were list entries wearing `wired`, `order_status` and a `pay_deadline` of -// `{t_ms}`, so they tested a response no backend produces. +// were list entries wearing `order_status` and a `pay_deadline` of `{t_ms}`, +// so they tested a response no backend produces. function listEntry( over: Partial<TalerMerchantApi.OrderHistoryEntry>, ): TalerMerchantApi.OrderHistoryEntry { @@ -90,10 +90,22 @@ test("a list entry with a refund of any kind reads as refunded", () => { ); }); -test("a list entry cannot say the money was wired, so the chosen filter does", () => { - // `OrderHistoryEntry` has no `wired` field. Asked for settled orders, the - // backend answered with settled orders, and that is the only evidence there - // is; the row would otherwise read "paid" forever. +test("a list entry uses its wired value before the chosen filter", () => { + assert.strictEqual( + mapOrderStatus(listEntry({ paid: true, wired: true }), "all"), + "settled", + ); + assert.strictEqual( + mapOrderStatus(listEntry({ paid: true, wired: true }), "paid"), + "settled", + ); + assert.strictEqual( + mapOrderStatus(listEntry({ paid: true, wired: false }), "settled"), + "paid", + ); + + // A backend older than protocol v38 omits `wired`. In that case the + // selected settled filter remains the only available evidence. assert.strictEqual(mapOrderStatus(listEntry({ paid: true }), "settled"), "settled"); assert.strictEqual(mapOrderStatus(listEntry({ paid: true }), "all"), "paid"); }); diff --git a/packages/taler-merchant-webui/src/api/hooks/useOrders.ts b/packages/taler-merchant-webui/src/api/hooks/useOrders.ts @@ -114,7 +114,8 @@ export function mapOrderStatus( if (isRefunded) return "refunded"; - const isSettled = queryStatus === "settled" || ("wired" in o && o.wired === true); + const hasWiredStatus = "wired" in o && o.wired !== undefined; + const isSettled = hasWiredStatus ? o.wired === true : queryStatus === "settled"; if (isSettled) return "settled"; diff --git a/packages/taler-merchant-webui/src/tutorial/tutorialData.tsx b/packages/taler-merchant-webui/src/tutorial/tutorialData.tsx @@ -1237,7 +1237,7 @@ function buildTutorialChapters(t: TranslateFn): TutorialChapter[] { menuEntry: "Orders", keyTakeaways: [ t`The list updates itself — you do not need to reload it to see a payment land.`, - t`The tabs sort orders by where they have got to: Offered, Paid, Refunded, Settled.`, + t`The tabs are filters: they group orders by payment, refund, payout, and expiration facts.`, t`You can refund an order in full or in part, as long as its refund window is still open.`, t`A refund the customer never collects does lapse. The order says so plainly when it does.`, ], @@ -1245,18 +1245,23 @@ function buildTutorialChapters(t: TranslateFn): TutorialChapter[] { { title: t`The Order List`, blocks: [ - { kind: "p", text: t`Each row reads left to right as when, what, how much, and where it has got to. The tabs across the top narrow the list down:` }, + { kind: "p", text: t`Each row reads left to right as when, what, how much, and where it has got to. The tabs across the top apply these filters:` }, { kind: "list", items: [ - t`**Offered** — you have asked for the money; nobody has paid yet.`, + t`**All** — every order, without a status filter.`, - t`**Paid** — the customer has paid. The money is on its way to you but has not arrived.`, + t`**Offered** — unpaid orders whose payment deadline has not passed.`, - t`**Settled** — your payment service has sent the money on to your bank. Whether it has landed is a separate question, and the Bank accounts screen is where you answer it.`, + t`**Paid** — paid orders that have no refund and whose payout has not yet been reported by the exchange.`, - t`**Refunded** — you have given some or all of it back.`, + t`**Refunded** — orders with a partial or full refund, whether or not their payout was already sent.`, + + t`**Settled** — orders with no refund whose exchange reports that it sent the payout to your bank. This does not confirm that your bank received it.`, + + t`**Expired** — unpaid orders whose payment deadline has passed.`, ] }, + { kind: "p", text: t`These are overlapping database filters rather than a single sequence of names. For example, the Refunded tab includes refunded orders on either side of payout, while the Paid and Settled tabs deliberately exclude them.` }, { kind: "p", text: t`Use the **Data** menu in the window bar to see the list before your first sale.` }, ], dataSets: [ @@ -1301,6 +1306,8 @@ function buildTutorialChapters(t: TranslateFn): TutorialChapter[] { previewMaxHeightClass: "max-h-none", blocks: [ { kind: "p", text: t`Opening an order shows its current state and total first. The essential dates follow in a short list; open **Order history** when you need the full sequence of what happened and when: created, paid, refunded, paid out.` }, + { kind: "p", text: t`The details page can be more specific than the list filters. It may say **Awaiting payment**, **Wallet completing payment**, **Expired unpaid**, **Paid, awaiting payout**, **Settled**, **Refund awaiting collection**, **Partially refunded**, **Fully refunded**, **Refund lapsed**, or simply **Refunded**.` }, + { kind: "p", text: t`In the backend protocol, **order_status** remains **paid** after an accepted payment even when its payout has been sent. The separate **wired** value is what changes the portal's display from **Paid, awaiting payout** to **Settled**. Settled means the exchange reports that it sent the proceeds; confirming arrival at your bank is a separate step.` }, { kind: "p", text: t`The **refund window** is worth knowing about. It is how long you can still refund the order, and once it closes you cannot — you would have to return the money another way.` }, ], dataSets: [ diff --git a/packages/taler-util/src/types-taler-merchant.test.ts b/packages/taler-util/src/types-taler-merchant.test.ts @@ -20,9 +20,34 @@ import { codecForLoginTokenSuccessResponse, codecForPaymentDeniedLegallyResponse, codecForQueryInstancesResponse, + codecForOrderHistoryEntry, codecForTokenFamilyDetails, } from "./types-taler-merchant.js"; +function orderHistoryEntry(): Record<string, unknown> { + return { + order_id: "2026.237-TEST", + row_id: 42, + timestamp: { t_s: 1_770_000_000 }, + pay_deadline: { t_s: 1_770_003_600 }, + amount: "KUDOS:10", + summary: "An order", + refundable: false, + paid: true, + }; +} + +test("order history entries decode v38 wired status and legacy omission", () => { + const legacy = codecForOrderHistoryEntry().decode(orderHistoryEntry()); + assert.strictEqual(legacy.wired, undefined); + + const current = codecForOrderHistoryEntry().decode({ + ...orderHistoryEntry(), + wired: true, + }); + assert.strictEqual(current.wired, true); +}); + test("legal payment refusal retains its protocol error code", () => { assert.deepStrictEqual( codecForPaymentDeniedLegallyResponse().decode({ diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts @@ -2972,6 +2972,10 @@ export interface OrderHistoryEntry { // Whether the order has been paid or not. paid: boolean; + + // Whether the exchange reports that it wired the order's funds. + // Optional for compatibility with backends older than protocol v38. + wired?: boolean; } export type MerchantOrderStatusResponse = @@ -5077,6 +5081,7 @@ export const codecForOrderHistoryEntry = (): Codec<OrderHistoryEntry> => .property("summary", codecForString()) .property("refundable", codecForBoolean()) .property("paid", codecForBoolean()) + .property("wired", codecOptional(codecForBoolean())) .build("TalerMerchantApi.OrderHistoryEntry"); export const codecForMerchant = (): Codec<Merchant> =>