/* This file is part of GNU Taler (C) 2021-2023 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 */ /** * * @author Sebastian Javier Marchano (sebasjm) */ import { AmountJson, Amounts, 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"; import { FormProvider } from "../../../../components/form/FormProvider.js"; import { Input } from "../../../../components/form/Input.js"; import { InputCurrency } from "../../../../components/form/InputCurrency.js"; import { InputDate } from "../../../../components/form/InputDate.js"; import { InputDuration } from "../../../../components/form/InputDuration.js"; 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 { 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; interface Props { onBack: () => void; selected: Entity; id: string; onRefund: (id: string, value: MerchantBackend.Orders.RefundRequest) => void; } type Paid = MerchantBackend.Orders.CheckPaymentPaidResponse & { refund_taken: string; }; type Unpaid = MerchantBackend.Orders.CheckPaymentUnpaidResponse; type Claimed = MerchantBackend.Orders.CheckPaymentClaimedResponse; function ContractTerms({ value }: { value: CT }) { const { i18n } = useTranslationContext(); return ( object={value} valueHandler={null}> readonly name="summary" label={i18n.str`Summary`} tooltip={i18n.str`human-readable description of the whole purchase`} /> readonly name="amount" label={i18n.str`Amount`} tooltip={i18n.str`total price for the transaction`} /> {value.fulfillment_url && ( readonly name="fulfillment_url" label={i18n.str`Fulfillment URL`} tooltip={i18n.str`URL for this purchase`} /> )} readonly name="max_fee" label={i18n.str`Max fee`} tooltip={i18n.str`maximum total deposit fee accepted by the merchant for this contract`} /> readonly name="timestamp" label={i18n.str`Created at`} tooltip={i18n.str`time when this contract was generated`} /> readonly name="refund_deadline" label={i18n.str`Refund deadline`} tooltip={i18n.str`after this deadline has passed no refunds will be accepted`} /> readonly name="pay_deadline" label={i18n.str`Payment deadline`} tooltip={i18n.str`after this deadline, the merchant won't accept payments for the contract`} /> readonly name="wire_transfer_deadline" label={i18n.str`Wire transfer deadline`} tooltip={i18n.str`transfer deadline for the exchange`} /> readonly name="delivery_date" label={i18n.str`Delivery date`} tooltip={i18n.str`time indicating when the order should be delivered`} /> {value.delivery_date && ( )} 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`} /> readonly name="extra" label={i18n.str`Extra info`} tooltip={i18n.str`extra data that is only interpreted by the merchant frontend`} /> ); } function ClaimedPage({ id, order, }: { id: string; order: MerchantBackend.Orders.CheckPaymentClaimedResponse; }) { 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") { 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.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", type: "delivery", }); } const [value, valueHandler] = useState>(order); const { i18n } = useTranslationContext(); const [settings] = useSettings() return (
Order #{id}
claimed

{order.contract_terms.amount}

claimed at: {" "} {format( new Date(order.contract_terms.timestamp.t_s * 1000), datetimeFormatForSettings(settings) )}

Timeline
Payment details
object={value} valueHandler={valueHandler} > name="order_status" readonly label={i18n.str`Order status`} />
{order.contract_terms.products.length ? (
Product list
) : undefined} {value.contract_terms && ( )}
); } function PaidPage({ id, order, onRefund, }: { id: string; order: MerchantBackend.Orders.CheckPaymentPaidResponse; onRefund: (id: string) => void; }) { 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") { 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.delivery_date && order.contract_terms.delivery_date.t_s !== "never" ) { if (order.contract_terms.delivery_date) events.push({ when: new Date(order.contract_terms.delivery_date?.t_s * 1000), description: "delivery", type: "delivery", }); } order.refund_details.reduce(mergeRefunds, []).forEach((e) => { if (e.timestamp.t_s !== "never") { events.push({ when: new Date(e.timestamp.t_s * 1000), description: `refund: ${e.amount}: ${e.reason}`, type: e.pending ? "refund" : "refund-taken", }); } }); 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 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() }) const [value, valueHandler] = useState>(order); const { url: backendURL } = useBackendContext() const refundurl = stringifyRefundUri({ merchantBaseUrl: backendURL, orderId: order.contract_terms.order_id }) const refundable = new Date().getTime() < order.contract_terms.refund_deadline.t_s * 1000; 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; }, Amounts.zeroOfCurrency(amount.currency)); value.refund_taken = Amounts.stringify(refund_taken); return (
Order #{id}
paid
{order.wired ? (
wired
) : null} {order.refunded ? (
refunded
) : null}

{order.contract_terms.amount}

{nextEvent &&

Next event in {formatDistance( nextEvent.when, new Date(), // "yyyy/MM/dd HH:mm:ss", )}

}
Timeline
Payment details
object={value} valueHandler={valueHandler} > {/* name="deposit_total" readonly label={i18n.str`Deposit total`} /> */} {order.refunded && ( name="refund_amount" readonly label={i18n.str`Refunded amount`} /> )} {order.refunded && ( name="refund_taken" readonly label={i18n.str`Refund taken`} /> )} name="order_status" readonly label={i18n.str`Order status`} /> name="order_status_url" label={i18n.str`Status URL`} > {order.order_status_url} {order.refunded && ( name="order_status_url" label={i18n.str`Refund URI`} > {refundurl} )}
{order.contract_terms.products.length ? (
Product list
) : undefined} {value.contract_terms && ( )}
); } function UnpaidPage({ id, order, }: { id: string; order: MerchantBackend.Orders.CheckPaymentUnpaidResponse; }) { const [value, valueHandler] = useState>(order); const { i18n } = useTranslationContext(); const [settings] = useSettings() return (

Order #{id}

unpaid

pay at: {" "} {order.order_status_url}

created at: {" "} {order.creation_time.t_s === "never" ? "never" : format( new Date(order.creation_time.t_s * 1000), datetimeFormatForSettings(settings) )}

object={value} valueHandler={valueHandler}> readonly name="summary" label={i18n.str`Summary`} tooltip={i18n.str`human-readable description of the whole purchase`} /> readonly name="total_amount" label={i18n.str`Amount`} tooltip={i18n.str`total price for the transaction`} /> name="order_status" readonly label={i18n.str`Order status`} /> name="order_status_url" readonly label={i18n.str`Order status URL`} /> name="taler_pay_uri" label={i18n.str`Payment URI`} > {value.taler_pay_uri}
); } export function DetailPage({ id, selected, onRefund, onBack }: Props): VNode { const [showRefund, setShowRefund] = useState(undefined); const { i18n } = useTranslationContext(); const DetailByStatus = function () { switch (selected.order_status) { case "claimed": return ; case "paid": return ; case "unpaid": return ; default: return (
Unknown order status. This is an error, please contact the administrator.
); } }; return ( {DetailByStatus()} {showRefund && ( setShowRefund(undefined)} onConfirm={(value) => { onRefund(showRefund, value); setShowRefund(undefined); }} /> )}
); } async function copyToClipboard(text: string) { return navigator.clipboard.writeText(text); }