diff options
Diffstat (limited to 'packages/merchant-backoffice-ui/src/paths/instance/orders/details')
4 files changed, 183 insertions, 163 deletions
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx index 6e73a01a5..7d4877db9 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Detail.stories.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,9 +19,9 @@ * @author Sebastian Javier Marchano (sebasjm) */ +import { AmountString, TalerMerchantApi } from "@gnu-taler/taler-util"; import { addDays } from "date-fns"; -import { h, VNode, FunctionalComponent } from "preact"; -import { MerchantBackend } from "../../../../declaration.js"; +import { FunctionalComponent, h } from "preact"; import { DetailPage as TestedComponent } from "./DetailPage.js"; export default { @@ -42,14 +42,13 @@ function createExample<Props>( return r; } -const defaultContractTerm = { - amount: "TESTKUDOS:10", +const defaultContractTerm: TalerMerchantApi.ContractTerms = { + amount: "TESTKUDOS:10" as AmountString, timestamp: { t_s: new Date().getTime() / 1000, }, - auditors: [], exchanges: [], - max_fee: "TESTKUDOS:1", + max_fee: "TESTKUDOS:1" as AmountString, merchant: {} as any, merchant_base_url: "http://merchant.url/", order_id: "2021.165-03GDFC26Y1NNG", @@ -66,7 +65,7 @@ const defaultContractTerm = { }, wire_method: "x-taler-bank", h_wire: "asd", -} as MerchantBackend.ContractTerms; +}; // contract_terms: defaultContracTerm, export const Claimed = createExample(TestedComponent, { @@ -83,16 +82,16 @@ export const PaidNotRefundable = createExample(TestedComponent, { order_status: "paid", contract_terms: defaultContractTerm, refunded: false, - deposit_total: "TESTKUDOS:10", - exchange_ec: 0, + deposit_total: "TESTKUDOS:10" as AmountString, + exchange_code: 0, order_status_url: "http://merchant.backend/status", - exchange_hc: 0, - refund_amount: "TESTKUDOS:0", + exchange_http_status: 0, + refund_amount: "TESTKUDOS:0" as AmountString, refund_details: [], refund_pending: false, wire_details: [], - wire_reports: [], wired: false, + wire_reports: [], }, }); @@ -107,15 +106,15 @@ export const PaidRefundable = createExample(TestedComponent, { }, }, refunded: false, - deposit_total: "TESTKUDOS:10", - exchange_ec: 0, + deposit_total: "TESTKUDOS:10" as AmountString, + exchange_code: 0, order_status_url: "http://merchant.backend/status", - exchange_hc: 0, - refund_amount: "TESTKUDOS:0", + exchange_http_status: 0, + refund_amount: "TESTKUDOS:0" as AmountString, refund_details: [], + wire_reports: [], refund_pending: false, wire_details: [], - wire_reports: [], wired: false, }, }); @@ -130,6 +129,6 @@ export const Unpaid = createExample(TestedComponent, { }, summary: "text summary", taler_pay_uri: "pay uri", - total_amount: "TESTKUDOS:10", + total_amount: "TESTKUDOS:10" as AmountString, }, }); 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 index 5ff76e37a..498ea83e3 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -19,8 +19,15 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { AmountJson, Amounts, stringifyRefundUri } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + AmountJson, + Amounts, + TalerMerchantApi, + stringifyRefundUri, +} from "@gnu-taler/taler-util"; +import { + useTranslationContext +} from "@gnu-taler/web-util/browser"; import { format, formatDistance } from "date-fns"; import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; @@ -33,28 +40,30 @@ import { InputGroup } from "../../../../components/form/InputGroup.js"; import { InputLocation } from "../../../../components/form/InputLocation.js"; import { TextField } from "../../../../components/form/TextField.js"; import { ProductList } from "../../../../components/product/ProductList.js"; -import { useBackendContext } from "../../../../context/backend.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { + datetimeFormatForSettings, + usePreference, +} from "../../../../hooks/preference.js"; import { mergeRefunds } from "../../../../utils/amount.js"; import { RefundModal } from "../list/Table.js"; import { Event, Timeline } from "./Timeline.js"; -type Entity = MerchantBackend.Orders.MerchantOrderStatusResponse; -type CT = MerchantBackend.ContractTerms; +type Entity = TalerMerchantApi.MerchantOrderStatusResponse; +type CT = TalerMerchantApi.ContractTerms; interface Props { onBack: () => void; selected: Entity; id: string; - onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void; + onRefund: (id: string, value: TalerMerchantApi.RefundRequest) => void; } -type Paid = MerchantBackend.Orders.CheckPaymentPaidResponse & { +type Paid = TalerMerchantApi.CheckPaymentPaidResponse & { refund_taken: string; }; -type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse; -type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse; +type Unpaid = TalerMerchantApi.CheckPaymentUnpaidResponse; +type Claimed = TalerMerchantApi.CheckPaymentClaimedResponse; function ContractTerms({ value }: { value: CT }) { const { i18n } = useTranslationContext(); @@ -149,8 +158,12 @@ function ClaimedPage({ order, }: { id: string; - order: MerchantBackend.Orders.CheckPaymentClaimedResponse; + order: TalerMerchantApi.CheckPaymentClaimedResponse; }) { + const now = new Date(); + const refundable = + order.contract_terms.refund_deadline.t_s !== "never" && + now.getTime() < order.contract_terms.refund_deadline.t_s * 1000; const events: Event[] = []; if (order.contract_terms.timestamp.t_s !== "never") { events.push({ @@ -166,20 +179,20 @@ function ClaimedPage({ type: "deadline", }); } - if (order.contract_terms.refund_deadline.t_s !== "never") { + 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", }); } - 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.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" @@ -193,7 +206,7 @@ function ClaimedPage({ const [value, valueHandler] = useState<Partial<Claimed>>(order); const { i18n } = useTranslationContext(); - const [settings] = useSettings() + const [settings] = usePreference(); return ( <div> @@ -237,10 +250,14 @@ function ClaimedPage({ <b> <i18n.Translate>claimed at</i18n.Translate>: </b>{" "} - {format( - new Date(order.contract_terms.timestamp.t_s * 1000), - datetimeFormatForSettings(settings) - )} + {order.contract_terms.timestamp.t_s === "never" + ? "never" + : format( + new Date( + order.contract_terms.timestamp.t_s * 1000, + ), + datetimeFormatForSettings(settings), + )} </p> </div> </div> @@ -311,25 +328,16 @@ function PaidPage({ onRefund, }: { id: string; - order: MerchantBackend.Orders.CheckPaymentPaidResponse; + order: TalerMerchantApi.CheckPaymentPaidResponse; onRefund: (id: string) => void; }) { + const now = new Date(); + const refundable = + order.contract_terms.refund_deadline.t_s !== "never" && + now.getTime() < order.contract_terms.refund_deadline.t_s * 1000; + const events: Event[] = []; - if (order.contract_terms.timestamp.t_s !== "never") { - events.push({ - when: new Date(order.contract_terms.timestamp.t_s * 1000), - description: "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", - }); - } - if (order.contract_terms.refund_deadline.t_s !== "never") { + 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", @@ -363,66 +371,71 @@ function PaidPage({ }); } }); - if (order.wire_details && order.wire_details.length) { - if (order.wire_details.length > 1) { - let last: MerchantBackend.Orders.TransactionWireTransfer | null = null; - let first: MerchantBackend.Orders.TransactionWireTransfer | null = null; - let total: AmountJson | null = null; - - order.wire_details.forEach((w) => { - if (last === null || last.execution_time.t_s < w.execution_time.t_s) { - last = w; - } - if (first === null || first.execution_time.t_s > w.execution_time.t_s) { - first = w; - } - total = - total === null - ? Amounts.parseOrThrow(w.amount) - : Amounts.add(total, Amounts.parseOrThrow(w.amount)).amount; - }); - const last_time = last!.execution_time.t_s; - if (last_time !== "never") { - events.push({ - when: new Date(last_time * 1000), - description: `wired ${Amounts.stringify(total!)}`, - type: "wired-range", + 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) { + if (order.wire_details && order.wire_details.length) { + if (order.wire_details.length > 1) { + let last: TalerMerchantApi.TransactionWireTransfer | null = null; + let first: TalerMerchantApi.TransactionWireTransfer | null = null; + let total: AmountJson | null = null; + + order.wire_details.forEach((w) => { + if (last === null || last.execution_time.t_s < w.execution_time.t_s) { + last = w; + } + if ( + first === null || + first.execution_time.t_s > w.execution_time.t_s + ) { + first = w; + } + total = + total === null + ? Amounts.parseOrThrow(w.amount) + : Amounts.add(total, Amounts.parseOrThrow(w.amount)).amount; }); - } - const first_time = first!.execution_time.t_s; - if (first_time !== "never") { - events.push({ - when: new Date(first_time * 1000), - description: `wire transfer started...`, - type: "wired-range", - }); - } - } else { - order.wire_details.forEach((e) => { - if (e.execution_time.t_s !== "never") { + const last_time = last!.execution_time.t_s; + if (last_time !== "never") { events.push({ - when: new Date(e.execution_time.t_s * 1000), - description: `wired ${e.amount}`, - type: "wired", + when: new Date(last_time * 1000), + description: `wired ${Amounts.stringify(total!)}`, + type: "wired-range", }); } - }); + const first_time = first!.execution_time.t_s; + if (first_time !== "never") { + events.push({ + when: new Date(first_time * 1000), + description: `wire transfer started...`, + type: "wired-range", + }); + } + } else { + order.wire_details.forEach((e) => { + if (e.execution_time.t_s !== "never") { + events.push({ + when: new Date(e.execution_time.t_s * 1000), + description: `wired ${e.amount}`, + type: "wired", + }); + } + }); + } } } - const now = new Date() const nextEvent = events.find((e) => { - return e.when.getTime() > now.getTime() - }) + return e.when.getTime() > now.getTime(); + }); const [value, valueHandler] = useState<Partial<Paid>>(order); - const { url: backendURL } = useBackendContext() + const { state } = useSessionContext(); + const refundurl = stringifyRefundUri({ - merchantBaseUrl: backendURL, - orderId: order.contract_terms.order_id - }) - const refundable = - new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000; + merchantBaseUrl: state.backendUrl.href, + orderId: order.contract_terms.order_id, + }); const { i18n } = useTranslationContext(); const amount = Amounts.parseOrThrow(order.contract_terms.amount); @@ -503,15 +516,16 @@ function PaidPage({ textOverflow: "ellipsis", }} > - {nextEvent && + {nextEvent && ( <p> - <i18n.Translate>Next event in </i18n.Translate> {formatDistance( + <i18n.Translate>Next event in </i18n.Translate>{" "} + {formatDistance( nextEvent.when, new Date(), // "yyyy/MM/dd HH:mm:ss", )} </p> - } + )} </div> </div> </div> @@ -607,11 +621,11 @@ function UnpaidPage({ order, }: { id: string; - order: MerchantBackend.Orders.CheckPaymentUnpaidResponse; + order: TalerMerchantApi.CheckPaymentUnpaidResponse; }) { const [value, valueHandler] = useState<Partial<Unpaid>>(order); const { i18n } = useTranslationContext(); - const [settings] = useSettings() + const [settings] = usePreference(); return ( <div> <section class="hero is-hero-bar"> @@ -659,9 +673,9 @@ function UnpaidPage({ {order.creation_time.t_s === "never" ? "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> @@ -764,7 +778,3 @@ export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode { </Fragment> ); } - -async function copyToClipboard(text: string) { - return navigator.clipboard.writeText(text); -} 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 index 8c863f386..2d62e2252 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/Timeline.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -16,7 +16,7 @@ import { format } from "date-fns"; import { h } from "preact"; import { useEffect, useState } from "preact/hooks"; -import { datetimeFormatForSettings, useSettings } from "../../../../hooks/useSettings.js"; +import { datetimeFormatForSettings, usePreference } from "../../../../hooks/preference.js"; interface Props { events: Event[]; @@ -31,7 +31,7 @@ export function Timeline({ events: e }: Props) { }); events.sort((a, b) => a.when.getTime() - b.when.getTime()); - const [settings] = useSettings(); + const [settings] = usePreference(); const [state, setState] = useState(events); useEffect(() => { const handle = setTimeout(() => { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx index 1517a3c42..b28e59b29 100644 --- a/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx +++ b/packages/merchant-backoffice-ui/src/paths/instance/orders/details/index.tsx @@ -1,6 +1,6 @@ /* This file is part of GNU Taler - (C) 2021-2023 Taler Systems S.A. + (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 @@ -14,55 +14,62 @@ GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> */ import { - useTranslationContext, - HttpError, - ErrorType, + HttpStatusCode, + TalerError, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { + useTranslationContext } from "@gnu-taler/web-util/browser"; -import { Fragment, h, VNode } from "preact"; +import { Fragment, VNode, h } from "preact"; import { useState } from "preact/hooks"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { NotificationCard } from "../../../../components/menu/index.js"; -import { MerchantBackend } from "../../../../declaration.js"; -import { useOrderAPI, useOrderDetails } from "../../../../hooks/order.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { useOrderDetails } from "../../../../hooks/order.js"; import { Notification } from "../../../../utils/types.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { DetailPage } from "./DetailPage.js"; -import { HttpStatusCode } from "@gnu-taler/taler-util"; export interface Props { oid: string; - onBack: () => void; - onUnauthorized: () => VNode; - onNotFound: () => VNode; - onLoadError: (error: HttpError<MerchantBackend.ErrorDetail>) => VNode; } -export default function Update({ - oid, - onBack, - onLoadError, - onNotFound, - onUnauthorized, -}: Props): VNode { - const { refundOrder } = useOrderAPI(); +export default function Update({ oid, onBack }: Props): VNode { const result = useOrderDetails(oid); const [notif, setNotif] = useState<Notification | undefined>(undefined); + const { lib: api } = useSessionContext(); + const { state } = useSessionContext(); const { i18n } = useTranslationContext(); - if (result.loading) return <Loading />; - if (!result.ok) { - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.Unauthorized - ) - return onUnauthorized(); - if ( - result.type === ErrorType.CLIENT && - result.status === HttpStatusCode.NotFound - ) - return onNotFound(); - return onLoadError(result); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + case HttpStatusCode.BadGateway: { + return <div>Failed to obtain a response from the exchange</div>; + } + case HttpStatusCode.GatewayTimeout: { + return ( + <div>The merchant's interaction with the exchange took too long</div> + ); + } + case HttpStatusCode.Unauthorized: { + return <LoginPage /> + } + default: { + assertUnreachable(result); + } + } } return ( @@ -72,8 +79,12 @@ export default function Update({ <DetailPage onBack={onBack} id={oid} - onRefund={(id, value) => - refundOrder(id, value) + onRefund={(id, value) => { + if (state.status !== "loggedIn") { + return; + } + api.instance + .addRefund(state.token, id, value) .then(() => setNotif({ message: i18n.str`refund created successfully`, @@ -86,9 +97,9 @@ export default function Update({ type: "ERROR", description: error.message, }), - ) - } - selected={result.data} + ); + }} + selected={result.body} /> </Fragment> ); |