commit 07b79506c9cb29b35757b52c64a4cd0c542154f2
parent d8c0ea2ca226465361557d68fbce0e4adc4e6406
Author: Sebastian <sebasjm@taler-systems.com>
Date: Thu, 5 Feb 2026 10:18:19 -0300
fix #10960
Diffstat:
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;