taler-typescript-core

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

commit b04f8d31821d3a9fc8c086c3c164a983c86da75a
parent f28473e3aee09e832baf1ce39ec4183fe7cb8c5e
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Wed,  3 Dec 2025 17:12:22 -0300

fix #10664

Diffstat:
Mpackages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx | 3---
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx | 7++++++-
Dpackages/merchant-backoffice-ui/src/paths/instance/orders/create/OrderCreatedSuccessfully.tsx | 173-------------------------------------------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx | 219+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
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) +}