commit 50f768fa601e4a891cecd122687553fe89a72606
parent 7032a2da0a6120a81b010097ef66b6beeef6aa5d
Author: Sebastian <sebasjm@taler-systems.com>
Date: Sun, 14 Dec 2025 16:58:07 -0300
fix #10695
Diffstat:
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