taler-typescript-core

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

commit 07b79506c9cb29b35757b52c64a4cd0c542154f2
parent d8c0ea2ca226465361557d68fbce0e4adc4e6406
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Thu,  5 Feb 2026 10:18:19 -0300

fix #10960

Diffstat:
Mpackages/merchant-backoffice-ui/src/hooks/transfer.ts | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/orders/details/DetailPage.tsx | 37++++++++++++++++++++-----------------
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx | 86+++++++++++--------------------------------------------------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx | 90+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
Mpackages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx | 63++++++++++++++++++++++++++++++++++++++++++++-------------------
Mpackages/merchant-backoffice-ui/src/settings.json | 2+-
Mpackages/taler-util/src/types-taler-merchant.ts | 59++++++++++++++++++++++++++++++++++++++++++++---------------
7 files changed, 185 insertions(+), 153 deletions(-)

diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.ts @@ -36,6 +36,7 @@ export interface InstanceConfirmedTransferFilter { payto_uri?: string; expected?: boolean; position?: string; + verified?: boolean; } export function revalidateInstanceIncomingTransfers() { 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 @@ -587,25 +587,25 @@ function PaidPage({ type: info.confirmed ? "wired-confirmed" : "wired-pending", }); }); - const totalFee = Amounts.add(totalRefunded, wireFee).amount - const shouldBeWired = Amounts.sub(amount, totalFee).amount - const notAllHasBeenWired = Amounts.cmp(totalWired, shouldBeWired) < 0 // FIXME: should be updated with the protocol see #10971 - const w_deadline = AbsoluteTime.fromProtocolTimestamp(order.contract_terms.wire_transfer_deadline) - if (w_deadline.t_ms !== "never") { - if (!AbsoluteTime.isExpired(w_deadline)) { + } + const totalFee = Amounts.add(totalRefunded, wireFee).amount + const shouldBeWired = Amounts.sub(amount, totalFee).amount + const notAllHasBeenWired = Amounts.cmp(totalWired, shouldBeWired) < 0 // FIXME: should be updated with the protocol see #10971 + const w_deadline = AbsoluteTime.fromProtocolTimestamp(order.contract_terms.wire_transfer_deadline) + if (w_deadline.t_ms !== "never") { + if (!AbsoluteTime.isExpired(w_deadline)) { + events.push({ + when: new Date(w_deadline.t_ms), + description: i18n.str`wire deadline`, + type: "wire-deadline", + }); + } else { + if (notAllHasBeenWired) { events.push({ when: new Date(w_deadline.t_ms), description: i18n.str`wire deadline`, - type: "wire-deadline", + type: "wired-overdue", }); - } else { - if (notAllHasBeenWired) { - events.push({ - when: new Date(w_deadline.t_ms), - description: i18n.str`wire deadline`, - type: "wired-overdue", - }); - } } } } @@ -648,8 +648,11 @@ function PaidPage({ <i18n.Translate>Paid</i18n.Translate> </div> {order.wired ? ( - <div class="tag is-success ml-4"> - <i18n.Translate>Wired</i18n.Translate> + hasUnconfirmedWireTransfer ? + <div class="tag is-warning ml-4"> + <i18n.Translate>Unconfirmed</i18n.Translate> + </div> : <div class="tag is-info ml-4"> + <i18n.Translate>Confirmed</i18n.Translate> </div> ) : null} {order.refunded ? ( diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/ListPage.tsx @@ -25,6 +25,7 @@ import { h, VNode } from "preact"; import { FormProvider } from "../../../../components/form/FormProvider.js"; import { InputSelector } from "../../../../components/form/InputSelector.js"; import { CardTableIncoming, CardTableVerified } from "./Table.js"; +import { InputToggle } from "../../../../components/form/InputToggle.js"; const TALER_SCREEN_ID = 71; @@ -33,84 +34,20 @@ export interface Props { incomings: TalerMerchantApi.ExpectedTransferDetails[]; onLoadMoreBefore?: () => void; onLoadMoreAfter?: () => void; - onShowVerified: () => void; - onShowUnverified: () => void; - isVerifiedTransfers?: boolean; - isNonVerifiedTransfers?: boolean; - accounts: string[]; - onChangePayTo: (p?: string) => void; - payTo?: string; onSelectedToConfirm: (wid: TalerMerchantApi.ExpectedTransferDetails) => void; } export function ListPage({ - payTo, - onChangePayTo, transfers, incomings, onSelectedToConfirm, - accounts, onLoadMoreBefore, onLoadMoreAfter, - isNonVerifiedTransfers, - isVerifiedTransfers, - onShowUnverified, - onShowVerified, }: Props): VNode { - const form = { payto_uri: payTo }; - - const { i18n } = useTranslationContext(); return ( <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-10"> - <FormProvider - object={form} - valueHandler={(updater) => onChangePayTo(updater(form).payto_uri)} - > - <InputSelector - name="payto_uri" - label={i18n.str`Bank account`} - values={accounts} - fromStr={(d) => { - const idx = accounts.indexOf(d); - if (idx === -1) return undefined; - return d; - }} - placeholder={i18n.str`All accounts`} - tooltip={i18n.str`Filter by account address`} - /> - </FormProvider> - </div> - <div class="column" /> - </div> - <div class="tabs"> - <ul> - <li class={isNonVerifiedTransfers ? "is-active" : ""}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`Only display transfers that have already been transferred to your bank account by the payment service provider.`} - > - <a onClick={onShowUnverified}> - <i18n.Translate>Incoming</i18n.Translate> - </a> - </div> - </li> - <li class={isVerifiedTransfers ? "is-active" : ""}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`Only show wire transfers confirmed by the merchant`} - > - <a onClick={onShowVerified}> - <i18n.Translate>Verified</i18n.Translate> - </a> - </div> - </li> - </ul> - </div> - {isNonVerifiedTransfers ? ( + {!incomings.length ? undefined : ( <CardTableIncoming transfers={incomings.map((o) => ({ ...o, @@ -120,16 +57,17 @@ export function ListPage({ onLoadMoreAfter={onLoadMoreAfter} onSelectedToConfirm={onSelectedToConfirm} /> - ) : ( - <CardTableVerified - transfers={transfers.map((o) => ({ - ...o, - id: String(o.wtid), - }))} - onLoadMoreBefore={onLoadMoreBefore} - onLoadMoreAfter={onLoadMoreAfter} - /> )} + {/* // ) : ( */} + <CardTableVerified + transfers={transfers.map((o) => ({ + ...o, + id: String(o.wtid), + }))} + onLoadMoreBefore={onLoadMoreBefore} + onLoadMoreAfter={onLoadMoreAfter} + /> + {/* // )} */} </section> ); } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/Table.tsx @@ -20,7 +20,10 @@ */ import { TalerMerchantApi } from "@gnu-taler/taler-util"; -import { useTranslationContext } from "@gnu-taler/web-util/browser"; +import { + ButtonBetterBulma, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; import { format } from "date-fns"; import { h, VNode } from "preact"; import { WithId } from "../../../../declaration.js"; @@ -51,9 +54,9 @@ export function CardTableIncoming({ <header class="card-header"> <p class="card-header-title"> <span class="icon"> - <i class="mdi mdi-arrow-left-right" /> + <i class="mdi mdi-star" /> </span> - <i18n.Translate>Incoming wire transfers</i18n.Translate> + <i18n.Translate>New wire transfers</i18n.Translate> </p> </header> <div class="card-content"> @@ -75,21 +78,15 @@ export function CardTableIncoming({ <thead> <tr> <th> - <i18n.Translate>ID</i18n.Translate> - </th> - <th> - <i18n.Translate>Expected credit</i18n.Translate> - </th> - <th> - <i18n.Translate>Confirmed</i18n.Translate> + <i18n.Translate>Date</i18n.Translate> </th> <th> - <i18n.Translate>Validated</i18n.Translate> + <i18n.Translate>Amount</i18n.Translate> </th> <th> - <i18n.Translate>Executed on</i18n.Translate> + <i18n.Translate>ID</i18n.Translate> </th> - {/* <th /> */} + <th></th> </tr> </thead> <tbody> @@ -97,19 +94,15 @@ export function CardTableIncoming({ return ( <tr key={i.id} - style={{ - cursor: !i.confirmed ? "pointer" : undefined, - }} - onClick={ - !i.confirmed - ? () => onSelectedToConfirm(i) - : undefined - } + // style={{ + // cursor: !i.confirmed ? "pointer" : undefined, + // }} + // onClick={ + // !i.confirmed + // ? () => onSelectedToConfirm(i) + // : undefined + // } > - <td title={i.wtid}>{i.wtid.substring(0, 16)}...</td> - <td>{i.expected_credit_amount}</td> - <td>{i.confirmed ? i18n.str`yes` : i18n.str`no`}</td> - <td>{i.validated ? i18n.str`yes` : i18n.str`no`}</td> <td> {i.execution_time ? i.execution_time.t_s == "never" @@ -120,6 +113,28 @@ export function CardTableIncoming({ ) : i18n.str`unknown`} </td> + <td> + {i.expected_credit_amount ?? ( + <span> + <i18n.Translate> + To be determined. + </i18n.Translate> + </span> + )} + </td> + <td title={i.wtid}>{i.wtid}</td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <a + class="button is-info is-small has-tooltip-left" + // type="button" + data-tooltip={i18n.str`Delete selected scheduled report from the database`} + onClick={() => onSelectedToConfirm(i)} + > + <i18n.Translate>Confirm</i18n.Translate> + </a> + </div> + </td> </tr> ); })} @@ -166,7 +181,9 @@ export function CardTableVerified({ <span class="icon"> <i class="mdi mdi-arrow-left-right" /> </span> - <i18n.Translate>Confirmed wire transfers into bank account</i18n.Translate> + <i18n.Translate> + Confirmed wire transfers into bank account + </i18n.Translate> </p> </header> <div class="card-content"> @@ -196,6 +213,9 @@ export function CardTableVerified({ <th> <i18n.Translate>ID</i18n.Translate> </th> + <th> + <i18n.Translate>Expected</i18n.Translate> + </th> </tr> </thead> <tbody> @@ -213,7 +233,23 @@ export function CardTableVerified({ : i18n.str`unknown`} </td> <td>{i.credit_amount}</td> - <td title={i.wtid} style={{wordBreak:"break-word"}}>{i.wtid}</td> + <td + title={i.wtid} + style={{ wordBreak: "break-word" }} + > + {i.wtid} + </td> + <td> + {i.expected ? ( + <span class="icon"> + <i class="mdi mdi-check" /> + </span> + ) : ( + <span class="icon"> + <i class="mdi mdi-close" /> + </span> + )} + </td> </tr> ); })} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/list/index.tsx @@ -24,9 +24,10 @@ import { HttpStatusCode, TalerError, TransferDetails, - assertUnreachable + assertUnreachable, } from "@gnu-taler/taler-util"; import { + LocalNotificationBannerBulma, PaginatedResult, useLocalNotificationBetter, useTranslationContext, @@ -51,6 +52,9 @@ import { import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { ListPage } from "./ListPage.js"; +import { InputToggle } from "../../../../components/form/InputToggle.js"; +import { FormProvider } from "../../../../components/form/FormProvider.js"; +import { InputSelector } from "../../../../components/form/InputSelector.js"; interface Props { // onCreate: () => void; @@ -75,7 +79,7 @@ export default function ListTransfer({}: Props): VNode { !instance || instance instanceof TalerError || instance.type === "fail" ? [] : instance.body.accounts.map((a) => a.payto_uri); - const [form, setForm] = useState<Form>({ payto_uri: "", verified: false }); + const [form, setForm] = useState<Form>({ payto_uri: "" }); const [selected, setSelected] = useState<ExpectedTransferDetails>(); const { i18n } = useTranslationContext(); const [notification, safeFunctionHandler] = useLocalNotificationBetter(); @@ -102,7 +106,7 @@ export default function ListTransfer({}: Props): VNode { ], ); confirm.onSuccess = () => { - setSelected(undefined) + setSelected(undefined); }; confirm.onFail = (fail) => { switch (fail.case) { @@ -117,18 +121,14 @@ export default function ListTransfer({}: Props): VNode { } }; - // const isVerifiedTransfers = form.verified === true; - // const isNonVerifiedTransfers = form.verified === false; - // const isAllTransfers = form.verified === undefined; - let incoming: PaginatedResult<ExpectedTransferDetails[]>; { const result = useInstanceIncomingTransfers( { position, payto_uri: form.payto_uri === "" ? undefined : form.payto_uri, - // verified: form.verified, - // confirmed: form.confirmed, + verified: form.verified, + confirmed: false, }, (id) => setPosition(id), ); @@ -158,6 +158,7 @@ export default function ListTransfer({}: Props): VNode { position, payto_uri: form.payto_uri === "" ? undefined : form.payto_uri, // expected: form.expected, + verified: form.verified, }, (id) => setPosition(id), ); @@ -184,7 +185,40 @@ export default function ListTransfer({}: Props): VNode { const show = form.verified ? confirmed : incoming; return ( <Fragment> - {/* <LocalNotificationBannerBulma notification={notification} /> */} + <LocalNotificationBannerBulma notification={notification} /> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column" /> + <div class="column is-10"> + <FormProvider + object={form} + valueHandler={setForm} + > + <InputSelector + name="payto_uri" + label={i18n.str`Bank account`} + values={accounts} + fromStr={(d) => { + const idx = accounts.indexOf(d); + if (idx === -1) return undefined; + return d; + }} + placeholder={i18n.str`All accounts`} + tooltip={i18n.str`Filter by account address`} + /> + <InputToggle + label={i18n.str`Verified`} + tooltip={i18n.str`A wire transfer is verified if match the amount of all orders being settled.`} + name="verified" + threeState + /> + </FormProvider> + </div> + <div class="column" /> + </div> + </section> + {!selected ? undefined : ( <ConfirmModal label={i18n.str`I have received the wire transfer`} @@ -230,7 +264,6 @@ export default function ListTransfer({}: Props): VNode { </ConfirmModal> )} <ListPage - accounts={accounts} transfers={confirmed.body} incomings={incoming.body} onSelectedToConfirm={(d) => { @@ -238,14 +271,6 @@ export default function ListTransfer({}: Props): VNode { }} onLoadMoreBefore={show.loadFirst} onLoadMoreAfter={show.loadNext} - onShowUnverified={() => setFilter(false)} - onShowVerified={() => setFilter(true)} - // onShowAll={() => setFilter(undefined)} - // isAllTransfers={isAllTransfers} - isVerifiedTransfers={form.verified} - isNonVerifiedTransfers={!form.verified} - payTo={form.payto_uri} - onChangePayTo={(p) => setForm((v) => ({ ...v, payto_uri: p }))} /> </Fragment> ); diff --git a/packages/merchant-backoffice-ui/src/settings.json b/packages/merchant-backoffice-ui/src/settings.json @@ -1,4 +1,4 @@ { "backendBaseURL": "https://merchant.taler/", - "supportedWireMethods": ["iban"] + "isTestingEnvironment": false } diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts @@ -1429,28 +1429,27 @@ export enum KycStatusLongPollingReason { } export type KycLongPollingReason = - KycLongPollingReasonWaitForStateEnter + | KycLongPollingReasonWaitForStateEnter | KycLongPollingReasonWaitForStateExit | KycLongPollingReasonWaitForStateChange; export type KycEtag = string; export type KycLongPollingReasonWaitForStateEnter = { - type: "state-enter", + type: "state-enter"; status: MerchantAccountKycStatus; - timeout: number, -} + timeout: number; +}; export type KycLongPollingReasonWaitForStateExit = { - type: "state-exit", + type: "state-exit"; status: MerchantAccountKycStatus; - timeout: number, -} + timeout: number; +}; export type KycLongPollingReasonWaitForStateChange = { - type: "state-change", + type: "state-change"; etag: KycEtag; - timeout: number, -} - + timeout: number; +}; export interface GetKycStatusRequestParams { // If specified, the KYC check should return @@ -1481,14 +1480,14 @@ export interface GetKycStatusRequestParams { reason?: KycStatusLongPollingReason; /** - * + * */ longpoll?: KycLongPollingReason; } export type OrderDetailLongPollingReason = { etag: string; timeout: number; -} +}; export interface GetOtpDeviceRequestParams { // Timestamp in seconds to use when calculating // the current OTP code of the device. Since protocol v10. @@ -1503,13 +1502,13 @@ export interface GetOrderRequestParams { sessionId?: string; /** * @deprecated use longpoll - * Timeout in milliseconds to wait for a payment if + * Timeout in milliseconds to wait for a payment if * the answer would otherwise be negative (long polling). */ timeout?: number; /** - * + * */ longpoll?: OrderDetailLongPollingReason; @@ -3173,6 +3172,17 @@ export interface ExpectedTransferDetails { // (a matching entry exists in /private/transfers) confirmed: boolean; + // List of orders that are settled by this wire + // transfer according to the exchange. Only + // available if last_http_status is 200. + // @since **v26** + reconciliation_details?: ExchangeTransferReconciliationDetails[]; + + // Wire fee paid by the merchant. Only + // available if last_http_status is 200. + // @since **v26** + wire_fee: AmountString; + // Last HTTP status we received from the exchange, 0 for // none (incl. timeout) last_http_status: Integer; @@ -3184,6 +3194,25 @@ export interface ExpectedTransferDetails { last_error_detail?: string; } +interface ExchangeTransferReconciliationDetails { + + // ID of the order for which these are the + // reconciliation details. + order_id: string; + + // Remaining deposit total to be paid, + // that is the total amount of the order + // minus any refunds that were granted. + // The actual amount to be wired is this + // amount minus deposit_fee and (overall) + // minus the wire_fee of the transfer. + remaining_deposit: AmountString; + + // Deposit fees paid to the exchange for this order. + deposit_fee: AmountString; + +} + export interface OtpDeviceAddDetails { // Device ID to use. otp_device_id: string;