commit b04f8d31821d3a9fc8c086c3c164a983c86da75a
parent f28473e3aee09e832baf1ce39ec4183fe7cb8c5e
Author: Sebastian <sebasjm@taler-systems.com>
Date: Wed, 3 Dec 2025 17:12:22 -0300
fix #10664
Diffstat:
4 files changed, 176 insertions(+), 226 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx
@@ -218,9 +218,6 @@ export function CreatePage({ onConfirm, onBack, forceId }: Props): VNode {
return (
<div>
<LocalNotificationBannerBulma notification={notification} />
- <pre>
- {JSON.stringify({errors}, undefined,2)}
- </pre>
<section class="section is-main-section">
<div class="columns">
<div class="column" />
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
@@ -25,6 +25,7 @@ import {
Amounts,
Duration,
HttpStatusCode,
+ OrderVersion,
TalerMerchantApi,
TalerProtocolDuration,
} from "@gnu-taler/taler-util";
@@ -254,7 +255,12 @@ export function CreatePage({
? undefined
: {
order: {
+ version: OrderVersion.V0,
amount: price,
+ // choices: [{
+ // amount: price,
+ // max_fee: value.payments.max_fee as AmountString,
+ // }],
summary: summary,
products: productList,
extra: undefinedIfEmpty(value.extra),
@@ -281,7 +287,6 @@ export function CreatePage({
value.payments.auto_refund_deadline,
)
: undefined,
- max_fee: value.payments.max_fee as AmountString,
delivery_date: value.shipping.delivery_date
? { t_s: value.shipping.delivery_date.getTime() / 1000 }
: undefined,
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx
@@ -1,173 +0,0 @@
-/*
- This file is part of GNU Taler
- (C) 2021-2024 Taler Systems S.A.
-
- GNU Taler is free software; you can redistribute it and/or modify it under the
- terms of the GNU General Public License as published by the Free Software
- Foundation; either version 3, or (at your option) any later version.
-
- GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
- WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
- A PARTICULAR PURPOSE. See the GNU General Public License for more details.
-
- 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 {
- HttpStatusCode,
- OrderVersion,
- TalerError,
- TalerErrorCode,
- assertUnreachable,
-} from "@gnu-taler/taler-util";
-import { Loading, useTranslationContext } from "@gnu-taler/web-util/browser";
-import { VNode, h } from "preact";
-import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
-import { CreatedSuccessfully } from "../../../../components/notifications/CreatedSuccessfully.js";
-import { useOrderDetails } from "../../../../hooks/order.js";
-import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
-import { Entity } from "./index.js";
-import { LoginPage } from "../../../login/index.js";
-
-const TALER_SCREEN_ID = 43;
-
-interface Props {
- entity: Entity;
- onConfirm: () => void;
- onCreateAnother?: () => void;
-}
-
-export function OrderCreatedSuccessfully({
- entity,
- onConfirm,
- onCreateAnother,
-}: Props): VNode {
- const result = useOrderDetails(entity.response.order_id);
- const { i18n } = useTranslationContext();
-
- if (!result) return <Loading />;
- if (result instanceof TalerError) {
- return <ErrorLoadingMerchant error={result} />;
- }
-
- if (
- entity.request.order.version !== undefined &&
- entity.request.order.version !== OrderVersion.V0
- ) {
- return <div>Unsupported order version {entity.request.order.version}</div>;
- }
-
- if (result.type === "fail") {
- switch (result.case) {
- case TalerErrorCode.MERCHANT_GENERIC_INSTANCE_UNKNOWN: {
- return <NotFoundPageOrAdminCreate />;
- }
- case TalerErrorCode.MERCHANT_GENERIC_ORDER_UNKNOWN: {
- return <i18n.Translate>Order unknown</i18n.Translate>;
- }
- case HttpStatusCode.Unauthorized: {
- return <LoginPage />;
- }
- default: {
- assertUnreachable(result);
- }
- }
- }
-
- const url =
- result.body.order_status === "unpaid"
- ? result.body.taler_pay_uri
- : result.body.contract_terms.fulfillment_url;
-
- return (
- <CreatedSuccessfully
- onConfirm={onConfirm}
- onCreateAnother={onCreateAnother}
- >
- {!entity.request.order.version ? (
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Amount</i18n.Translate>
- </label>
- </div>
- <div class="field-body is-flex-grow-3">
- <div class="field">
- <p class="control">
- <input
- class="input"
- readonly
- value={entity.request.order.amount}
- />
- </p>
- </div>
- </div>
- </div>
- ) : (
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Choices</i18n.Translate>
- </label>
- </div>
- <div class="field-body is-flex-grow-3">
- <div class="field">
- <p class="control">
- <i18n.Translate>
- This feature is not yet supported
- </i18n.Translate>
- </p>
- </div>
- </div>
- </div>
- )}
-
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Summary</i18n.Translate>
- </label>
- </div>
- <div class="field-body is-flex-grow-3">
- <div class="field">
- <p class="control">
- <input
- class="input"
- readonly
- value={entity.request.order.summary}
- />
- </p>
- </div>
- </div>
- </div>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Order ID</i18n.Translate>
- </label>
- </div>
- <div class="field-body is-flex-grow-3">
- <div class="field">
- <p class="control">
- <input class="input" readonly value={entity.response.order_id} />
- </p>
- </div>
- </div>
- </div>
- <div class="field is-horizontal">
- <div class="field-label is-normal">
- <label class="label">
- <i18n.Translate>Payment URL</i18n.Translate>
- </label>
- </div>
- <div class="field-body is-flex-grow-3">
- <div class="field">
- <p class="control">
- <input class="input" readonly value={url} />
- </p>
- </div>
- </div>
- </div>
- </CreatedSuccessfully>
- );
-}
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
@@ -24,7 +24,9 @@ import {
Amounts,
HostPortPath,
MerchantContractVersion,
+ OrderVersion,
TalerMerchantApi,
+ assertUnreachable,
stringifyRefundUri,
} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
@@ -53,7 +55,9 @@ import { Event, Timeline } from "./Timeline.js";
const TALER_SCREEN_ID = 44;
type Entity = TalerMerchantApi.MerchantOrderStatusResponse;
-type CT = TalerMerchantApi.MerchantContractTermsV0;
+type CT = TalerMerchantApi.MerchantContractTerms;
+type CT0 = TalerMerchantApi.MerchantContractTermsV0;
+type CT1 = TalerMerchantApi.MerchantContractTermsV1;
export interface Props {
onBack: () => void;
@@ -69,62 +73,162 @@ type Unpaid = TalerMerchantApi.CheckPaymentUnpaidResponse;
type Claimed = TalerMerchantApi.CheckPaymentClaimedResponse;
function ContractTerms({ value }: { value: CT }) {
+ if (value.version === undefined) {
+ return <ContractTerms_V0 value={value} />
+ }
+ switch (value.version) {
+ case MerchantContractVersion.V0:
+ return <ContractTerms_V0 value={value} />
+ case MerchantContractVersion.V1:
+ return <ContractTerms_V1 value={value} />
+ default:
+ assertUnreachable(value.version)
+ }
+}
+
+function ContractTerms_V0({ value }: { value: CT0 }) {
const { i18n } = useTranslationContext();
return (
<InputGroup name="contract_terms" label={i18n.str`Contract terms`}>
- <FormProvider<CT> object={value} valueHandler={null}>
- <Input<CT>
+ <FormProvider<CT0> object={value} valueHandler={null}>
+ <Input<CT0>
readonly
name="summary"
label={i18n.str`Summary`}
tooltip={i18n.str`Human-readable description of the whole purchase`}
/>
- <InputCurrency<CT>
+ <InputCurrency<CT0>
readonly
name="amount"
label={i18n.str`Amount`}
tooltip={i18n.str`Total price for the transaction`}
/>
{value.fulfillment_url && (
- <Input<CT>
+ <Input<CT0>
readonly
name="fulfillment_url"
label={i18n.str`Fulfillment URL`}
tooltip={i18n.str`URL for this purchase`}
/>
)}
- <Input<CT>
+ <Input<CT0>
readonly
name="max_fee"
label={i18n.str`Max fee`}
tooltip={i18n.str`Maximum total deposit fee accepted by the merchant for this contract`}
/>
- <InputDate<CT>
+ <InputDate<CT0>
+ readonly
+ name="timestamp"
+ label={i18n.str`Created at`}
+ tooltip={i18n.str`Time when this contract was generated`}
+ />
+ <InputDate<CT0>
+ readonly
+ name="refund_deadline"
+ label={i18n.str`Refund deadline`}
+ tooltip={i18n.str`After this deadline has passed, no refunds will be accepted.`}
+ />
+ <InputDate<CT0>
+ readonly
+ name="pay_deadline"
+ label={i18n.str`Payment deadline`}
+ tooltip={i18n.str`After this deadline, the merchant won't accept payments for the contract`}
+ />
+ <InputDate<CT0>
+ readonly
+ name="wire_transfer_deadline"
+ label={i18n.str`Wire transfer deadline`}
+ tooltip={i18n.str`Transfer deadline for the exchange`}
+ />
+ <InputDate<CT0>
+ readonly
+ name="delivery_date"
+ label={i18n.str`Delivery date`}
+ tooltip={i18n.str`Time indicating when the order should be delivered`}
+ />
+ <InputGroup
+ name="delivery_location"
+ label={i18n.str`Delivery location`}
+ tooltip={i18n.str`Where the order will be delivered`}
+ >
+ <InputLocation name="delivery_location" />
+ </InputGroup>
+ <InputDuration<CT0>
+ readonly
+ name="auto_refund"
+ label={i18n.str`Auto-refund delay`}
+ tooltip={i18n.str`How long the wallet should try to get an automatic refund for the purchase`}
+ />
+ <Input<CT0>
+ readonly
+ name="extra"
+ label={i18n.str`Extra info`}
+ tooltip={i18n.str`Extra data that is only interpreted by the merchant frontend`}
+ />
+ </FormProvider>
+ </InputGroup>
+ );
+}
+
+function ContractTerms_V1({ value }: { value: CT1 }) {
+ const { i18n } = useTranslationContext();
+
+ return (
+ <InputGroup name="contract_terms" label={i18n.str`Contract terms`}>
+ <FormProvider<CT1> object={value} valueHandler={null}>
+ <Input<CT1>
+ readonly
+ name="summary"
+ label={i18n.str`Summary`}
+ tooltip={i18n.str`Human-readable description of the whole purchase`}
+ />
+ {/* <InputCurrency<CT1>
+ readonly
+ name="amount"
+ label={i18n.str`Amount`}
+ tooltip={i18n.str`Total price for the transaction`}
+ />
+ <Input<CT1>
+ readonly
+ name="max_fee"
+ label={i18n.str`Max fee`}
+ tooltip={i18n.str`Maximum total deposit fee accepted by the merchant for this contract`}
+ /> */}
+ {value.fulfillment_url && (
+ <Input<CT1>
+ readonly
+ name="fulfillment_url"
+ label={i18n.str`Fulfillment URL`}
+ tooltip={i18n.str`URL for this purchase`}
+ />
+ )}
+ <InputDate<CT1>
readonly
name="timestamp"
label={i18n.str`Created at`}
tooltip={i18n.str`Time when this contract was generated`}
/>
- <InputDate<CT>
+ <InputDate<CT1>
readonly
name="refund_deadline"
label={i18n.str`Refund deadline`}
tooltip={i18n.str`After this deadline has passed, no refunds will be accepted.`}
/>
- <InputDate<CT>
+ <InputDate<CT1>
readonly
name="pay_deadline"
label={i18n.str`Payment deadline`}
tooltip={i18n.str`After this deadline, the merchant won't accept payments for the contract`}
/>
- <InputDate<CT>
+ <InputDate<CT1>
readonly
name="wire_transfer_deadline"
label={i18n.str`Wire transfer deadline`}
tooltip={i18n.str`Transfer deadline for the exchange`}
/>
- <InputDate<CT>
+ <InputDate<CT1>
readonly
name="delivery_date"
label={i18n.str`Delivery date`}
@@ -137,13 +241,13 @@ function ContractTerms({ value }: { value: CT }) {
>
<InputLocation name="delivery_location" />
</InputGroup>
- <InputDuration<CT>
+ <InputDuration<CT1>
readonly
name="auto_refund"
label={i18n.str`Auto-refund delay`}
tooltip={i18n.str`How long the wallet should try to get an automatic refund for the purchase`}
/>
- <Input<CT>
+ <Input<CT1>
readonly
name="extra"
label={i18n.str`Extra info`}
@@ -161,11 +265,6 @@ function ClaimedPage({
id: string;
order: TalerMerchantApi.CheckPaymentClaimedResponse;
}) {
- if (order.contract_terms.version !== undefined &&
- order.contract_terms.version !== MerchantContractVersion.V0
- ) {
- return <div>Unsupported contract version {order.contract_terms.version}</div>;
- }
const now = new Date();
const refundable =
@@ -234,13 +333,14 @@ function ClaimedPage({
</div>
</div>
- <div class="level">
+ {/* {order.contract_terms.version} */}
+ {/* <div class="level">
<div class="level-left">
<div class="level-item">
<h1 class="title">{order.contract_terms.amount}</h1>
</div>
</div>
- </div>
+ </div> */}
<div class="level">
<div class="level-left" style={{ maxWidth: "100%" }}>
@@ -260,11 +360,11 @@ function ClaimedPage({
{order.contract_terms.timestamp.t_s === "never"
? i18n.str`Never`
: format(
- new Date(
- order.contract_terms.timestamp.t_s * 1000,
- ),
- datetimeFormatForSettings(settings),
- )}
+ new Date(
+ order.contract_terms.timestamp.t_s * 1000,
+ ),
+ datetimeFormatForSettings(settings),
+ )}
</p>
</div>
</div>
@@ -319,10 +419,7 @@ function ClaimedPage({
</Fragment>
) : undefined}
- {value.contract_terms && (
- value.contract_terms.version === undefined
- || value.contract_terms.version === MerchantContractVersion.V0
- ) && (
+ {!value.contract_terms ? undefined : (
<ContractTerms value={value.contract_terms} />
)}
</div>
@@ -341,11 +438,7 @@ function PaidPage({
order: TalerMerchantApi.CheckPaymentPaidResponse;
onRefund: (id: string) => void;
}) {
- if (order.contract_terms.version !== undefined &&
- order.contract_terms.version !== MerchantContractVersion.V0
- ) {
- return <div>Unsupported contract version {order.contract_terms.version}</div>;
- }
+ const { config } = useSessionContext();
const now = new Date();
const refundable =
@@ -387,9 +480,28 @@ function PaidPage({
});
}
});
+
+ const [value, valueHandler] = useState<Partial<Paid>>(order);
+ const { state } = useSessionContext();
+ const { i18n } = useTranslationContext();
+
+
+ const orderAmounts = getOrderAmountAndWirefee(order)
+ if (orderAmounts === "v1-without-index") {
+ return <i18n.Translate>The contract terms has a v1 order without choices_index.</i18n.Translate>
+ }
+ if (orderAmounts === "v1-wrong-index") {
+ return <i18n.Translate>The contract terms has a v1 order with a bad choices_index.</i18n.Translate>
+
+ }
+ const { amount, wireFee } = orderAmounts
+ if (order.contract_terms.version === MerchantContractVersion.V1) {
+
+ order.choice_index
+ }
const ra = !order.refunded ? undefined : Amounts.parse(order.refund_amount);
- const am = Amounts.parseOrThrow(order.contract_terms.amount);
- if (ra && Amounts.cmp(ra, am) === 1) {
+ // const am = Amounts.parseOrThrow(order.contract_terms.amount);
+ if (ra && Amounts.cmp(ra, amount) === 1) {
if (order.wire_details && order.wire_details.length) {
if (order.wire_details.length > 1) {
let last: TalerMerchantApi.TransactionWireTransfer | null = null;
@@ -445,16 +557,12 @@ function PaidPage({
return e.when.getTime() > now.getTime();
});
- const [value, valueHandler] = useState<Partial<Paid>>(order);
- const { state } = useSessionContext();
const refundurl = stringifyRefundUri({
merchantBaseUrl: state.backendUrl.href as HostPortPath,
orderId: order.contract_terms.order_id,
});
- const { i18n } = useTranslationContext();
- const amount = Amounts.parseOrThrow(order.contract_terms.amount);
const refund_taken = order.refund_details.reduce((prev, cur) => {
if (cur.pending) return prev;
return Amounts.add(prev, Amounts.parseOrThrow(cur.amount)).amount;
@@ -492,7 +600,7 @@ function PaidPage({
<div class="level">
<div class="level-left">
<div class="level-item">
- <h1 class="title">{order.contract_terms.amount}</h1>
+ <h1 class="title">{Amounts.stringify(amount)}</h1>
</div>
</div>
<div class="level-right">
@@ -507,7 +615,7 @@ function PaidPage({
: i18n.str`Not refundable`
}
>
- <button type="button"
+ <button type="button"
class="button is-danger"
disabled={!refundable}
onClick={() => onRefund(id)}
@@ -621,10 +729,7 @@ function PaidPage({
</Fragment>
) : undefined}
- {value.contract_terms && (
- value.contract_terms.version === undefined
- || value.contract_terms.version === MerchantContractVersion.V0
- ) && (
+ {!value.contract_terms ? undefined : (
<ContractTerms value={value.contract_terms} />
)}
</div>
@@ -692,9 +797,9 @@ function UnpaidPage({
{order.creation_time.t_s === "never"
? i18n.str`Never`
: format(
- new Date(order.creation_time.t_s * 1000),
- datetimeFormatForSettings(settings),
- )}
+ new Date(order.creation_time.t_s * 1000),
+ datetimeFormatForSettings(settings),
+ )}
</p>
</div>
</div>
@@ -798,3 +903,19 @@ export function DetailPage({ id, selected, onRefunded, onBack }: Props): VNode {
</Fragment>
);
}
+export function getOrderAmountAndWirefee(order: TalerMerchantApi.CheckPaymentPaidResponse) {
+ if (order.contract_terms.version === undefined || order.contract_terms.version === MerchantContractVersion.V0) {
+ const amount = Amounts.parseOrThrow(order.contract_terms.amount)
+ const wireFee = Amounts.parseOrThrow(order.contract_terms.wire_method)
+ return { amount, wireFee }
+ }
+ if (order.contract_terms.version === MerchantContractVersion.V1) {
+ if (order.choice_index === undefined) return "v1-without-index" as const
+ if (order.choice_index > order.contract_terms.choices.length) return "v1-wrong-index" as const
+ const choice = order.contract_terms.choices[order.choice_index]
+ const amount = Amounts.parseOrThrow(choice.amount)
+ const wireFee = Amounts.parseOrThrow(choice.max_fee)
+ return { amount, wireFee }
+ }
+ assertUnreachable(order.contract_terms.version)
+}