taler-typescript-core

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

commit 50f768fa601e4a891cecd122687553fe89a72606
parent 7032a2da0a6120a81b010097ef66b6beeef6aa5d
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Sun, 14 Dec 2025 16:58:07 -0300

fix #10695

Diffstat:
Mpackages/merchant-backoffice-ui/src/components/form/InputDuration.tsx | 2+-
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx | 132++++++++++++++++++++++++++++++++++++++++---------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx | 81++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx | 80++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Mpackages/merchant-backoffice-ui/src/utils/amount.ts | 1+
5 files changed, 177 insertions(+), 119 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx b/packages/merchant-backoffice-ui/src/components/form/InputDuration.tsx @@ -176,7 +176,7 @@ export function InputDuration<T>({ <span data-tooltip={i18n.str`Change value to empty`}> <button type="button" - class="button is-info " + class="button is-info mr-3" onClick={() => onChange(undefined as any)} > <i18n.Translate>Clear</i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx @@ -28,6 +28,7 @@ import { OrderVersion, TalerMerchantApi, TalerProtocolDuration, + durationAdd, } from "@gnu-taler/taler-util"; import { ButtonBetterBulma, @@ -61,7 +62,6 @@ import { rate } from "../../../../utils/amount.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { FragmentPersonaFlag } from "../../../../components/menu/SideBar.js"; - const TALER_SCREEN_ID = 42; export interface Props { @@ -89,7 +89,7 @@ function with_defaults( ); const defaultRefundDelay = Duration.fromTalerProtocolDuration( config.default_refund_delay, - );; + ); return { inventoryProducts: {}, @@ -163,7 +163,6 @@ export function CreatePage({ const instance_default = with_defaults(instanceConfig, config.currency); const [value, valueHandler] = useState(instance_default); const zero = Amounts.zeroOfCurrency(config.currency); - const [pref, updatePrefs] = usePreference(); const inventoryList = Object.values(value.inventoryProducts || {}); const productList = Object.values(value.products || {}); @@ -185,21 +184,11 @@ export function CreatePage({ : undefined, }), payments: undefinedIfEmpty({ - // refund_delay: !value.payments?.refund_delay - // ? undefined - // : undefined, - // pay_delay: !value.payments?.pay_delay - // ? i18n.str`Required` - // : value.payments.wire_transfer_delay && - // Duration.cmp( - // value.payments.wire_transfer_delay, - // value.payments.pay_delay, - // ) === -1 - // ? i18n.str`Wire transfer deadline can't be before pay deadline` - // : undefined, - // wire_transfer_delay: !value.payments?.wire_transfer_delay - // ? i18n.str`Required` - // : undefined, + wire_transfer_delay: !value.payments?.wire_transfer_delay + ? i18n.str`Required` + : undefined, + // refund_delay: !value.payments?.refund_delay ? i18n.str`Required` : undefined, + pay_delay: !value.payments?.pay_delay ? i18n.str`Required` : undefined, auto_refund_delay: !value.payments?.auto_refund_delay ? undefined : !value.payments?.refund_delay @@ -232,12 +221,61 @@ export function CreatePage({ : undefined, }), }); - const hasErrors = errors !== undefined; const order = value; const price = order.pricing?.order_price as AmountString | undefined; const summary = order.pricing?.summary; + const payDelay = !value.payments?.pay_delay + ? Duration.getZero() + : value.payments.pay_delay; + + const refundDelay = !value.payments?.refund_delay + ? Duration.getZero() + : Duration.add( + payDelay, + !value.payments?.refund_delay + ? Duration.getZero() + : value.payments.refund_delay, + ); + + const autoRefundDelay = !value.payments?.auto_refund_delay + ? Duration.getZero() + : Duration.add( + payDelay, + !value.payments?.auto_refund_delay + ? Duration.getZero() + : value.payments.auto_refund_delay, + ); + + const wireDelay = !value.payments?.wire_transfer_delay + ? Duration.getZero() + : Duration.add( + refundDelay, + !value.payments?.wire_transfer_delay + ? Duration.getZero() + : value.payments.wire_transfer_delay, + ); + + const pay_deadline = !value.payments?.pay_delay + ? undefined + : AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration(AbsoluteTime.now(), payDelay), + ); + + const refund_deadline = !value.payments?.refund_delay + ? undefined + : AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration(AbsoluteTime.now(), refundDelay), + ); + + const wire_transfer_deadline = + !value.payments || !value.payments.wire_transfer_delay + ? undefined + : AbsoluteTime.toProtocolTimestamp( + AbsoluteTime.addDuration(AbsoluteTime.now(), wireDelay), + ); + const request: undefined | TalerMerchantApi.PostOrderRequest = !value.payments || !value.shipping || !price || !summary ? undefined @@ -253,24 +291,9 @@ export function CreatePage({ summary: summary, products: productList, extra: undefinedIfEmpty(value.extra), - pay_deadline: AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration( - AbsoluteTime.now(), - value.payments.pay_delay!, - ), - ), - wire_transfer_deadline: AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration( - AbsoluteTime.now(), - value.payments.wire_transfer_delay!, - ), - ), - refund_deadline: AbsoluteTime.toProtocolTimestamp( - AbsoluteTime.addDuration( - AbsoluteTime.now(), - value.payments.refund_delay!, - ), - ), + pay_deadline, + refund_deadline, + wire_transfer_deadline, auto_refund: value.payments.auto_refund_delay ? Duration.toTalerProtocolDuration( value.payments.auto_refund_delay, @@ -565,11 +588,7 @@ export function CreatePage({ <InputDuration name="payments.pay_delay" label={i18n.str`Payment time`} - help={ - <DeadlineHelp duration={value.payments?.pay_delay} /> - } - withForever - withoutClear + help={<DeadlineHelp duration={payDelay} />} tooltip={i18n.str`Time for the customer to pay before the offer expires. Inventory products will be reserved until this deadline. The time starts after the order is created.`} side={ <span> @@ -596,20 +615,12 @@ export function CreatePage({ </FragmentPersonaFlag> <FragmentPersonaFlag point={UIElement.option_advanceOrderCreation} - showAnywayIf={ - errors?.payments?.refund_delay !== undefined - } + showAnywayIf={errors?.payments?.refund_delay !== undefined} > <InputDuration name="payments.refund_delay" label={i18n.str`Refund time`} - help={ - <DeadlineHelp - duration={value.payments?.refund_delay} - /> - } - withForever - withoutClear + help={<DeadlineHelp duration={refundDelay} />} tooltip={i18n.str`Time while the order can be refunded by the merchant. Time starts after the order is created.`} side={ <span> @@ -644,13 +655,7 @@ export function CreatePage({ <InputDuration name="payments.wire_transfer_delay" label={i18n.str`Wire transfer time`} - help={ - <DeadlineHelp - duration={value.payments?.wire_transfer_delay} - /> - } - withoutClear - withForever + help={<DeadlineHelp duration={wireDelay} />} tooltip={i18n.str`Time for the exchange to make the wire transfer. Time starts after the order is created.`} side={ <span> @@ -684,11 +689,7 @@ export function CreatePage({ <InputDuration name="payments.auto_refund_delay" label={i18n.str`Auto-refund time`} - help={ - <DeadlineHelp - duration={value.payments?.auto_refund_delay} - /> - } + help={<DeadlineHelp duration={autoRefundDelay} />} tooltip={i18n.str`Time until which the wallet will automatically check for refunds without user interaction.`} withForever /> @@ -864,7 +865,8 @@ function DeadlineHelp({ duration }: { duration?: Duration }): VNode { clearInterval(iid); }; }); - if (!duration) return <i18n.Translate>Disabled</i18n.Translate>; + if (!duration || !duration.d_ms) + return <i18n.Translate>Disabled</i18n.Translate>; const when = AbsoluteTime.addDuration(now, duration); if (when.t_ms === "never") return <i18n.Translate>No deadline</i18n.Translate>; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx @@ -20,11 +20,11 @@ */ import { + AbsoluteTime, AmountJson, Amounts, HostPortPath, MerchantContractVersion, - OrderVersion, TalerMerchantApi, assertUnreachable, stringifyRefundUri, @@ -43,6 +43,7 @@ import { InputLocation } from "../../../../components/form/InputLocation.js"; import { TextField } from "../../../../components/form/TextField.js"; import { ProductList } from "../../../../components/product/ProductList.js"; import { useSessionContext } from "../../../../context/session.js"; +import { useWaitForOrderPayment } from "../../../../hooks/order.js"; import { datetimeFormatForSettings, usePreference, @@ -50,7 +51,6 @@ import { import { mergeRefunds } from "../../../../utils/amount.js"; import { RefundModal } from "../list/Table.js"; import { Event, Timeline } from "./Timeline.js"; -import { useWaitForOrderPayment } from "../../../../hooks/order.js"; const TALER_SCREEN_ID = 44; @@ -265,6 +265,9 @@ function ClaimedPage({ id: string; order: TalerMerchantApi.CheckPaymentClaimedResponse; }) { + const { i18n } = useTranslationContext(); + const [value, valueHandler] = useState<Partial<Claimed>>(order); + const [settings] = usePreference(); useWaitForOrderPayment(id, order); const now = new Date(); const refundable = @@ -274,46 +277,35 @@ function ClaimedPage({ if (order.contract_terms.timestamp.t_s !== "never") { events.push({ when: new Date(order.contract_terms.timestamp.t_s * 1000), - description: "order created", + description: i18n.str`order created`, type: "start", }); } if (order.contract_terms.pay_deadline.t_s !== "never") { events.push({ when: new Date(order.contract_terms.pay_deadline.t_s * 1000), - description: "pay deadline", - type: "deadline", + description: i18n.str`pay deadline`, + type: "pay-deadline", }); } if (order.contract_terms.refund_deadline.t_s !== "never" && refundable) { events.push({ when: new Date(order.contract_terms.refund_deadline.t_s * 1000), - description: "refund deadline", - type: "deadline", + description: i18n.str`refund deadline`, + type: "refund-deadline", }); } - // if (order.contract_terms.wire_transfer_deadline.t_s !== "never") { - // events.push({ - // when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000), - // description: "wire deadline", - // type: "deadline", - // }); - // } if ( order.contract_terms.delivery_date && order.contract_terms.delivery_date.t_s !== "never" ) { events.push({ when: new Date(order.contract_terms.delivery_date?.t_s * 1000), - description: "delivery", + description: i18n.str`delivery`, type: "delivery", }); } - const [value, valueHandler] = useState<Partial<Claimed>>(order); - const { i18n } = useTranslationContext(); - const [settings] = usePreference(); - return ( <div> <section class="section"> @@ -438,7 +430,6 @@ function PaidPage({ order: TalerMerchantApi.CheckPaymentPaidResponse; onRefund: (id: string) => void; }) { - const { config } = useSessionContext(); const [value, valueHandler] = useState<Partial<Paid>>(order); const { state } = useSessionContext(); const { i18n } = useTranslationContext(); @@ -469,8 +460,8 @@ function PaidPage({ ) { events.push({ when: new Date(order.contract_terms.refund_deadline.t_s * 1000), - description: "refund deadline", - type: "deadline", + description: i18n.str`refund deadline`, + type: "refund-deadline", }); } if ( @@ -480,18 +471,44 @@ function PaidPage({ if (order.contract_terms.delivery_date) events.push({ when: new Date(order.contract_terms.delivery_date?.t_s * 1000), - description: "delivery", + description: i18n.str`delivery`, type: "delivery", }); } - order.refund_details.reduce(mergeRefunds, []).forEach((e) => { - if (e.timestamp.t_s !== "never") { + + const sortedOrders = [...order.refund_details].sort((a, b) => + AbsoluteTime.cmp( + AbsoluteTime.fromProtocolTimestamp(a.timestamp), + AbsoluteTime.fromProtocolTimestamp(b.timestamp), + ), + ); + sortedOrders.reduce(mergeRefunds, []).forEach((e) => { + if (e.timestamp.t_s === "never") return; + if (e.pending) { + if (hasBeenWired) { + events.push({ + when: new Date(e.timestamp.t_s * 1000), + description: !e.reason + ? i18n.str`refund missed: ${e.amount}` + : i18n.str`refund missed: ${e.amount}: ${e.reason}`, + type: "refund-missed", + }); + } else { + events.push({ + when: new Date(e.timestamp.t_s * 1000), + description: !e.reason + ? i18n.str`refund created: ${e.amount}` + : i18n.str`refund created: ${e.amount}: ${e.reason}`, + type: "refund-created", + }); + } + } else { events.push({ when: new Date(e.timestamp.t_s * 1000), description: !e.reason - ? `refund: ${e.amount}` - : `refund: ${e.amount}: ${e.reason}`, - type: e.pending ? "refund" : "refund-taken", + ? i18n.str`refund taken: ${e.amount}` + : i18n.str`refund taken: ${e.amount}: ${e.reason}`, + type: "refund-taken", }); } }); @@ -511,7 +528,7 @@ function PaidPage({ </i18n.Translate> ); } - const { amount, wireFee } = orderAmounts; + const { amount } = orderAmounts; const wireMap: Record< string, @@ -547,7 +564,7 @@ function PaidPage({ Object.values(wireMap).forEach((info) => { events.push({ when: new Date(info.time), - description: `wired ${Amounts.stringify(info.amount)}`, + description: i18n.str`wired ${Amounts.stringify(info.amount)}`, type: info.confirmed ? "wired-confirmed" : "wired", }); }); @@ -555,8 +572,8 @@ function PaidPage({ if (order.contract_terms.wire_transfer_deadline.t_s !== "never") { events.push({ when: new Date(order.contract_terms.wire_transfer_deadline.t_s * 1000), - description: "wire deadline", - type: "deadline", + description: i18n.str`wire deadline`, + type: "wire-deadline", }); } } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx @@ -13,20 +13,29 @@ You should have received a copy of the GNU General Public License along with GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ +import { + assertUnreachable, + TranslatedString +} from "@gnu-taler/taler-util"; +import { useTranslationContext } from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { h } from "preact"; import { useEffect, useState } from "preact/hooks"; -import { datetimeFormatForSettings, usePreference } from "../../../../hooks/preference.js"; +import { + datetimeFormatForSettings, + usePreference, +} from "../../../../hooks/preference.js"; interface Props { events: Event[]; } export function Timeline({ events: e }: Props) { + const { i18n } = useTranslationContext(); const events = [...e]; events.push({ when: new Date(), - description: "now", + description: i18n.str`now`, type: "now", }); @@ -38,7 +47,7 @@ export function Timeline({ events: e }: Props) { const eventsWithoutNow = state.filter((e) => e.type !== "now"); eventsWithoutNow.push({ when: new Date(), - description: "now", + description: i18n.str`now`, type: "now", }); setState(eventsWithoutNow); @@ -54,9 +63,21 @@ export function Timeline({ events: e }: Props) { <div key={i} class="timeline-item"> {(() => { switch (e.type) { - case "deadline": + case "refund-deadline": return ( - <div class="timeline-marker is-icon "> + <div class="timeline-marker is-icon " data-tooltip={i18n.str`This is when the time for making refund has been expired.`}> + <i class="mdi mdi-flag" /> + </div> + ); + case "pay-deadline": + return ( + <div class="timeline-marker is-icon " data-tooltip={i18n.str`This is when the time for making the payment has been expired.`}> + <i class="mdi mdi-flag" /> + </div> + ); + case "wire-deadline": + return ( + <div class="timeline-marker is-icon " data-tooltip={i18n.str`This is when the wire transfer is going to be executed by the payment service provider.`}> <i class="mdi mdi-flag" /> </div> ); @@ -74,38 +95,52 @@ export function Timeline({ events: e }: Props) { ); case "wired": return ( - <div class="timeline-marker is-icon"> + <div class="timeline-marker is-icon" data-tooltip={i18n.str`This is when the wire transfer is going to be executed by the payment service provider.`}> <i class="mdi mdi-cash" /> </div> ); case "wired-confirmed": return ( - <div class="timeline-marker is-icon is-success"> + <div class="timeline-marker is-icon is-success" data-tooltip={i18n.str`This wire transfer has been notified by the payment service provider.`}> <i class="mdi mdi-cash" /> </div> ); - case "refund": + case "refund-created": return ( - <div class="timeline-marker is-icon is-danger"> + <div class="timeline-marker is-icon is-danger" data-tooltip={i18n.str`This refund is waiting to be claimed by the customer.`}> <i class="mdi mdi-cash" /> </div> ); case "refund-taken": return ( - <div class="timeline-marker is-icon is-success"> + <div class="timeline-marker is-icon is-success" data-tooltip={i18n.str`This refund has been claimed by the customer.`}> <i class="mdi mdi-cash" /> </div> ); case "now": return ( - <div class="timeline-marker is-icon is-info"> + <div class="timeline-marker is-icon is-info" data-tooltip={i18n.str`The current moment in time.`}> <i class="mdi mdi-clock" /> </div> ); + case "refund-missed": { + return ( + <div class="timeline-marker is-icon is-danger" data-tooltip={i18n.str`This refund can't be claimed because the wire transfer has already be made.`}> + <i class="mdi mdi-cash" /> + </div> + ); + } + default: { + assertUnreachable(e.type); + } } })()} <div class="timeline-content"> - {e.description !== "now" && <p class="heading">{format(e.when, datetimeFormatForSettings(settings))}</p>} + {e.type !== "now" && ( + <p class="heading"> + {format(e.when, datetimeFormatForSettings(settings))} + </p> + )} <p>{e.description}</p> </div> </div> @@ -116,14 +151,17 @@ export function Timeline({ events: e }: Props) { } export interface Event { when: Date; - description: string; + description: TranslatedString; type: - | "start" - | "refund" - | "refund-taken" - | "wired" - | "wired-confirmed" - | "deadline" - | "delivery" - | "now"; + | "start" + | "refund-created" + | "refund-taken" + | "refund-missed" + | "refund-deadline" + | "pay-deadline" + | "wired" + | "wired-confirmed" + | "wire-deadline" + | "delivery" + | "now"; } diff --git a/packages/merchant-backoffice-ui/src/utils/amount.ts b/packages/merchant-backoffice-ui/src/utils/amount.ts @@ -22,6 +22,7 @@ import { /** * merge refund with the same description and a difference less than one minute + * * @param prev list of refunds that will hold the merged refunds * @param cur new refund to add to the list * @returns list with the new refund, may be merged with the last