commit 3c86919f17b715e22ade34a040e26932beccf8f9 parent 199886eb614548526fe947e49481af50c8a78327 Author: Sebastian <sebasjm@gmail.com> Date: Sun, 23 Nov 2025 12:22:48 -0300 incoming wire transfers Diffstat:
14 files changed, 447 insertions(+), 551 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx @@ -74,7 +74,6 @@ import { revalidateTokenFamilies, revalidateTokenFamilyDetails, } from "./hooks/tokenfamily.js"; -import { revalidateInstanceTransfers } from "./hooks/transfer.js"; import { revalidateInstanceWebhooks, revalidateWebhookDetails, @@ -85,6 +84,10 @@ import { buildDefaultBackendBaseURL, fetchSettings, } from "./settings.js"; +import { + revalidateInstanceConfirmedTransfers, + revalidateInstanceIncomingTransfers, +} from "./hooks/transfer.js"; const WITH_LOCAL_STORAGE_CACHE = false; export function Application(): VNode { @@ -321,11 +324,17 @@ const swrCacheEvictor = new (class return; } case TalerMerchantInstanceCacheEviction.CREATE_TRANSFER: { - await Promise.all([revalidateInstanceTransfers()]); + await Promise.all([ + revalidateInstanceConfirmedTransfers(), + revalidateInstanceIncomingTransfers(), + ]); return; } case TalerMerchantInstanceCacheEviction.DELETE_TRANSFER: { - await Promise.all([revalidateInstanceTransfers()]); + await Promise.all([ + revalidateInstanceConfirmedTransfers(), + revalidateInstanceIncomingTransfers(), + ]); return; } case TalerMerchantInstanceCacheEviction.CREATE_DEVICE: { diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx @@ -72,7 +72,6 @@ import TemplateUsePage from "./paths/instance/templates/use/index.js"; import TokenFamilyCreatePage from "./paths/instance/tokenfamilies/create/index.js"; import TokenFamilyListPage from "./paths/instance/tokenfamilies/list/index.js"; import TokenFamilyUpdatePage from "./paths/instance/tokenfamilies/update/index.js"; -import TransferCreatePage from "./paths/instance/transfers/create/index.js"; import TransferListPage from "./paths/instance/transfers/list/index.js"; import InstanceUpdatePage, { AdminUpdate as InstanceAdminUpdatePage, @@ -520,20 +519,7 @@ export function Routing(_p: Props): VNode { <Route path={InstancePaths.transfers_list} component={TransferListPage} - // onCreate={() => { - // route(InstancePaths.transfers_new); - // }} /> - {/* <Route - path={InstancePaths.transfers_new} - component={TransferCreatePage} - onConfirm={() => { - route(InstancePaths.transfers_list); - }} - onBack={() => { - route(InstancePaths.transfers_list); - }} - /> */} {/* * * Token family pages */} diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.test.ts @@ -27,9 +27,9 @@ import { import * as tests from "@gnu-taler/web-util/testing"; import { expect } from "chai"; import { ApiMockEnvironment } from "./testing.js"; -import { useInstanceTransfers } from "./transfer.js"; import { API_INFORM_TRANSFERS, API_LIST_TRANSFERS } from "./urls.js"; import { useMerchantApiContext } from "@gnu-taler/web-util/browser"; +import { useInstanceConfirmedTransfers } from "./transfer.js"; describe("transfer api interaction with listing", () => { it("should evict cache when informing a transfer", async () => { @@ -48,7 +48,7 @@ describe("transfer api interaction with listing", () => { const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useInstanceTransfers({}, moveCursor); + const query = useInstanceConfirmedTransfers({}, moveCursor); const { lib: api } = useMerchantApiContext(); return { query, api }; }, @@ -86,12 +86,12 @@ describe("transfer api interaction with listing", () => { }, }); - api.instance.informWireTransfer(undefined, { - wtid: "3", - credit_amount: "EUR:1" as AmountString, - exchange_url: "exchange.url", - payto_uri: "payto://" as PaytoString, - }); + // api.instance.informWireTransfer(undefined, { + // wtid: "3", + // credit_amount: "EUR:1" as AmountString, + // exchange_url: "exchange.url", + // payto_uri: "payto://" as PaytoString, + // }); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ @@ -130,7 +130,7 @@ describe("transfer listing pagination", () => { }; const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useInstanceTransfers( + const query = useInstanceConfirmedTransfers( { payto_uri: "payto://" }, moveCursor, ); @@ -194,7 +194,7 @@ describe("transfer listing pagination", () => { const hookBehavior = await tests.hookBehaveLikeThis( () => { - const query = useInstanceTransfers( + const query = useInstanceConfirmedTransfers( { payto_uri: "payto://", position: "1" }, moveCursor, ); diff --git a/packages/merchant-backoffice-ui/src/hooks/transfer.ts b/packages/merchant-backoffice-ui/src/hooks/transfer.ts @@ -26,36 +26,42 @@ import { useSessionContext } from "../context/session.js"; import { buildPaginatedResult } from "@gnu-taler/web-util/browser"; const useSWR = _useSWR as unknown as SWRHook; -export interface InstanceTransferFilter { +export interface InstanceIncomingTransferFilter { payto_uri?: string; verified?: boolean; + confirmed?: boolean; + position?: string; +} +export interface InstanceConfirmedTransferFilter { + payto_uri?: string; + expected?: boolean; position?: string; } -export function revalidateInstanceTransfers() { +export function revalidateInstanceIncomingTransfers() { return mutate( - (key) => Array.isArray(key) && key[key.length - 1] === "listWireTransfers", + (key) => Array.isArray(key) && key[key.length - 1] === "listIncomingWireTransfers", undefined, { revalidate: true }, ); } -export function useInstanceTransfers( - args?: InstanceTransferFilter, +export function useInstanceIncomingTransfers( + args?: InstanceIncomingTransferFilter, updatePosition: (id: string | undefined) => void = () => {}, ) { const { state, lib } = useSessionContext(); - // const [offset, setOffset] = useState<string | undefined>(args?.position); - - async function fetcher([token, o, p, v]: [ + async function fetcher([token, o, p, v, c]: [ AccessToken, string, string, boolean, + boolean, ]) { - return await lib.instance.listWireTransfers(token, { + return await lib.instance.listIncomingWireTransfers(token, { paytoURI: p, verified: v, + confirmed: c, limit: PAGINATED_LIST_REQUEST, offset: o, order: "dec", @@ -63,7 +69,7 @@ export function useInstanceTransfers( } const { data, error } = useSWR< - TalerMerchantManagementResultByMethod<"listWireTransfers">, + TalerMerchantManagementResultByMethod<"listIncomingWireTransfers">, TalerHttpError >( [ @@ -71,7 +77,62 @@ export function useInstanceTransfers( args?.position, args?.payto_uri, args?.verified, - "listWireTransfers", + args?.confirmed, + "listIncomingWireTransfers", + ], + fetcher, + ); + + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; + + return buildPaginatedResult( + data.body.transfers, + args?.position, + updatePosition, + (d) => String(d.transfer_serial_id), + PAGINATED_LIST_REQUEST, + ); +} +export function revalidateInstanceConfirmedTransfers() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listConfirmedWireTransfers", + undefined, + { revalidate: true }, + ); +} +export function useInstanceConfirmedTransfers( + args?: InstanceConfirmedTransferFilter, + updatePosition: (id: string | undefined) => void = () => {}, +) { + const { state, lib } = useSessionContext(); + + async function fetcher([token, o, p, e]: [ + AccessToken, + string, + string, + boolean, + ]) { + return await lib.instance.listConfirmedWireTransfers(token, { + paytoURI: p, + expected: e, + limit: PAGINATED_LIST_REQUEST, + offset: o, + order: "dec", + }); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listConfirmedWireTransfers">, + TalerHttpError + >( + [ + state.token, + args?.position, + args?.payto_uri, + args?.expected, + "listConfirmedWireTransfers", ], fetcher, ); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx @@ -52,6 +52,8 @@ import { useSessionContext } from "../../../../context/session.js"; import { undefinedIfEmpty } from "../../../../utils/table.js"; import { safeConvertURL } from "../update/UpdatePage.js"; import { TestRevenueErrorType, testRevenueAPI } from "./index.js"; +import { FragmentPersonaFlag } from "../../../../components/menu/SideBar.js"; +import { UIElement, usePreference } from "../../../../hooks/preference.js"; type Entity = TalerMerchantApi.AccountAddDetails & { verified?: boolean; @@ -65,8 +67,8 @@ interface Props { export function CreatePage({ onCreated, onBack }: Props): VNode { const { i18n } = useTranslationContext(); - const [{ showDebugInfo }] = useCommonPreferences(); - const accountAuthType = showDebugInfo + const [{ persona }] = usePreference(); + const accountAuthType = persona === "developer" ? ["none", "basic", "bearer"] : ["none", "basic"]; @@ -239,79 +241,76 @@ export function CreatePage({ onCreated, onBack }: Props): VNode { name="payto_uri" label={i18n.str`Account details`} /> - {!showDebugInfo ? undefined : ( - <Fragment> - <div class="message-body" style={{ marginBottom: 10 }}> - <p> - <i18n.Translate> - If the bank supports Taler Revenue API then you can add - the endpoint URL below to keep the revenue information - in sync. - </i18n.Translate> - </p> - </div> - <Input<Entity> - name="credit_facade_url" - label={i18n.str`Endpoint URL`} - help="https://bank.demo.taler.net/accounts/${USERNAME}/taler-revenue/" - expand - tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`} - /> - <InputSelector - name="credit_facade_credentials.type" - label={i18n.str`Auth type`} - tooltip={i18n.str`Choose the authentication type for the account info URL`} - values={accountAuthType} - toStr={(str) => { - if (str === "none") - return i18n.str`Without authentication`; - if (str === "bearer") return i18n.str`With token`; - return "With username and password"; - }} - /> - {state.credit_facade_credentials?.type === "basic" ? ( - <Fragment> - <Input - name="credit_facade_credentials.username" - label={i18n.str`Username`} - tooltip={i18n.str`Username to access the account information.`} - /> - <Input - name="credit_facade_credentials.password" - inputType="password" - label={i18n.str`Password`} - tooltip={i18n.str`Password to access the account information.`} - /> - </Fragment> - ) : undefined} - {state.credit_facade_credentials?.type === "bearer" ? ( - <Fragment> - <Input - name="credit_facade_credentials.token" - label={i18n.str`Token`} - inputType="password" - tooltip={i18n.str`Access token to access the account information.`} - /> - </Fragment> - ) : undefined} - <InputToggle<Entity> - label={i18n.str`Match`} - tooltip={i18n.str`Check if the information matches the server info.`} - name="verified" - readonly - threeState - side={ - <ButtonBetterBulma - class="button is-info" - data-tooltip={i18n.str`Compare info from server with account form`} - onClick={test} - > - <i18n.Translate>Test</i18n.Translate> - </ButtonBetterBulma> - } - /> - </Fragment> - )} + <FragmentPersonaFlag point={UIElement.action_useRevenueApi}> + <div class="message-body" style={{ marginBottom: 10 }}> + <p> + <i18n.Translate> + If the bank supports Taler Revenue API then you can add + the endpoint URL below to keep the revenue information in + sync. + </i18n.Translate> + </p> + </div> + <Input<Entity> + name="credit_facade_url" + label={i18n.str`Endpoint URL`} + help="https://bank.demo.taler.net/accounts/${USERNAME}/taler-revenue/" + expand + tooltip={i18n.str`From where the merchant can download information about incoming wire transfers to this account`} + /> + <InputSelector + name="credit_facade_credentials.type" + label={i18n.str`Auth type`} + tooltip={i18n.str`Choose the authentication type for the account info URL`} + values={accountAuthType} + toStr={(str) => { + if (str === "none") return i18n.str`Without authentication`; + if (str === "bearer") return i18n.str`With token`; + return "With username and password"; + }} + /> + {state.credit_facade_credentials?.type === "basic" ? ( + <Fragment> + <Input + name="credit_facade_credentials.username" + label={i18n.str`Username`} + tooltip={i18n.str`Username to access the account information.`} + /> + <Input + name="credit_facade_credentials.password" + inputType="password" + label={i18n.str`Password`} + tooltip={i18n.str`Password to access the account information.`} + /> + </Fragment> + ) : undefined} + {state.credit_facade_credentials?.type === "bearer" ? ( + <Fragment> + <Input + name="credit_facade_credentials.token" + label={i18n.str`Token`} + inputType="password" + tooltip={i18n.str`Access token to access the account information.`} + /> + </Fragment> + ) : undefined} + <InputToggle<Entity> + label={i18n.str`Match`} + tooltip={i18n.str`Check if the information matches the server info.`} + name="verified" + readonly + threeState + side={ + <ButtonBetterBulma + class="button is-info" + data-tooltip={i18n.str`Compare info from server with account form`} + onClick={test} + > + <i18n.Translate>Test</i18n.Translate> + </ButtonBetterBulma> + } + /> + </FragmentPersonaFlag> </FormProvider> <div class="buttons is-right mt-5"> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/update/UpdatePage.tsx @@ -31,6 +31,7 @@ import { import { ButtonBetterBulma, LocalNotificationBannerBulma, + undefinedIfEmpty, useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; @@ -159,7 +160,7 @@ export function UpdatePage({ template, onUpdated, onBack }: Props): VNode { const parsedPrice = !state.amount ? undefined : Amounts.parse(state.amount); - const errors: FormErrors<Entity> = { + const errors: FormErrors<Entity> | undefined = undefinedIfEmpty({ description: !state.description ? i18n.str`Required` : undefined, summary: !state.summary ? state.summary_editable @@ -188,11 +189,7 @@ export function UpdatePage({ template, onUpdated, onBack }: Props): VNode { : state.pay_duration.d_ms < 1000 // less than one second ? i18n.str`Too short` : undefined, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as Record<string, unknown>)[k] !== undefined, - ); + }); const zero = Amounts.stringify( Amounts.zeroOfCurrency(state.currency ?? default_currency), @@ -348,7 +345,7 @@ export function UpdatePage({ template, onUpdated, onBack }: Props): VNode { tooltip={i18n.str`How much time has the customer to complete the payment once the order was created.`} /> </FragmentPersonaFlag> - + {!deviceList.length ? ( <ComponentPersonaFlag Comp={TextField} @@ -389,7 +386,7 @@ export function UpdatePage({ template, onUpdated, onBack }: Props): VNode { )} <ButtonBetterBulma data-tooltip={ - hasErrors + errors !== undefined ? i18n.str`Please complete the marked fields` : i18n.str`Confirm operation` } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/templates/use/UsePage.tsx @@ -28,6 +28,7 @@ import { import { ButtonBetterBulma, LocalNotificationBannerBulma, + undefinedIfEmpty, useLocalNotificationBetter, useTranslationContext, } from "@gnu-taler/web-util/browser"; @@ -71,29 +72,23 @@ export function UsePage({ template.editable_defaults?.summary ?? template.template_contract.summary, }); - const errors: FormErrors<Entity> = { + const errors: FormErrors<Entity> | undefined = undefinedIfEmpty({ amount: !state.amount ? i18n.str`An amount is required` : undefined, summary: !state.summary ? i18n.str`An order summary is required` : undefined, - }; - - const hasErrors = Object.keys(errors).some( - (k) => (errors as any)[k] !== undefined, - ); + }); - /* - * if (template.template_contract.amount) { - delete state.amount; - } - if (template.template_contract.summary) { - delete state.summary; - } - + /** + * If the template already has fields then do not send those. */ + const details: UsingTemplateDetails = { + amount: template.template_contract.amount ? undefined : state.amount, + summary: template.template_contract.summary ? undefined : state.summary, + }; const useTemplate = safeFunctionHandler( lib.instance.useTemplateCreateOrder.bind(lib.instance), - !!errors ? undefined : [id, state as UsingTemplateDetails], + !!errors ? undefined : [id, details], ); useTemplate.onSuccess = (success) => { onOrderCreated(success.order_id); @@ -172,7 +167,7 @@ export function UsePage({ )} <ButtonBetterBulma data-tooltip={ - hasErrors + errors !== undefined ? i18n.str`Please complete the marked fields` : "confirm operation" } diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/Create.stories.tsx @@ -1,45 +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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { h, VNode, FunctionalComponent } from "preact"; -import { CreatePage as TestedComponent } from "./CreatePage.js"; - -export default { - title: "Pages/Transfer/Create", - component: TestedComponent, - argTypes: { - onUpdate: { action: "onUpdate" }, - onBack: { action: "onBack" }, - }, -}; - -function createExample<Props>( - Component: FunctionalComponent<Props>, - props: Partial<Props>, -) { - const r = (args: any) => <Component {...args} />; - r.args = props; - return r; -} - -export const Example = createExample(TestedComponent, { - accounts: ["payto://x-taler-bank/account1", "payto://x-taler-bank/account2"], -}); diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/CreatePage.tsx @@ -1,174 +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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { - AmountString, - HttpStatusCode, - TalerMerchantApi, - TransferInformation, -} from "@gnu-taler/taler-util"; -import { - ButtonBetterBulma, - LocalNotificationBannerBulma, - useLocalNotificationBetter, - useTranslationContext, -} from "@gnu-taler/web-util/browser"; -import { VNode, h } from "preact"; -import { useState } from "preact/hooks"; -import { - FormErrors, - FormProvider, -} from "../../../../components/form/FormProvider.js"; -import { Input } from "../../../../components/form/Input.js"; -import { InputCurrency } from "../../../../components/form/InputCurrency.js"; -import { InputSelector } from "../../../../components/form/InputSelector.js"; -import { - CROCKFORD_BASE32_REGEX, - URL_REGEX, -} from "../../../../utils/constants.js"; -import { undefinedIfEmpty } from "../../../../utils/table.js"; -import { useSessionContext } from "../../../../context/session.js"; - -type Entity = TalerMerchantApi.TransferInformation; - -export interface Props { - onCreated: () => void; - onBack?: () => void; - accounts: string[]; -} - -/** - * @deprecated this is going to be removed from the SPEC - * - * @param param0 - * @returns - */ -export function CreatePage({ accounts, onCreated, onBack }: Props): VNode { - const { i18n } = useTranslationContext(); - - const [state, setState] = useState<Partial<Entity>>({ - wtid: "", - // payto_uri: , - // exchange_url: 'http://exchange.taler:8081/', - credit_amount: `` as AmountString, - }); - - // <LocalNotificationBannerBulma notification={notification} /> - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - const { state: session, lib } = useSessionContext(); - const errors = undefinedIfEmpty<FormErrors<Entity>>({ - wtid: !state.wtid - ? i18n.str`Required` - : !CROCKFORD_BASE32_REGEX.test(state.wtid) - ? i18n.str`Please check the ID; it does not appear to be valid.` - : state.wtid.length !== 52 - ? i18n.str`Must have 52 characters, current ${state.wtid.length}` - : undefined, - payto_uri: !state.payto_uri ? i18n.str`Required` : undefined, - credit_amount: !state.credit_amount ? i18n.str`Required` : undefined, - exchange_url: !state.exchange_url - ? i18n.str`Required` - : !URL_REGEX.test(state.exchange_url) - ? i18n.str`URL doesn't have the right format` - : undefined, - }); - // const data = state as TransferInformation; - - // const create = safeFunctionHandler( - // lib.instance.informWireTransfer.bind(lib.instance), - // !session.token || !!errors ? undefined : [session.token, data], - // ); - // create.onSuccess = onCreated; - // create.onFail = (fail) => { - // switch (fail.case) { - // case HttpStatusCode.Unauthorized: - // return i18n.str`Unauthorized.`; - // case HttpStatusCode.NotFound: - // return i18n.str`Not found.`; - // case HttpStatusCode.Conflict: - // return i18n.str`Conflict.`; - // } - // }; - const hasErrors = errors !== undefined; - - return ( - <div> - <LocalNotificationBannerBulma notification={notification} /> - <section class="section is-main-section"> - <div class="columns"> - <div class="column" /> - <div class="column is-four-fifths"> - <FormProvider - object={state} - valueHandler={setState} - errors={errors} - > - <InputSelector - name="payto_uri" - label={i18n.str`Credited bank account`} - values={accounts} - placeholder={i18n.str`Select an account`} - tooltip={i18n.str`Bank account of the merchant where the payment was received`} - /> - <Input<Entity> - name="wtid" - label={i18n.str`Wire transfer ID`} - help="" - tooltip={i18n.str`Unique identifier of the wire transfer used by the exchange, must be 52 characters long`} - /> - <Input<Entity> - name="exchange_url" - label={i18n.str`Exchange URL`} - tooltip={i18n.str`Base URL of the exchange that made the transfer, should have been in the wire transfer subject`} - help="http://exchange.taler:8081/" - /> - <InputCurrency<Entity> - name="credit_amount" - label={i18n.str`Amount credited`} - tooltip={i18n.str`Actual amount that was wired to the merchant's bank account`} - /> - </FormProvider> - - <div class="buttons is-right mt-5"> - {onBack && ( - <button type="button"class="button" onClick={onBack}> - <i18n.Translate>Cancel</i18n.Translate> - </button> - )} - {/* <ButtonBetterBulma - data-tooltip={ - hasErrors - ? i18n.str`Please complete the marked fields` - : i18n.str`Confirm operation` - } - type="submit" - // onClick={create} - > - <i18n.Translate>Confirm</i18n.Translate> - </ButtonBetterBulma> */} - </div> - </div> - <div class="column" /> - </div> - </section> - </div> - ); -} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/transfers/create/index.tsx @@ -1,50 +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/> - */ - -/** - * - * @author Sebastian Javier Marchano (sebasjm) - */ - -import { TalerError, TalerMerchantApi } from "@gnu-taler/taler-util"; -import { Fragment, VNode, h } from "preact"; -import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; -import { CreatePage } from "./CreatePage.js"; - -export type Entity = TalerMerchantApi.TransferInformation; -interface Props { - onBack?: () => void; - onConfirm: () => void; -} - -/** - * @deprecated is going to be removed from the SPEC - * @param param0 - * @returns - */ -export default function CreateTransfer({ onConfirm, onBack }: Props): VNode { - const instance = useInstanceBankAccounts(); - const accounts = - !instance || instance instanceof TalerError || instance.type === "fail" - ? [] - : instance.body.accounts.map((a) => a.payto_uri); - - return ( - <> - <CreatePage onBack={onBack} accounts={accounts} onCreated={onConfirm} /> - </> - ); -} 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 @@ -30,12 +30,12 @@ export interface Props { transfers: TalerMerchantApi.TransferDetails[]; onLoadMoreBefore?: () => void; onLoadMoreAfter?: () => void; - onShowAll: () => void; + // onShowAll: () => void; onShowVerified: () => void; onShowUnverified: () => void; isVerifiedTransfers?: boolean; isNonVerifiedTransfers?: boolean; - isAllTransfers?: boolean; + // isAllTransfers?: boolean; accounts: string[]; onChangePayTo: (p?: string) => void; payTo?: string; @@ -52,10 +52,10 @@ export function ListPage({ accounts, onLoadMoreBefore, onLoadMoreAfter, - isAllTransfers, + // isAllTransfers, isNonVerifiedTransfers, isVerifiedTransfers, - onShowAll, + // onShowAll, onShowUnverified, onShowVerified, }: Props): VNode { @@ -89,13 +89,13 @@ export function ListPage({ </div> <div class="tabs"> <ul> - <li class={isAllTransfers ? "is-active" : ""}> + <li class={isNonVerifiedTransfers ? "is-active" : ""}> <div class="has-tooltip-right" - data-tooltip={i18n.str`Remove all filters`} + data-tooltip={i18n.str`Only show wire transfers claimed by the exchange`} > - <a onClick={onShowAll}> - <i18n.Translate>All</i18n.Translate> + <a onClick={onShowUnverified}> + <i18n.Translate>Incoming</i18n.Translate> </a> </div> </li> @@ -109,16 +109,6 @@ export function ListPage({ </a> </div> </li> - <li class={isNonVerifiedTransfers ? "is-active" : ""}> - <div - class="has-tooltip-right" - data-tooltip={i18n.str`Only show wire transfers claimed by the exchange`} - > - <a onClick={onShowUnverified}> - <i18n.Translate>Unverified</i18n.Translate> - </a> - </div> - </li> </ul> </div> <CardTable 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 @@ -22,18 +22,23 @@ import { HttpStatusCode, TalerError, + TransferDetails, assertUnreachable, } from "@gnu-taler/taler-util"; import { LocalNotificationBannerBulma, - useLocalNotificationBetter + PaginatedResult, + useLocalNotificationBetter, } from "@gnu-taler/web-util/browser"; import { Fragment, VNode, h } from "preact"; import { useEffect, useState } from "preact/hooks"; import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; import { Loading } from "../../../../components/exception/loading.js"; import { useInstanceBankAccounts } from "../../../../hooks/bank.js"; -import { useInstanceTransfers } from "../../../../hooks/transfer.js"; +import { + useInstanceConfirmedTransfers, + useInstanceIncomingTransfers +} from "../../../../hooks/transfer.js"; import { LoginPage } from "../../../login/index.js"; import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; import { ListPage } from "./ListPage.js"; @@ -42,11 +47,13 @@ interface Props { // onCreate: () => void; } interface Form { + expected?: boolean; + confirmed?: boolean; verified?: boolean; payto_uri?: string; } -export default function ListTransfer({ }: Props): VNode { +export default function ListTransfer({}: Props): VNode { const setFilter = (s?: boolean) => setForm({ ...form, verified: s }); // const { i18n } = useTranslationContext(); @@ -54,6 +61,7 @@ export default function ListTransfer({ }: Props): VNode { const [position, setPosition] = useState<string | undefined>(undefined); + // const [notification, safeFunctionHandler] = useLocalNotificationBetter(); const instance = useInstanceBankAccounts(); const accounts = !instance || instance instanceof TalerError || instance.type === "fail" @@ -68,66 +76,86 @@ export default function ListTransfer({ }: Props): VNode { } }, [shoulUseDefaultAccount]); - const isVerifiedTransfers = form.verified === true; - const isNonVerifiedTransfers = form.verified === false; - const isAllTransfers = form.verified === undefined; + // const isVerifiedTransfers = form.verified === true; + // const isNonVerifiedTransfers = form.verified === false; + // const isAllTransfers = form.verified === undefined; - const result = useInstanceTransfers( - { - position, - payto_uri: form.payto_uri === "" ? undefined : form.payto_uri, - verified: form.verified, - }, - (id) => setPosition(id), - ); - // <LocalNotificationBannerBulma notification={notification} /> - const [notification, safeFunctionHandler] = useLocalNotificationBetter(); - // const remove = safeFunctionHandler( - // lib.instance.deleteWireTransfer.bind(lib.instance), - // ); - if (!result) return <Loading />; - if (result instanceof TalerError) { - return <ErrorLoadingMerchant error={result} />; - } - if (result.type === "fail") { - switch (result.case) { - case HttpStatusCode.Unauthorized: { - return <LoginPage />; - } - case HttpStatusCode.NotFound: { - return <NotFoundPageOrAdminCreate />; + let incoming: PaginatedResult<TransferDetails[]>; + { + const result = useInstanceIncomingTransfers( + { + position, + payto_uri: form.payto_uri === "" ? undefined : form.payto_uri, + // verified: form.verified, + // confirmed: form.confirmed, + }, + (id) => setPosition(id), + ); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.Unauthorized: { + return <LoginPage />; + } + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + default: { + assertUnreachable(result); + } } - default: { - assertUnreachable(result); + } + incoming = result + } + let confirmed: PaginatedResult<TransferDetails[]>; + { + const result = useInstanceConfirmedTransfers( + { + position, + payto_uri: form.payto_uri === "" ? undefined : form.payto_uri, + // expected: form.expected, + }, + (id) => setPosition(id), + ); + if (!result) return <Loading />; + if (result instanceof TalerError) { + return <ErrorLoadingMerchant error={result} />; + } + if (result.type === "fail") { + switch (result.case) { + case HttpStatusCode.Unauthorized: { + return <LoginPage />; + } + case HttpStatusCode.NotFound: { + return <NotFoundPageOrAdminCreate />; + } + default: { + assertUnreachable(result); + } } } + confirmed = result } + const show = form.verified ? confirmed : incoming; return ( <Fragment> - <LocalNotificationBannerBulma notification={notification} /> + {/* <LocalNotificationBannerBulma notification={notification} /> */} <ListPage accounts={accounts} - transfers={result.body} - onLoadMoreBefore={result.loadFirst} - onLoadMoreAfter={result.loadNext} - // onCreate={onCreate} - // onDelete={async (transfer) => { - // (!session.token - // ? remove - // : remove.withArgs( - // session.token, - // String(transfer.transfer_serial_id), - // ) - // ).call(); - // }} - onShowAll={() => setFilter(undefined)} + transfers={show.body} + onLoadMoreBefore={show.loadFirst} + onLoadMoreAfter={show.loadNext} + // onShowAll={() => setFilter(undefined)} onShowUnverified={() => setFilter(false)} onShowVerified={() => setFilter(true)} - isAllTransfers={isAllTransfers} - isVerifiedTransfers={isVerifiedTransfers} - isNonVerifiedTransfers={isNonVerifiedTransfers} + // isAllTransfers={isAllTransfers} + isVerifiedTransfers={form.verified} + isNonVerifiedTransfers={!form.verified} payTo={form.payto_uri} onChangePayTo={(p) => setForm((v) => ({ ...v, payto_uri: p }))} /> diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts @@ -1473,27 +1473,27 @@ export class TalerMerchantInstanceHttpClient { ) { const url = new URL(`private/orders`, this.baseUrl); - if (params.date && !AbsoluteTime.isNever(params.date)) { - const time = AbsoluteTime.toProtocolTimestamp(params.date) - url.searchParams.set("date_s", String(time.t_s)); - } - if (params.fulfillmentUrl) { - url.searchParams.set("fulfillment_url", params.fulfillmentUrl); - } if (params.paid !== undefined) { url.searchParams.set("paid", params.paid ? "YES" : "NO"); } if (params.refunded !== undefined) { url.searchParams.set("refunded", params.refunded ? "YES" : "NO"); } - if (params.sessionId) { - url.searchParams.set("session_id", params.sessionId); + if (params.wired !== undefined) { + url.searchParams.set("wired", params.wired ? "YES" : "NO"); + } + if (params.date && !AbsoluteTime.isNever(params.date)) { + const time = AbsoluteTime.toProtocolTimestamp(params.date); + url.searchParams.set("date_s", String(time.t_s)); } if (params.timeout) { - url.searchParams.set("timeout", String(params.timeout)); + url.searchParams.set("timeout_ms", String(params.timeout)); } - if (params.wired !== undefined) { - url.searchParams.set("wired", params.wired ? "YES" : "NO"); + if (params.sessionId) { + url.searchParams.set("session_id", params.sessionId); + } + if (params.fulfillmentUrl) { + url.searchParams.set("fulfillment_url", params.fulfillmentUrl); } if (params.summary) { url.searchParams.set("summary_filter", params.summary); @@ -1715,38 +1715,82 @@ export class TalerMerchantInstanceHttpClient { // Wire Transfer // + // /** + // * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-transfers + // * @deprecated + // */ + // async informWireTransfer( + // token: AccessToken | undefined, + // body: TalerMerchantApi.TransferInformation, + // ) { + // const url = new URL(`private/transfers`, this.baseUrl); + + // const headers: Record<string, string> = {}; + // if (token) { + // headers.Authorization = makeBearerTokenAuthHeader(token); + // } + // const resp = await this.httpLib.fetch(url.href, { + // method: "POST", + // body, + // headers, + // }); + + // switch (resp.status) { + // case HttpStatusCode.NoContent: { + // this.cacheEvictor.notifySuccess( + // TalerMerchantInstanceCacheEviction.CREATE_TRANSFER, + // ); + // return opEmptySuccess(); + // } + // case HttpStatusCode.Unauthorized: // FIXME: missing in docs + // return opKnownHttpFailure(resp.status, resp); + // case HttpStatusCode.NotFound: + // return opKnownHttpFailure(resp.status, resp); + // case HttpStatusCode.Conflict: + // return opKnownHttpFailure(resp.status, resp); + // default: + // return opUnknownHttpFailure(resp); + // } + // } + /** - * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-transfers - * @deprecated + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-transfers */ - async informWireTransfer( + async listConfirmedWireTransfers( token: AccessToken | undefined, - body: TalerMerchantApi.TransferInformation, + params: TalerMerchantApi.ListConfirmedWireTransferRequestParams = {}, ) { const url = new URL(`private/transfers`, this.baseUrl); + if (params.paytoURI) { + url.searchParams.set("payto_uri", params.paytoURI); + } + if (params.before) { + url.searchParams.set("before", String(params.before)); + } + if (params.after) { + url.searchParams.set("after", String(params.after)); + } + if (params.expected !== undefined) { + url.searchParams.set("expected", params.expected ? "YES" : "NO"); + } + addPaginationParams(url, params); + const headers: Record<string, string> = {}; if (token) { headers.Authorization = makeBearerTokenAuthHeader(token); } const resp = await this.httpLib.fetch(url.href, { - method: "POST", - body, + method: "GET", headers, }); switch (resp.status) { - case HttpStatusCode.NoContent: { - this.cacheEvictor.notifySuccess( - TalerMerchantInstanceCacheEviction.CREATE_TRANSFER, - ); - return opEmptySuccess(); - } + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForTansferList()); case HttpStatusCode.Unauthorized: // FIXME: missing in docs return opKnownHttpFailure(resp.status, resp); - case HttpStatusCode.NotFound: - return opKnownHttpFailure(resp.status, resp); - case HttpStatusCode.Conflict: + case HttpStatusCode.NotFound: // FIXME: missing in docs return opKnownHttpFailure(resp.status, resp); default: return opUnknownHttpFailure(resp); @@ -1756,24 +1800,27 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-transfers */ - async listWireTransfers( + async listIncomingWireTransfers( token: AccessToken | undefined, - params: TalerMerchantApi.ListWireTransferRequestParams = {}, + params: TalerMerchantApi.ListIncomingWireTransferRequestParams = {}, ) { const url = new URL(`private/transfers`, this.baseUrl); - if (params.after) { - url.searchParams.set("after", String(params.after)); + if (params.paytoURI) { + url.searchParams.set("payto_uri", params.paytoURI); } if (params.before) { url.searchParams.set("before", String(params.before)); } - if (params.paytoURI) { - url.searchParams.set("payto_uri", params.paytoURI); + if (params.after) { + url.searchParams.set("after", String(params.after)); } if (params.verified !== undefined) { url.searchParams.set("verified", params.verified ? "YES" : "NO"); } + if (params.confirmed !== undefined) { + url.searchParams.set("verified", params.confirmed ? "YES" : "NO"); + } addPaginationParams(url, params); const headers: Record<string, string> = {}; @@ -1797,39 +1844,39 @@ export class TalerMerchantInstanceHttpClient { } } - /** - * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-transfers-$TID - * @deprecated - */ - async deleteWireTransfer(token: AccessToken | undefined, transferId: string) { - const url = new URL(`private/transfers/${transferId}`, this.baseUrl); - - const headers: Record<string, string> = {}; - if (token) { - headers.Authorization = makeBearerTokenAuthHeader(token); - } - const resp = await this.httpLib.fetch(url.href, { - method: "DELETE", - headers, - }); - - switch (resp.status) { - case HttpStatusCode.NoContent: { - this.cacheEvictor.notifySuccess( - TalerMerchantInstanceCacheEviction.DELETE_TRANSFER, - ); - return opEmptySuccess(); - } - case HttpStatusCode.Unauthorized: // FIXME: missing in docs - return opKnownHttpFailure(resp.status, resp); - case HttpStatusCode.NotFound: - return opKnownHttpFailure(resp.status, resp); - case HttpStatusCode.Conflict: - return opKnownHttpFailure(resp.status, resp); - default: - return opUnknownHttpFailure(resp); - } - } + // /** + // * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-transfers-$TID + // * @deprecated + // */ + // async deleteWireTransfer(token: AccessToken | undefined, transferId: string) { + // const url = new URL(`private/transfers/${transferId}`, this.baseUrl); + + // const headers: Record<string, string> = {}; + // if (token) { + // headers.Authorization = makeBearerTokenAuthHeader(token); + // } + // const resp = await this.httpLib.fetch(url.href, { + // method: "DELETE", + // headers, + // }); + + // switch (resp.status) { + // case HttpStatusCode.NoContent: { + // this.cacheEvictor.notifySuccess( + // TalerMerchantInstanceCacheEviction.DELETE_TRANSFER, + // ); + // return opEmptySuccess(); + // } + // case HttpStatusCode.Unauthorized: // FIXME: missing in docs + // return opKnownHttpFailure(resp.status, resp); + // case HttpStatusCode.NotFound: + // return opKnownHttpFailure(resp.status, resp); + // case HttpStatusCode.Conflict: + // return opKnownHttpFailure(resp.status, resp); + // default: + // return opUnknownHttpFailure(resp); + // } + // } // // OTP Devices diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts @@ -1461,24 +1461,77 @@ export interface GetOrderRequestParams { // explicitly to “YES”. allowRefundedForRepurchase?: boolean; } -export interface ListWireTransferRequestParams { - // Filter for transfers to the given bank account - // (subject and amount MUST NOT be given in the payto URI). +export interface ListConfirmedWireTransferRequestParams { + /** + * Filter for transfers to the given bank account + * (subject and amount MUST NOT be given in the payto URI). + */ paytoURI?: string; - // Filter for transfers executed before the given timestamp. + /** + * Filter for transfers executed before the given timestamp. + */ before?: number; - // Filter for transfers executed after the given timestamp. + /** + * Filter for transfers executed after the given timestamp. + */ after?: number; - // At most return the given number of results. Negative for - // descending in execution time, positive for ascending in - // execution time. Default is -20. + /** + * At most return the given number of results. Negative for + * descending in execution time, positive for ascending in + * execution time. Default is -20. + */ limit?: number; - // Starting transfer_serial_id for an iteration. + /** + * + */ + order?: "asc" | "dec"; + /** + * Starting transfer_serial_id for an iteration. + */ offset?: string; - // Filter transfers by verification status. - verified?: boolean; + /** + * Filter transfers that we expected to receive. + */ + expected?: boolean; +} +export interface ListIncomingWireTransferRequestParams { + /** + * Filter for transfers to the given bank account + * (subject and amount MUST NOT be given in the payto URI). + */ + paytoURI?: string; + /** + * Filter for transfers executed before the given timestamp. + */ + before?: number; + /** + * Filter for transfers executed after the given timestamp. + */ + after?: number; + /** + * At most return the given number of results. Negative for + * descending in execution time, positive for ascending in + * execution time. Default is -20. + */ + limit?: number; + /** + * + */ order?: "asc" | "dec"; + /** + * Starting transfer_serial_id for an iteration. + */ + offset?: string; + /** + * Filter transfers by verification status. + */ + verified?: boolean; + /** + * Filter transfers that were confirmed + */ + confirmed?: boolean; } + export interface ListOrdersRequestParams { /** * If set to yes, only return paid orders, if no only