commit ea8356b5f1f315f09245221a143451f24e6c7ea1 parent 1e3c9b85b64dd4367a1ed9272b1ff6b0f63090f3 Author: Sebastian <sebasjm@taler-systems.com> Date: Wed, 7 Jan 2026 17:17:16 -0300 fix #10839 Diffstat:
38 files changed, 3099 insertions(+), 124 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/Application.tsx b/packages/merchant-backoffice-ui/src/Application.tsx @@ -88,6 +88,9 @@ import { revalidateInstanceConfirmedTransfers, revalidateInstanceIncomingTransfers, } from "./hooks/transfer.js"; +import { revalidateInstanceMoneyPots } from "./hooks/pots.js"; +import { revalidateInstanceGroups } from "./hooks/groups.js"; +import { revalidateInstanceScheduledReports } from "./hooks/reports.js"; const TALER_SCREEN_ID = 2; const WITH_LOCAL_STORAGE_CACHE = false; @@ -407,6 +410,42 @@ const swrCacheEvictor = new (class await Promise.all([revalidateInstanceAccessTokens()]); return; } + case TalerMerchantInstanceCacheEviction.CREATE_REPORTS: { + await Promise.all([revalidateInstanceScheduledReports()]); + return; + } + case TalerMerchantInstanceCacheEviction.UPDATE_REPORTS: { + await Promise.all([revalidateInstanceScheduledReports()]); + return; + } + case TalerMerchantInstanceCacheEviction.DELETE_REPORTS: { + await Promise.all([revalidateInstanceScheduledReports()]); + return; + } + case TalerMerchantInstanceCacheEviction.CREATE_POTS: { + await Promise.all([revalidateInstanceMoneyPots()]); + return; + } + case TalerMerchantInstanceCacheEviction.UPDATE_POTS: { + await Promise.all([revalidateInstanceMoneyPots()]); + return; + } + case TalerMerchantInstanceCacheEviction.DELETE_POTS: { + await Promise.all([revalidateInstanceMoneyPots()]); + return; + } + case TalerMerchantInstanceCacheEviction.CREATE_GROUPS: { + await Promise.all([revalidateInstanceGroups()]); + return; + } + case TalerMerchantInstanceCacheEviction.UPDATE_GROUPS: { + await Promise.all([revalidateInstanceGroups()]); + return; + } + case TalerMerchantInstanceCacheEviction.DELETE_GROUPS: { + await Promise.all([revalidateInstanceGroups()]); + return; + } case TalerMerchantInstanceCacheEviction.LAST: { return; } diff --git a/packages/merchant-backoffice-ui/src/Routing.tsx b/packages/merchant-backoffice-ui/src/Routing.tsx @@ -66,6 +66,7 @@ import PasswordPage, { import ProductCreatePage from "./paths/instance/products/create/index.js"; import ProductListPage from "./paths/instance/products/list/index.js"; import ProductUpdatePage from "./paths/instance/products/update/index.js"; +import Statistics from "./paths/instance/statistics/list/index.js"; import TemplateCreatePage from "./paths/instance/templates/create/index.js"; import TemplateListPage from "./paths/instance/templates/list/index.js"; import TemplateQrPage from "./paths/instance/templates/qr/index.js"; @@ -86,8 +87,15 @@ import { LoginPage } from "./paths/login/index.js"; import { NewAccount } from "./paths/newAccount/index.js"; import { ResetAccount } from "./paths/resetAccount/index.js"; import { Settings } from "./paths/settings/index.js"; -import Statistics from "./paths/instance/statistics/list/index.js"; import { Notification } from "./utils/types.js"; +import ListMoneyPots from "./paths/instance/pots/list/index.js"; +import UpdateMoneyPots from "./paths/instance/pots/update/index.js"; +import CreateMoneyPots from "./paths/instance/pots/create/index.js"; +import ListProductGroups from "./paths/instance/groups/list/index.js"; +import CreateProductGroup from "./paths/instance/groups/create/index.js"; +import ListScheduledReport from "./paths/instance/reports/list/index.js"; +import UpdateScheduledReport from "./paths/instance/reports/update/index.js"; +import CreateScheduledReport from "./paths/instance/reports/create/index.js"; const TALER_SCREEN_ID = 3; @@ -144,6 +152,18 @@ export enum InstancePaths { newAccount = "/account/new", resetAccount = "/account/reset/:id", statistics = "/statistics", + + pots_list = "/pots", + pots_update = "/pots/:cid/update", + pots_new = "/pots/new", + + groups_list = "/groups", + groups_update = "/groups/:cid/update", + groups_new = "/groups/new", + + reports_list = "/reports", + reports_update = "/reports/:cid/update", + reports_new = "/reports/new", } export enum AdminPaths { @@ -388,6 +408,95 @@ export function Routing(_p: Props): VNode { }} /> {/** + * Money pots pages + */} + <Route + path={InstancePaths.pots_list} + component={ListMoneyPots} + onCreate={() => { + route(InstancePaths.pots_new); + }} + onSelect={(id: string) => { + route(InstancePaths.pots_update.replace(":cid", id)); + }} + /> + <Route + path={InstancePaths.pots_update} + component={UpdateMoneyPots} + onConfirm={() => { + route(InstancePaths.pots_list); + }} + onBack={() => { + route(InstancePaths.pots_list); + }} + /> + <Route + path={InstancePaths.pots_new} + component={CreateMoneyPots} + onConfirm={() => { + route(InstancePaths.pots_list); + }} + onBack={() => { + route(InstancePaths.pots_list); + }} + /> + {/** + * Product group pages + */} + <Route + path={InstancePaths.groups_list} + component={ListProductGroups} + onCreate={() => { + route(InstancePaths.groups_new); + }} + onSelect={(id: string) => { + route(InstancePaths.groups_update.replace(":cid", id)); + }} + /> + <Route + path={InstancePaths.groups_new} + component={CreateProductGroup} + onConfirm={() => { + route(InstancePaths.groups_list); + }} + onBack={() => { + route(InstancePaths.groups_list); + }} + /> + {/** + * Scheduled report pages + */} + <Route + path={InstancePaths.reports_list} + component={ListScheduledReport} + onCreate={() => { + route(InstancePaths.reports_new); + }} + onSelect={(id: string) => { + route(InstancePaths.reports_update.replace(":cid", id)); + }} + /> + <Route + path={InstancePaths.reports_update} + component={UpdateScheduledReport} + onConfirm={() => { + route(InstancePaths.reports_list); + }} + onBack={() => { + route(InstancePaths.reports_list); + }} + /> + <Route + path={InstancePaths.reports_new} + component={CreateScheduledReport} + onConfirm={() => { + route(InstancePaths.reports_list); + }} + onBack={() => { + route(InstancePaths.reports_list); + }} + /> + {/** * Category pages */} <Route diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx @@ -166,6 +166,45 @@ export function Sidebar({ mobile }: Props): VNode { </HtmlPersonaFlag> <HtmlPersonaFlag htmlElement="li" + point={UIElement.sidebar_group} + > + <a href={"#/groups"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-group" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Product groups</i18n.Translate> + </span> + </a> + </HtmlPersonaFlag> + <HtmlPersonaFlag + htmlElement="li" + point={UIElement.sidebar_pots} + > + <a href={"#/pots"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-pot" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Money pots</i18n.Translate> + </span> + </a> + </HtmlPersonaFlag> + <HtmlPersonaFlag + htmlElement="li" + point={UIElement.sidebar_reports} + > + <a href={"#/reports"} class="has-icon"> + <span class="icon"> + <i class="mdi mdi-paperclip" /> + </span> + <span class="menu-item-label"> + <i18n.Translate>Reports</i18n.Translate> + </span> + </a> + </HtmlPersonaFlag> + <HtmlPersonaFlag + htmlElement="li" point={UIElement.sidebar_statistics} > <a href={"#/statistics"} class="has-icon"> @@ -428,6 +467,9 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap { [UIElement.sidebar_password]: true, [UIElement.sidebar_accessTokens]: true, [UIElement.sidebar_categories]: true, + [UIElement.sidebar_group]: true, + [UIElement.sidebar_pots]: true, + [UIElement.sidebar_reports]: true, [UIElement.sidebar_webhooks]: true, [UIElement.sidebar_wireTransfers]: true, [UIElement.sidebar_inventory]: true, @@ -458,6 +500,9 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap { [UIElement.sidebar_accessTokens]: false, [UIElement.sidebar_categories]: false, + [UIElement.sidebar_group]: false, + [UIElement.sidebar_pots]: false, + [UIElement.sidebar_reports]: false, [UIElement.sidebar_discounts]: false, [UIElement.sidebar_subscriptions]: false, [UIElement.sidebar_tokenFamilies]: false, @@ -483,6 +528,9 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap { [UIElement.sidebar_templates]: true, [UIElement.sidebar_inventory]: true, [UIElement.sidebar_categories]: true, + [UIElement.sidebar_group]: false, + [UIElement.sidebar_pots]: false, + [UIElement.sidebar_reports]: false, [UIElement.sidebar_accessTokens]: false, [UIElement.sidebar_kycStatus]: true, [UIElement.sidebar_bankAccounts]: true, @@ -518,6 +566,9 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap { [UIElement.sidebar_templates]: false, [UIElement.sidebar_categories]: false, + [UIElement.sidebar_group]: false, + [UIElement.sidebar_pots]: false, + [UIElement.sidebar_reports]: false, [UIElement.sidebar_discounts]: false, [UIElement.sidebar_subscriptions]: false, [UIElement.sidebar_tokenFamilies]: false, @@ -548,6 +599,9 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap { [UIElement.sidebar_templates]: false, [UIElement.sidebar_categories]: false, + [UIElement.sidebar_group]: false, + [UIElement.sidebar_pots]: false, + [UIElement.sidebar_reports]: false, [UIElement.sidebar_discounts]: false, [UIElement.sidebar_subscriptions]: false, [UIElement.sidebar_tokenFamilies]: false, diff --git a/packages/merchant-backoffice-ui/src/hooks/groups.ts b/packages/merchant-backoffice-ui/src/hooks/groups.ts @@ -0,0 +1,55 @@ +/* + 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/> + */ + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; +import { PAGINATED_LIST_REQUEST } from "../utils/constants.js"; +const useSWR = _useSWR as unknown as SWRHook; + +export function revalidateInstanceGroups() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listProductGroups", + undefined, + { revalidate: true }, + ); +} +export function useInstanceProductGroups() { + const { state, lib } = useSessionContext(); + + // const [offset, setOffset] = useState<string | undefined>(); + + async function fetcher([token, bid]: [AccessToken, string]) { + return await lib.instance.listProductGroups(token, { + // limit: PAGINATED_LIST_REQUEST, + // offset: bid, + // order: "dec", + }); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listProductGroups">, + TalerHttpError + >([state.token, "offset", "listProductGroups"], fetcher); + + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; + + // return buildPaginatedResult(data.body.groups, offset, setOffset, (d) => d.otp_device_id) + return data; +} diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.test.ts b/packages/merchant-backoffice-ui/src/hooks/instance.test.ts @@ -19,7 +19,7 @@ * @author Sebastian Javier Marchano (sebasjm) */ -import { MerchantAuthMethod, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { AccessToken, MerchantAuthMethod, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useMerchantApiContext } from "@gnu-taler/web-util/browser"; import * as tests from "@gnu-taler/web-util/testing"; import { expect } from "chai"; @@ -78,7 +78,7 @@ describe("instance api interaction with details", () => { name: "other_name", } as TalerMerchantApi.QueryInstancesResponse, }); - api.instance.updateCurrentInstance(undefined, { + api.instance.updateCurrentInstance("undefined" as AccessToken, { name: "other_name", } as TalerMerchantApi.InstanceReconfigurationMessage); }, @@ -163,7 +163,7 @@ describe("instance api interaction with details", () => { }, } as TalerMerchantApi.QueryInstancesResponse, }); - // api.setNewAccessToken(undefined, "secret" as AccessToken); + // api.setNewAccessToken("undefined" as AccessToken, "secret" as AccessToken); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ @@ -241,7 +241,7 @@ describe("instance api interaction with details", () => { } as TalerMerchantApi.QueryInstancesResponse, }); - api.instance.updateCurrentInstanceAuthentication(undefined, { + api.instance.updateCurrentInstanceAuthentication("undefined" as AccessToken, { method: MerchantAuthMethod.TOKEN, password: "asd" }); @@ -380,7 +380,7 @@ describe("instance admin api interaction with listing", () => { }, }); - api.instance.createInstance(undefined, { + api.instance.createInstance("undefined" as AccessToken, { name: "other_name", } as TalerMerchantApi.InstanceConfigurationMessage); }, @@ -470,7 +470,7 @@ describe("instance admin api interaction with listing", () => { }, }); - api.instance.deleteInstance(undefined, "the_id"); + api.instance.deleteInstance("undefined" as AccessToken, "the_id"); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ @@ -628,7 +628,7 @@ describe("instance admin api interaction with listing", () => { }, }); - api.instance.deleteInstance(undefined, "the_id", { purge: true }); + api.instance.deleteInstance("undefined" as AccessToken, "the_id", { purge: true }); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ @@ -712,7 +712,7 @@ describe("instance management api interaction with listing", () => { }, }); - api.instance.updateCurrentInstance(undefined, { + api.instance.updateCurrentInstance("undefined" as AccessToken, { name: "other_name", } as TalerMerchantApi.InstanceConfigurationMessage); }, diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts @@ -150,6 +150,7 @@ export function useInstanceKYCDetailsLongPolling() { const token = state.token; const fetcher = useMemo(() => { + if (!token) throw Error("should login first"); // unreachable return async () => { const DEFAULT_WAIT = 5000; const now = new Date().getTime(); @@ -168,7 +169,7 @@ export function useInstanceKYCDetailsLongPolling() { }; }, [token, latestReason]); - const { result: data, error } = useAsyncWithRetry(fetcher, (r, err) => { + const { result: data, error } = useAsyncWithRetry(!token ? undefined : fetcher, (r, err) => { // halt if error if (err !== undefined) return false; // loading, just wait diff --git a/packages/merchant-backoffice-ui/src/hooks/order.test.ts b/packages/merchant-backoffice-ui/src/hooks/order.test.ts @@ -21,6 +21,7 @@ import { AbsoluteTime, + AccessToken, AmountString, TalerMerchantApi, } from "@gnu-taler/taler-util"; @@ -97,7 +98,7 @@ describe("order api interaction with listing", () => { }, }); - api.instance.createOrder(undefined, { + api.instance.createOrder("undefined" as AccessToken, { order: { amount: "ARS:12" as AmountString, summary: "pay me" }, }); }, @@ -184,7 +185,7 @@ describe("order api interaction with listing", () => { }, }); - api.instance.addRefund(undefined, "1", { + api.instance.addRefund("undefined" as AccessToken, "1", { reason: "double pay", refund: "EUR:1" as AmountString, }); @@ -263,7 +264,7 @@ describe("order api interaction with listing", () => { }, }); - api.instance.deleteOrder(undefined, "1"); + api.instance.deleteOrder("undefined" as AccessToken, "1"); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ @@ -332,7 +333,7 @@ describe("order api interaction with details", () => { } as unknown as TalerMerchantApi.CheckPaymentPaidResponse, }); - api.instance.addRefund(undefined, "1", { + api.instance.addRefund("undefined" as AccessToken, "1", { reason: "double pay", refund: "EUR:1" as AmountString, }); @@ -402,7 +403,7 @@ describe("order api interaction with details", () => { } as unknown as TalerMerchantApi.CheckPaymentPaidResponse, }); - api.instance.forgetOrder(undefined, "1", { + api.instance.forgetOrder("undefined" as AccessToken, "1", { fields: ["$.summary"], }); }, diff --git a/packages/merchant-backoffice-ui/src/hooks/order.ts b/packages/merchant-backoffice-ui/src/hooks/order.ts @@ -67,6 +67,7 @@ export function useWaitForOrderPayment( const token = state.status === "loggedIn" ? state.token : undefined; const evictWhenPaid = useMemo(() => { + if (!token) throw Error("should login first"); // unreachable return async () => { const DEFAULT_WAIT = 5000; const now = new Date().getTime(); @@ -86,7 +87,10 @@ export function useWaitForOrderPayment( }; }, [token]); - const fetcher = !current || current.order_status === "paid" ? undefined : evictWhenPaid; + const fetcher = + !token || !current || current.order_status === "paid" + ? undefined + : evictWhenPaid; useAsyncWithRetry(fetcher, (r, err) => { // halt if error diff --git a/packages/merchant-backoffice-ui/src/hooks/pots.ts b/packages/merchant-backoffice-ui/src/hooks/pots.ts @@ -0,0 +1,78 @@ +/* + 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/> + */ + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; +const useSWR = _useSWR as unknown as SWRHook; + +export function revalidateInstanceMoneyPots() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listMoneyPots", + undefined, + { revalidate: true }, + ); +} +export function useInstanceMoneyPots() { + const { state, lib } = useSessionContext(); + + // const [offset, setOffset] = useState<string | undefined>(); + + async function fetcher([token, _bid]: [AccessToken, string]) { + return await lib.instance.listMoneyPots(token, { + // limit: PAGINATED_LIST_REQUEST, + // offset: bid, + // order: "dec", + }); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listMoneyPots">, + TalerHttpError + >([state.token, "offset", "listMoneyPots"], fetcher); + + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; + + // return buildPaginatedResult(data.body.otp_devices, offset, setOffset, (d) => d.otp_device_id) + return data; +} + +export function revalidateMoneyPotDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getMoneyPotDetails", + undefined, + { revalidate: true }, + ); +} +export function useMoneyPotDetails(deviceId: string) { + const { state, lib } = useSessionContext(); + + async function fetcher([dId, token]: [string, AccessToken]) { + return await lib.instance.getMoneyPotDetails(token, dId); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getMoneyPotDetails">, + TalerHttpError + >([deviceId, state.token, "getMoneyPotDetails"], fetcher); + + if (data) return data; + if (error) return error; + return undefined; +} diff --git a/packages/merchant-backoffice-ui/src/hooks/preference.ts b/packages/merchant-backoffice-ui/src/hooks/preference.ts @@ -30,6 +30,9 @@ export enum UIElement { sidebar_orders, sidebar_inventory, sidebar_categories, + sidebar_group, + sidebar_pots, + sidebar_reports, sidebar_wireTransfers, sidebar_templates, sidebar_kycStatus, diff --git a/packages/merchant-backoffice-ui/src/hooks/product.test.ts b/packages/merchant-backoffice-ui/src/hooks/product.test.ts @@ -33,7 +33,7 @@ import { API_LIST_PRODUCTS, API_UPDATE_PRODUCT_BY_ID, } from "./urls.js"; -import { TalerMerchantApi } from "@gnu-taler/taler-util"; +import { AccessToken, TalerMerchantApi } from "@gnu-taler/taler-util"; import { useMerchantApiContext } from "@gnu-taler/web-util/browser"; describe("product api interaction with listing", () => { @@ -99,7 +99,7 @@ describe("product api interaction with listing", () => { } as TalerMerchantApi.ProductDetailResponse, }); - api.instance.addProduct(undefined, { + api.instance.addProduct("undefined" as AccessToken, { price: "ARS:23", } as any); }, @@ -187,7 +187,7 @@ describe("product api interaction with listing", () => { } as TalerMerchantApi.ProductDetailResponse, }); - api.instance.updateProduct(undefined, "1234", { + api.instance.updateProduct("undefined" as AccessToken, "1234", { price: "ARS:13", } as any); }, @@ -267,7 +267,7 @@ describe("product api interaction with listing", () => { price: "ARS:12", } as TalerMerchantApi.ProductDetailResponse, }); - api.instance.deleteProduct(undefined, "2345"); + api.instance.deleteProduct("undefined" as AccessToken, "2345"); }, ({ query, api }) => { expect(env.assertJustExpectedRequestWereMade()).deep.eq({ @@ -337,7 +337,7 @@ describe("product api interaction with details", () => { } as TalerMerchantApi.ProductDetailResponse, }); - api.instance.updateProduct(undefined, "12", { + api.instance.updateProduct("undefined" as AccessToken, "12", { description: "other description", } as any); }, diff --git a/packages/merchant-backoffice-ui/src/hooks/reports.ts b/packages/merchant-backoffice-ui/src/hooks/reports.ts @@ -0,0 +1,78 @@ +/* + 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/> + */ + +// FIX default import https://github.com/microsoft/TypeScript/issues/49189 +import { AccessToken, TalerHttpError, TalerMerchantManagementResultByMethod } from "@gnu-taler/taler-util"; +import _useSWR, { SWRHook, mutate } from "swr"; +import { useSessionContext } from "../context/session.js"; +const useSWR = _useSWR as unknown as SWRHook; + +export function revalidateInstanceScheduledReports() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "listScheduledReports", + undefined, + { revalidate: true }, + ); +} +export function useInstanceScheduledReports() { + const { state, lib } = useSessionContext(); + + // const [offset, setOffset] = useState<string | undefined>(); + + async function fetcher([token, _bid]: [AccessToken, string]) { + return await lib.instance.listScheduledReports(token, { + // limit: PAGINATED_LIST_REQUEST, + // offset: bid, + // order: "dec", + }); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"listScheduledReports">, + TalerHttpError + >([state.token, "offset", "listScheduledReports"], fetcher); + + if (error) return error; + if (data === undefined) return undefined; + if (data.type !== "ok") return data; + + // return buildPaginatedResult(data.body.otp_devices, offset, setOffset, (d) => d.otp_device_id) + return data; +} + +export function revalidateScheduledReportDetails() { + return mutate( + (key) => Array.isArray(key) && key[key.length - 1] === "getScheduledReportDetails", + undefined, + { revalidate: true }, + ); +} +export function useScheduledReportDetails(deviceId: string) { + const { state, lib } = useSessionContext(); + + async function fetcher([dId, token]: [string, AccessToken]) { + return await lib.instance.getScheduledReportDetails(token, dId); + } + + const { data, error } = useSWR< + TalerMerchantManagementResultByMethod<"getScheduledReportDetails">, + TalerHttpError + >([deviceId, state.token, "getScheduledReportDetails"], fetcher); + + if (data) return data; + if (error) return error; + return undefined; +} diff --git a/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/admin/create/CreatePage.tsx @@ -45,7 +45,6 @@ import { Input } from "../../../components/form/Input.js"; import { DefaultInstanceFormFields } from "../../../components/instance/DefaultInstanceFormFields.js"; import { SolveMFAChallenges } from "../../../components/SolveMFA.js"; import { useSessionContext } from "../../../context/session.js"; -import { usePreference } from "../../../hooks/preference.js"; import { EMAIL_REGEX, INSTANCE_ID_REGEX, @@ -156,7 +155,7 @@ export function CreatePage({ onConfirm, onBack, forceId }: Props): VNode { }; const create = safeFunctionHandler( async ( - token: AccessToken | undefined, + token: AccessToken, data: TalerMerchantApi.InstanceConfigurationMessage, challengeIds: string[], ) => { @@ -181,7 +180,7 @@ export function CreatePage({ onConfirm, onBack, forceId }: Props): VNode { } return opEmptySuccess(); }, - hasErrors ? undefined : [session.token, data, []], + !session.token || hasErrors ? undefined : [session.token, data, []], ); create.onSuccess = (success, oldtoken, data) => { if (success) { diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/Table.tsx @@ -58,9 +58,10 @@ export function CardTable({ accounts, onCreate, onSelect }: Props): VNode { const [deleting, setDeleting] = useState<TalerMerchantApi.BankAccountEntry | null>(null); - const remove = safeFunctionHandler((w: string) => { - return lib.instance.deleteBankAccount.bind(lib.instance)(session.token, w); - }); + const remove = safeFunctionHandler( + lib.instance.deleteBankAccount.bind(lib.instance), + !session.token || !deleting ? undefined : [session.token, deleting.h_wire] + ); remove.onFail = (fail) => { switch (fail.case) { case HttpStatusCode.Unauthorized: @@ -90,7 +91,7 @@ export function CardTable({ accounts, onCreate, onSelect }: Props): VNode { danger active onCancel={() => setDeleting(null)} - confirm={remove.withArgs(deleting.h_wire)} + confirm={remove} > <p> <i18n.Translate> diff --git a/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/categories/update/UpdatePage.tsx @@ -78,7 +78,7 @@ export function UpdatePage({ category, onUpdated, onBack }: Props): VNode { }); useEffect(() => { - if (!category || !category?.products) return; + if (!category || !category?.products || !token) return; const ps = category.products.map((prod) => { return lib.instance .getProductDetails(token, String(prod.product_id)) diff --git a/packages/merchant-backoffice-ui/src/paths/instance/groups/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/groups/create/CreatePage.tsx @@ -0,0 +1,131 @@ +/* + 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 { HttpStatusCode, TalerMerchantApi } 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 { useSessionContext } from "../../../../context/session.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; + +const TALER_SCREEN_ID = 37; + +type Entity = TalerMerchantApi.GroupAddRequest; + +interface Props { + onCreated: () => void; + onBack?: () => void; +} + +export function CreatePage({ onCreated, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const [state, setState] = useState<Partial<Entity>>({}); + + const errors = undefinedIfEmpty<FormErrors<Entity>>({ + group_name: !state.group_name + ? i18n.str`Required` + : !/[a-zA-Z0-9]*/.test(state.group_name) + ? i18n.str`Invalid. Please use only letters and numbers.` + : undefined, + description: !state.description ? i18n.str`Required` : undefined, + }); + + const hasErrors = errors !== undefined; + const { state: session, lib } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + + const data = !!errors + ? undefined + : (state as TalerMerchantApi.GroupAddRequest); + + const create = safeFunctionHandler( + lib.instance.createProductGroup.bind(lib.instance), + !session.token || !data ? 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`; + } + }; + + 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} + > + <Input<Entity> + name="group_name" + label={i18n.str`Name`} + // tooltip={i18n.str`Group name`} + /> + <Input<Entity> + name="description" + label={i18n.str`Description`} + // tooltip={i18n.str`Group description`} + /> + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" type="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> + </FormProvider> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/groups/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/groups/create/index.tsx @@ -0,0 +1,40 @@ +/* + 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 { Fragment, VNode, h } from "preact"; +import { CreatePage } from "./CreatePage.js"; + +interface Props { + onBack?: () => void; + onConfirm: () => void; +} + +export default function CreateProductGroup({ onConfirm, onBack }: Props): VNode { + + return ( + <> + <CreatePage + onBack={onBack} + onCreated={onConfirm} + /> + </> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/groups/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/groups/list/Table.tsx @@ -0,0 +1,221 @@ +/* + 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 { HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + ButtonBetterBulma, + LocalNotificationBannerBulma, + SafeHandlerTemplate, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { useSessionContext } from "../../../../context/session.js"; + +const TALER_SCREEN_ID = 38; + +type Entity = TalerMerchantApi.GroupEntry; + +interface Props { + devices: Entity[]; + onSelect: (e: Entity) => void; + onCreate: () => void; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; +} + +export function CardTable({ + devices, + onCreate, + onSelect, + onLoadMoreAfter, + onLoadMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); + + const { i18n } = useTranslationContext(); + const { state: session, lib } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + + const remove = safeFunctionHandler( + lib.instance.deleteProductGroup.bind(lib.instance), + ).lambda((id: string) => (!session.token ? undefined! : [session.token, id])); + remove.onSuccess = () => i18n.str`Product group deleted`; + remove.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized`; + case HttpStatusCode.NotFound: + return i18n.str`Not found`; + } + }; + return ( + <Fragment> + <LocalNotificationBannerBulma notification={notification} /> + + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-label" /> + </span> + <i18n.Translate>Product groups</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`Add new group`} + > + <button class="button is-info" accessKey="+" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {devices.length > 0 ? ( + <Table + instances={devices} + onDelete={remove} + onSelect={onSelect} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + </Fragment> + ); +} +interface TableProps { + rowSelection: string[]; + instances: Entity[]; + onDelete: SafeHandlerTemplate<[id: string], unknown>; + onSelect: (e: Entity) => void; + rowSelectionHandler: StateUpdater<string[]>; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; +} + +function Table({ + instances, + onLoadMoreAfter, + onDelete, + onSelect, + onLoadMoreBefore, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="table-container"> + {onLoadMoreBefore && ( + <button type="button" + class="button is-fullwidth" + data-tooltip={i18n.str`Load more groups before the first one`} + onClick={onLoadMoreBefore} + > + <i18n.Translate>Load first page</i18n.Translate> + </button> + )} + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Name</i18n.Translate> + </th> + <th> + <i18n.Translate>Description</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + return ( + <tr key={i.group_serial}> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.group_name} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.description} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <ButtonBetterBulma + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`Delete selected group from the database`} + onClick={onDelete.withArgs(String(i.group_serial))} + > + <i18n.Translate>Delete</i18n.Translate> + </ButtonBetterBulma> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + {onLoadMoreAfter && ( + <button type="button" + class="button is-fullwidth" + data-tooltip={i18n.str`Load more groups after the last one`} + onClick={onLoadMoreAfter} + > + <i18n.Translate>Load next page</i18n.Translate> + </button> + )} + </div> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-magnify mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There are no product groups yet, add more pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/groups/list/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/groups/list/UpdatePage.tsx @@ -0,0 +1,172 @@ +/* + 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 { + assertUnreachable, + HttpStatusCode, + TalerError, + TalerMerchantApi, +} from "@gnu-taler/taler-util"; +import { + ButtonBetterBulma, + Loading, + LocalNotificationBannerBulma, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useEffect, useState } from "preact/hooks"; +import emptyImage from "../../../../assets/empty.png"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; +import { FormProvider } from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { WithId } from "../../../../declaration.js"; +import { + ProductWithId, + useInstanceProductsFromIds, +} from "../../../../hooks/product.js"; + +const TALER_SCREEN_ID = 39; + +type Entity = TalerMerchantApi.GroupEntry & WithId; + +interface Props { + onUpdated: () => void; + onBack?: () => void; + group: Entity; +} +export function UpdatePage({ group, onUpdated, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + const { + state: { token }, + lib, + } = useSessionContext(); + // FIXME: if the product list is big the will bring a lot of info + // const inventoryResult = useInstanceProducts(); + + // const inventory = + // !inventoryResult || + // inventoryResult instanceof TalerError || + // inventoryResult.type === "fail" + // ? [] + // : inventoryResult.body; + + const [state, setState] = useState< + Partial<Entity & { product_map: { id: string; description: string }[] }> + >({ + ...group, + product_map: [], + }); + + // useEffect(() => { + // if (!group || !group?.products || !token) return; + // const ps = category.products.map((prod) => { + // return lib.instance + // .getProductDetails(token, String(prod.product_id)) + // .then((res) => { + // return res.type === "fail" + // ? undefined + // : { + // id: String(prod.product_id), + // description: res.body.description, + // }; + // }); + // }); + // Promise.all(ps).then((all) => { + // const product_map = all.filter(notEmpty); + // setState({ ...state, product_map }); + // }); + // }, []); + + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const data = state as TalerMerchantApi.GroupAddRequest; + const update = safeFunctionHandler( + lib.instance.updateProductGroup.bind(lib.instance), + !token ? undefined : [token, group.id, data], + ); + update.onSuccess = onUpdated; + update.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized`; + case HttpStatusCode.Conflict: + return i18n.str`Conflict`; + case HttpStatusCode.NotFound: + return i18n.str`Not found`; + default: + assertUnreachable(fail); + } + }; + + return ( + <div> + <LocalNotificationBannerBulma notification={notification} /> + + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + <i18n.Translate>Id:</i18n.Translate> + + <b>{group.id}</b> + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column is-four-fifths"> + <FormProvider object={state} valueHandler={setState}> + <Input<Entity> name="group_name" label={i18n.str`Name`} /> + <Input<Entity> + name="description" + label={i18n.str`Description`} + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <ButtonBetterBulma + data-tooltip={i18n.str`Confirm operation`} + onClick={update} + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> + </div> + </div> + </section> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/groups/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/groups/list/index.tsx @@ -0,0 +1,77 @@ +/* + 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 { + HttpStatusCode, + TalerError, + assertUnreachable +} from "@gnu-taler/taler-util"; +import { Fragment, VNode, h } from "preact"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; +import { Loading } from "../../../../components/exception/loading.js"; +import { useInstanceProductGroups } from "../../../../hooks/groups.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; +import { CardTable } from "./Table.js"; + +interface Props { + onCreate: () => void; + onSelect: (id: string) => void; +} + +export default function ListProductGroups({ onCreate, onSelect }: Props): VNode { + const result = useInstanceProductGroups(); + + 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.Unauthorized: { + return <LoginPage />; + } + default: { + assertUnreachable(result); + } + } + } + + return ( + <Fragment> + + <section class="section is-main-section"> + <CardTable + devices={result.body.groups} + onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst} + onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext} + onCreate={onCreate} + onSelect={(e) => { + onSelect(String(e.group_serial)); + }} + /> + </section> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/create/CreatePage.tsx @@ -0,0 +1,130 @@ +/* + 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 { HttpStatusCode, TalerMerchantApi } 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 { useSessionContext } from "../../../../context/session.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; + +const TALER_SCREEN_ID = 37; + +type Entity = TalerMerchantApi.PotAddRequest; + +interface Props { + onCreated: () => void; + onBack?: () => void; +} + +export function CreatePage({ onCreated, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const [state, setState] = useState<Partial<Entity>>({}); + + const errors = undefinedIfEmpty<FormErrors<Entity>>({ + pot_name: !state.pot_name + ? i18n.str`Required` + : undefined, + description: !state.description + ? i18n.str`Required` + : undefined, + }); + + const hasErrors = errors !== undefined; + const { state: session, lib } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + + const data = !!errors + ? undefined + : (state as TalerMerchantApi.PotAddRequest); + const create = safeFunctionHandler( + lib.instance.createMoneyPot.bind(lib.instance), + !session.token || !data ? 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`; + } + }; + + 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} + > + <Input<Entity> + name="pot_name" + label={i18n.str`Name`} + // tooltip={i18n.str`Category name`} + /> + <Input<Entity> + name="description" + label={i18n.str`Description`} + // tooltip={i18n.str`Category name`} + /> + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" type="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> + </FormProvider> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/create/index.tsx @@ -0,0 +1,40 @@ +/* + 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 { Fragment, VNode, h } from "preact"; +import { CreatePage } from "./CreatePage.js"; + +interface Props { + onBack?: () => void; + onConfirm: () => void; +} + +export default function CreateMoneyPots({ onConfirm, onBack }: Props): VNode { + + return ( + <> + <CreatePage + onBack={onBack} + onCreated={onConfirm} + /> + </> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/list/Table.tsx @@ -0,0 +1,221 @@ +/* + 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 { HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + ButtonBetterBulma, + LocalNotificationBannerBulma, + SafeHandlerTemplate, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { useSessionContext } from "../../../../context/session.js"; + +const TALER_SCREEN_ID = 38; + +type Entity = TalerMerchantApi.PotEntry; + +interface Props { + pots: Entity[]; + onSelect: (e: Entity) => void; + onCreate: () => void; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; +} + +export function CardTable({ + pots, + onCreate, + onSelect, + onLoadMoreAfter, + onLoadMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); + + const { i18n } = useTranslationContext(); + const { state: session, lib } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + + const remove = safeFunctionHandler( + lib.instance.deleteMoneyPot.bind(lib.instance), + ).lambda((id: string) => (!session.token ? undefined! : [session.token, id])); + remove.onSuccess = () => i18n.str`Money pot deleted`; + remove.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized`; + case HttpStatusCode.NotFound: + return i18n.str`Not found`; + } + }; + return ( + <Fragment> + <LocalNotificationBannerBulma notification={notification} /> + + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-label" /> + </span> + <i18n.Translate>Money pots</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`Add new pots`} + > + <button class="button is-info" accessKey="+" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {pots.length > 0 ? ( + <Table + instances={pots} + onDelete={remove} + onSelect={onSelect} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + </Fragment> + ); +} +interface TableProps { + rowSelection: string[]; + instances: Entity[]; + onDelete: SafeHandlerTemplate<[id: string], unknown>; + onSelect: (e: Entity) => void; + rowSelectionHandler: StateUpdater<string[]>; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; +} + +function Table({ + instances, + onLoadMoreAfter, + onDelete, + onSelect, + onLoadMoreBefore, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="table-container"> + {onLoadMoreBefore && ( + <button type="button" + class="button is-fullwidth" + data-tooltip={i18n.str`Load more pots before the first one`} + onClick={onLoadMoreBefore} + > + <i18n.Translate>Load first page</i18n.Translate> + </button> + )} + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>Name</i18n.Translate> + </th> + <th> + <i18n.Translate>Total</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + return ( + <tr key={i.pot_serial}> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.pot_name} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.pot_totals} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <ButtonBetterBulma + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`Delete selected pots from the database`} + onClick={onDelete.withArgs(String(i.pot_serial))} + > + <i18n.Translate>Delete</i18n.Translate> + </ButtonBetterBulma> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + {onLoadMoreAfter && ( + <button type="button" + class="button is-fullwidth" + data-tooltip={i18n.str`Load more pots after the last one`} + onClick={onLoadMoreAfter} + > + <i18n.Translate>Load next page</i18n.Translate> + </button> + )} + </div> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-magnify mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There are no money pots yet, add more pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/list/index.tsx @@ -0,0 +1,76 @@ +/* + 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 { + HttpStatusCode, + TalerError, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { Fragment, VNode, h } from "preact"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; +import { Loading } from "../../../../components/exception/loading.js"; +import { useInstanceMoneyPots } from "../../../../hooks/pots.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; +import { CardTable } from "./Table.js"; + +interface Props { + onCreate: () => void; + onSelect: (id: string) => void; +} + +export default function ListMoneyPots({ onCreate, onSelect }: Props): VNode { + const result = useInstanceMoneyPots(); + + 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.Unauthorized: { + return <LoginPage />; + } + default: { + assertUnreachable(result); + } + } + } + + return ( + <Fragment> + <section class="section is-main-section"> + <CardTable + pots={result.body.pots} + onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst} + onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext} + onCreate={onCreate} + onSelect={(e) => { + onSelect(String(e.pot_serial)); + }} + /> + </section> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/update/UpdatePage.tsx @@ -0,0 +1,169 @@ +/* + 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 { + assertUnreachable, + HttpStatusCode, + TalerMerchantApi +} from "@gnu-taler/taler-util"; +import { + ButtonBetterBulma, + LocalNotificationBannerBulma, + useLocalNotificationBetter, + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { FormProvider } from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { WithId } from "../../../../declaration.js"; + +const TALER_SCREEN_ID = 39; + +type Entity = TalerMerchantApi.PotDetailResponse & WithId; + +interface Props { + onUpdated: () => void; + onBack?: () => void; + moneyPot: Entity; +} +export function UpdatePage({ moneyPot, onUpdated, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + const { + state: { token }, + lib, + } = useSessionContext(); + // FIXME: if the product list is big the will bring a lot of info + // const inventoryResult = useInstanceProducts(); + + // const inventory = + // !inventoryResult || + // inventoryResult instanceof TalerError || + // inventoryResult.type === "fail" + // ? [] + // : inventoryResult.body; + + const [state, setState] = useState<Partial<Entity>>({ + ...moneyPot, + }); + + // useEffect(() => { + // if (!category || !category?.products || !token) return; + // const ps = category.products.map((prod) => { + // return lib.instance + // .getProductDetails(token, String(prod.product_id)) + // .then((res) => { + // return res.type === "fail" + // ? undefined + // : { + // id: String(prod.product_id), + // description: res.body.description, + // }; + // }); + // }); + // Promise.all(ps).then((all) => { + // const product_map = all.filter(notEmpty); + // setState({ ...state, product_map }); + // }); + // }, []); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const data = state as TalerMerchantApi.PotModifyRequest; + const update = safeFunctionHandler( + lib.instance.updateMoneyPot.bind(lib.instance), + !token ? undefined : [token, moneyPot.id, data], + ); + update.onSuccess = onUpdated; + update.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`; + default: + assertUnreachable(fail); + } + }; + + return ( + <div> + <LocalNotificationBannerBulma notification={notification} /> + + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + <i18n.Translate>Id:</i18n.Translate> + + <b>{moneyPot.id}</b> + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column is-four-fifths"> + <FormProvider object={state} valueHandler={setState}> + <Input<Entity> + name="pot_name" + label={i18n.str`Name`} + // tooltip={i18n.str`Name of the category`} + /> + <Input<Entity> + name="description" + label={i18n.str`Descripton`} + // tooltip={i18n.str`Name of the category`} + /> + {/* <Input<Entity> + name="pot_totals" + label={i18n.str`Totals`} + /> */} + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <ButtonBetterBulma + data-tooltip={i18n.str`Confirm operation`} + onClick={update} + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> + </div> + </div> + </section> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/pots/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/pots/update/index.tsx @@ -0,0 +1,77 @@ +/* + 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 { + HttpStatusCode, + TalerError, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { Fragment, VNode, h } from "preact"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; +import { Loading } from "../../../../components/exception/loading.js"; +import { useMoneyPotDetails } from "../../../../hooks/pots.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; +import { UpdatePage } from "./UpdatePage.js"; + +interface Props { + onBack?: () => void; + onConfirm: () => void; + cid: string; +} +export default function UpdateMoneyPots({ + cid, + onConfirm, + onBack, +}: Props): VNode { + const result = useMoneyPotDetails(cid); + + 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.Unauthorized: { + return <LoginPage />; + } + default: { + assertUnreachable(result); + } + } + } + + return ( + <Fragment> + <UpdatePage + moneyPot={{ + ...result.body, + id: cid, + }} + onBack={onBack} + onUpdated={onConfirm} + /> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/create/Create.stories.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/create/Create.stories.tsx @@ -0,0 +1,28 @@ +/* + 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/OtpDevices/Create", + component: TestedComponent, +}; diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/create/CreatePage.tsx @@ -0,0 +1,161 @@ +/* + 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 { HttpStatusCode, TalerMerchantApi } 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 { useSessionContext } from "../../../../context/session.js"; +import { undefinedIfEmpty } from "../../../../utils/table.js"; + +const TALER_SCREEN_ID = 37; + +type Entity = TalerMerchantApi.ReportAddRequest; + +interface Props { + onCreated: () => void; + onBack?: () => void; +} + +export function CreatePage({ onCreated, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + + const [state, setState] = useState<Partial<Entity>>({}); + + const errors = undefinedIfEmpty<FormErrors<Entity>>({ + description: !state.description ? i18n.str`Required` : undefined, + data_source: !state.data_source ? i18n.str`Required` : undefined, + mime_type: !state.mime_type ? i18n.str`Required` : undefined, + program_section: !state.mime_type ? i18n.str`Required` : undefined, + report_frequency: !state.mime_type ? i18n.str`Required` : undefined, + // report_frequency_shift: !state.mime_type ? i18n.str`Required` : undefined, + target_address: !state.mime_type ? i18n.str`Required` : undefined, + }); + + const hasErrors = errors !== undefined; + const { state: session, lib } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + + const data = !!errors + ? undefined + : (state as TalerMerchantApi.ReportAddRequest); + const create = safeFunctionHandler( + lib.instance.createScheduledReport.bind(lib.instance), + !session.token || !data ? 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`; + } + }; + + 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} + > + <Input<Entity> + name="description" + label={i18n.str`Description`} + help={i18n.str`Description of the report. Possibly included in the report message`} + /> + + <Input<Entity> + name="data_source" + label={i18n.str`Source`} + help={i18n.str`Base URL to request the data from.`} + /> + + <Input<Entity> + name="mime_type" + label={i18n.str`Type`} + help={i18n.str`Type of the data source`} + /> + + <Input<Entity> + name="program_section" + label={i18n.str`Program`} + help={i18n.str`Merchant backend configuration section specifying the program to use to transmit the report`} + /> + + <Input<Entity> + name="target_address" + label={i18n.str`Address`} + help={i18n.str`Where the report program should send the report`} + /> + + <Input<Entity> + name="report_frequency" + label={i18n.str`Report frequency`} + /> + + <Input<Entity> + name="report_frequency_shift" + label={i18n.str`Description`} + /> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" type="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> + </FormProvider> + </div> + <div class="column" /> + </div> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/create/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/create/index.tsx @@ -0,0 +1,40 @@ +/* + 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 { Fragment, VNode, h } from "preact"; +import { CreatePage } from "./CreatePage.js"; + +interface Props { + onBack?: () => void; + onConfirm: () => void; +} + +export default function CreateScheduledReport({ onConfirm, onBack }: Props): VNode { + + return ( + <> + <CreatePage + onBack={onBack} + onCreated={onConfirm} + /> + </> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/list/Table.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/list/Table.tsx @@ -0,0 +1,224 @@ +/* + 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 { HttpStatusCode, TalerMerchantApi } from "@gnu-taler/taler-util"; +import { + ButtonBetterBulma, + LocalNotificationBannerBulma, + SafeHandlerTemplate, + useLocalNotificationBetter, + useTranslationContext, +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { StateUpdater, useState } from "preact/hooks"; +import { useSessionContext } from "../../../../context/session.js"; + +const TALER_SCREEN_ID = 38; + +type Entity = TalerMerchantApi.ReportEntry; + +interface Props { + reports: Entity[]; + onSelect: (e: Entity) => void; + onCreate: () => void; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; +} + +export function CardTable({ + reports, + onCreate, + onSelect, + onLoadMoreAfter, + onLoadMoreBefore, +}: Props): VNode { + const [rowSelection, rowSelectionHandler] = useState<string[]>([]); + + const { i18n } = useTranslationContext(); + const { state: session, lib } = useSessionContext(); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + + const remove = safeFunctionHandler( + lib.instance.deleteScheduledReport.bind(lib.instance), + ).lambda((id: string) => (!session.token ? undefined! : [session.token, id])); + remove.onSuccess = () => i18n.str`Scheduled report deleted`; + remove.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized`; + case HttpStatusCode.NotFound: + return i18n.str`Not found`; + } + }; + return ( + <Fragment> + <LocalNotificationBannerBulma notification={notification} /> + + <div class="card has-table"> + <header class="card-header"> + <p class="card-header-title"> + <span class="icon"> + <i class="mdi mdi-label" /> + </span> + <i18n.Translate>Scheduled reports</i18n.Translate> + </p> + <div class="card-header-icon" aria-label="more options"> + <span + class="has-tooltip-left" + data-tooltip={i18n.str`Add new reports`} + > + <button class="button is-info" accessKey="+" type="button" onClick={onCreate}> + <span class="icon is-small"> + <i class="mdi mdi-plus mdi-36px" /> + </span> + </button> + </span> + </div> + </header> + <div class="card-content"> + <div class="b-table has-pagination"> + <div class="table-wrapper has-mobile-cards"> + {reports.length > 0 ? ( + <Table + instances={reports} + onDelete={remove} + onSelect={onSelect} + rowSelection={rowSelection} + rowSelectionHandler={rowSelectionHandler} + onLoadMoreAfter={onLoadMoreAfter} + onLoadMoreBefore={onLoadMoreBefore} + /> + ) : ( + <EmptyTable /> + )} + </div> + </div> + </div> + </div> + </Fragment> + ); +} +interface TableProps { + rowSelection: string[]; + instances: Entity[]; + onDelete: SafeHandlerTemplate<[id: string], unknown>; + onSelect: (e: Entity) => void; + rowSelectionHandler: StateUpdater<string[]>; + onLoadMoreBefore?: () => void; + onLoadMoreAfter?: () => void; +} + +function Table({ + instances, + onLoadMoreAfter, + onDelete, + onSelect, + onLoadMoreBefore, +}: TableProps): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="table-container"> + {onLoadMoreBefore && ( + <button type="button" + class="button is-fullwidth" + data-tooltip={i18n.str`Load more devices before the first one`} + onClick={onLoadMoreBefore} + > + <i18n.Translate>Load first page</i18n.Translate> + </button> + )} + <table class="table is-fullwidth is-striped is-hoverable is-fullwidth"> + <thead> + <tr> + <th> + <i18n.Translate>ID</i18n.Translate> + </th> + <th> + <i18n.Translate>Name</i18n.Translate> + </th> + <th> + <i18n.Translate>Total products</i18n.Translate> + </th> + <th /> + </tr> + </thead> + <tbody> + {instances.map((i) => { + return ( + <tr key={i.report_serial}> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.description} + </td> + <td + onClick={(): void => onSelect(i)} + style={{ cursor: "pointer" }} + > + {i.report_frequency} + </td> + <td class="is-actions-cell right-sticky"> + <div class="buttons is-right"> + <ButtonBetterBulma + class="button is-danger is-small has-tooltip-left" + data-tooltip={i18n.str`Delete selected scheduled report from the database`} + onClick={onDelete.withArgs(String(i.report_serial))} + > + <i18n.Translate>Delete</i18n.Translate> + </ButtonBetterBulma> + </div> + </td> + </tr> + ); + })} + </tbody> + </table> + {onLoadMoreAfter && ( + <button type="button" + class="button is-fullwidth" + data-tooltip={i18n.str`Load more devices after the last one`} + onClick={onLoadMoreAfter} + > + <i18n.Translate>Load next page</i18n.Translate> + </button> + )} + </div> + ); +} + +function EmptyTable(): VNode { + const { i18n } = useTranslationContext(); + return ( + <div class="content has-text-grey has-text-centered"> + <p> + <span class="icon is-large"> + <i class="mdi mdi-magnify mdi-48px" /> + </span> + </p> + <p> + <i18n.Translate> + There are no reports yet, add more pressing the + sign + </i18n.Translate> + </p> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/list/index.tsx @@ -0,0 +1,77 @@ +/* + 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 { + HttpStatusCode, + TalerError, + assertUnreachable +} from "@gnu-taler/taler-util"; +import { Fragment, VNode, h } from "preact"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; +import { Loading } from "../../../../components/exception/loading.js"; +import { useInstanceScheduledReports } from "../../../../hooks/reports.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; +import { CardTable } from "./Table.js"; + +interface Props { + onCreate: () => void; + onSelect: (id: string) => void; +} + +export default function ListScheduledReport({ onCreate, onSelect }: Props): VNode { + const result = useInstanceScheduledReports(); + + 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.Unauthorized: { + return <LoginPage />; + } + default: { + assertUnreachable(result); + } + } + } + + return ( + <Fragment> + + <section class="section is-main-section"> + <CardTable + reports={result.body.reports} + onLoadMoreBefore={undefined} //result.isFirstPage ? undefined : result.loadFirst} + onLoadMoreAfter={undefined} //result.isLastPage ? undefined : result.loadNext} + onCreate={onCreate} + onSelect={(e) => { + onSelect(String(e.report_serial)); + }} + /> + </section> + </Fragment> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/update/UpdatePage.tsx @@ -0,0 +1,156 @@ +/* + 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 { + HttpStatusCode, + TalerMerchantApi +} from "@gnu-taler/taler-util"; +import { + ButtonBetterBulma, + LocalNotificationBannerBulma, + useLocalNotificationBetter, + useTranslationContext +} from "@gnu-taler/web-util/browser"; +import { Fragment, h, VNode } from "preact"; +import { useState } from "preact/hooks"; +import { FormProvider } from "../../../../components/form/FormProvider.js"; +import { Input } from "../../../../components/form/Input.js"; +import { useSessionContext } from "../../../../context/session.js"; +import { WithId } from "../../../../declaration.js"; + +const TALER_SCREEN_ID = 39; + +type Entity = TalerMerchantApi.ReportDetailResponse & WithId; + +interface Props { + onUpdated: () => void; + onBack?: () => void; + report: Entity; +} +export function UpdatePage({ report, onUpdated, onBack }: Props): VNode { + const { i18n } = useTranslationContext(); + const { + state: { token }, + lib, + } = useSessionContext(); + // FIXME: if the product list is big the will bring a lot of info + // const inventoryResult = useInstanceProducts(); + + // const inventory = + // !inventoryResult || + // inventoryResult instanceof TalerError || + // inventoryResult.type === "fail" + // ? [] + // : inventoryResult.body; + + const [state, setState] = useState< + Partial<Entity & { product_map: { id: string; description: string }[] }> + >({ + ...report, + }); + + // useEffect(() => { + // if (!category || !category?.products || !token) return; + // const ps = category.products.map((prod) => { + // return lib.instance + // .getProductDetails(token, String(prod.product_id)) + // .then((res) => { + // return res.type === "fail" + // ? undefined + // : { + // id: String(prod.product_id), + // description: res.body.description, + // }; + // }); + // }); + // Promise.all(ps).then((all) => { + // const product_map = all.filter(notEmpty); + // setState({ ...state, product_map }); + // }); + // }, []); + const [notification, safeFunctionHandler] = useLocalNotificationBetter(); + const data = state as TalerMerchantApi.ReportAddRequest; + const update = safeFunctionHandler( + lib.instance.updateScheduledReport.bind(lib.instance), + !token ? undefined : [token, report.id, data], + ); + update.onSuccess = onUpdated; + update.onFail = (fail) => { + switch (fail.case) { + case HttpStatusCode.Unauthorized: + return i18n.str`Unauthorized`; + case HttpStatusCode.NotFound: + return i18n.str`Not found`; + } + }; + + return ( + <div> + <LocalNotificationBannerBulma notification={notification} /> + + <section class="section"> + <section class="hero is-hero-bar"> + <div class="hero-body"> + <div class="level"> + <div class="level-left"> + <div class="level-item"> + <span class="is-size-4"> + <i18n.Translate>Id:</i18n.Translate> + + <b>{report.id}</b> + </span> + </div> + </div> + </div> + </div> + </section> + <hr /> + + <section class="section is-main-section"> + <div class="columns"> + <div class="column is-four-fifths"> + <FormProvider object={state} valueHandler={setState}> + <Input<Entity> + name="description" + label={i18n.str`Description`} + /> + </FormProvider> + + <div class="buttons is-right mt-5"> + {onBack && ( + <button class="button" onClick={onBack}> + <i18n.Translate>Cancel</i18n.Translate> + </button> + )} + <ButtonBetterBulma + data-tooltip={i18n.str`Confirm operation`} + onClick={update} + > + <i18n.Translate>Confirm</i18n.Translate> + </ButtonBetterBulma> + </div> + </div> + </div> + </section> + </section> + </div> + ); +} diff --git a/packages/merchant-backoffice-ui/src/paths/instance/reports/update/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/reports/update/index.tsx @@ -0,0 +1,78 @@ +/* + 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 { + HttpStatusCode, + TalerError, + assertUnreachable, +} from "@gnu-taler/taler-util"; +import { Fragment, VNode, h } from "preact"; +import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js"; +import { Loading } from "../../../../components/exception/loading.js"; +import { useCategoryDetails } from "../../../../hooks/category.js"; +import { LoginPage } from "../../../login/index.js"; +import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js"; +import { UpdatePage } from "./UpdatePage.js"; +import { useScheduledReportDetails } from "../../../../hooks/reports.js"; + +interface Props { + onBack?: () => void; + onConfirm: () => void; + cid: string; +} +export default function UpdateScheduledReport({ + cid, + onConfirm, + onBack, +}: Props): VNode { + const result = useScheduledReportDetails(cid); + + 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.Unauthorized: { + return <LoginPage />; + } + default: { + assertUnreachable(result); + } + } + } + + return ( + <Fragment> + <UpdatePage + report={{ + ...result.body, + id: cid, + }} + onBack={onBack} + onUpdated={onConfirm} + /> + </Fragment> + ); +} diff --git a/packages/taler-harness/src/index.ts b/packages/taler-harness/src/index.ts @@ -927,6 +927,9 @@ deploymentCli } else { merchantAdminToken = merchantAdminTokenArg; } + if (!merchantAdminToken) { + throw Error("no merchant token") + } /** * create bank account diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances-delete.ts @@ -18,6 +18,7 @@ * Imports. */ import { + AccessToken, HttpStatusCode, MerchantAuthMethod, TalerMerchantManagementHttpClient, @@ -113,7 +114,7 @@ export async function runMerchantInstancesDeleteTest(t: GlobalTestState) { // of the default instance. { const res = await merchantApi.deleteInstance( - undefined, + "undefined" as AccessToken, "myinst", ); t.assertTrue(res.type === "fail"); diff --git a/packages/taler-harness/src/integrationtests/test-merchant-instances.ts b/packages/taler-harness/src/integrationtests/test-merchant-instances.ts @@ -139,7 +139,7 @@ export async function runMerchantInstancesTest(t: GlobalTestState) { ); console.log("requesting instances with no auth"); - const exc = await merchantClient.listInstances(undefined); + const exc = await merchantClient.listInstances("undefined" as AccessToken); t.assertTrue(exc.type === "fail"); t.assertTrue(exc.case === HttpStatusCode.Unauthorized); diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts @@ -89,6 +89,11 @@ import { codecForReportAddedResponse, codecForReportDetailResponse, codecForReportsSummaryResponse, + codecForGroupsSummaryResponse, + codecForGroupAddedResponse, + codecForPotAddedResponse, + codecForPotsSummaryResponse, + codecForPotDetailResponse, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, @@ -146,6 +151,15 @@ export enum TalerMerchantInstanceCacheEviction { DELETE_TOKENFAMILY, CREATE_ACCESSTOKEN, DELETE_ACCESSTOKEN, + CREATE_REPORTS, + UPDATE_REPORTS, + DELETE_REPORTS, + CREATE_POTS, + UPDATE_POTS, + DELETE_POTS, + CREATE_GROUPS, + UPDATE_GROUPS, + DELETE_GROUPS, LAST, } @@ -304,7 +318,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-tokens-$SERIAL * */ - async deleteAccessToken(token: AccessToken | undefined, serial: number) { + async deleteAccessToken(token: AccessToken, serial: number) { const url = new URL(`private/tokens/${String(serial)}`, this.baseUrl); const headers: Record<string, string> = {}; if (token) { @@ -598,7 +612,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-auth */ async updateCurrentInstanceAuthentication( - token: AccessToken | undefined, + token: AccessToken, body: TalerMerchantApi.InstanceAuthConfigurationMessage, params: { challengeIds?: string[]; @@ -644,7 +658,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private */ async updateCurrentInstance( - token: AccessToken | undefined, + token: AccessToken, body: TalerMerchantApi.InstanceReconfigurationMessage, params: { challengeIds?: string[] } = {}, ) { @@ -718,7 +732,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private */ async deleteCurrentInstance( - token: AccessToken | undefined, + token: AccessToken, params: { purge?: boolean; challengeIds?: string[] } = {}, ) { const url = new URL(`private`, this.baseUrl); @@ -767,7 +781,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#get--instances-$INSTANCE-private-kyc */ async getCurrentInstanceKycStatus( - token: AccessToken | undefined, + token: AccessToken, params: TalerMerchantApi.GetKycStatusRequestParams = {}, ): Promise< | OperationOk<TalerMerchantApi.MerchantAccountKycRedirectsResponse> @@ -831,7 +845,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-accounts */ async addBankAccount( - token: AccessToken | undefined, + token: AccessToken, body: TalerMerchantApi.AccountAddDetails, params: { challengeIds?: string[]; @@ -880,7 +894,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-accounts-$H_WIRE */ async updateBankAccount( - token: AccessToken | undefined, + token: AccessToken, wireAccount: string, body: TalerMerchantApi.AccountPatchDetails, ) { @@ -914,10 +928,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts */ - async listBankAccounts( - token: AccessToken | undefined, - params?: PaginationParams, - ) { + async listBankAccounts(token: AccessToken, params?: PaginationParams) { const url = new URL(`private/accounts`, this.baseUrl); // addPaginationParams(url, params); @@ -946,10 +957,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts-$H_WIRE */ - async getBankAccountDetails( - token: AccessToken | undefined, - wireAccount: string, - ) { + async getBankAccountDetails(token: AccessToken, wireAccount: string) { const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -976,7 +984,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-accounts-$H_WIRE */ - async deleteBankAccount(token: AccessToken | undefined, wireAccount: string) { + async deleteBankAccount(token: AccessToken, wireAccount: string) { const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -1011,10 +1019,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-categories */ - async listCategories( - token: AccessToken | undefined, - params?: PaginationParams, - ) { + async listCategories(token: AccessToken, params?: PaginationParams) { const url = new URL(`private/categories`, this.baseUrl); // addPaginationParams(url, params); @@ -1043,7 +1048,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-categories-$CATEGORY_ID */ - async getCategoryDetails(token: AccessToken | undefined, cId: string) { + async getCategoryDetails(token: AccessToken, cId: string) { const url = new URL(`private/categories/${cId}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -1071,7 +1076,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-categories */ async addCategory( - token: AccessToken | undefined, + token: AccessToken, body: TalerMerchantApi.CategoryCreateRequest, ) { const url = new URL(`private/categories`, this.baseUrl); @@ -1108,7 +1113,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-categories */ async updateCategory( - token: AccessToken | undefined, + token: AccessToken, cid: string, body: TalerMerchantApi.CategoryCreateRequest, ) { @@ -1145,7 +1150,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-categories-$CATEGORY_ID */ - async deleteCategory(token: AccessToken | undefined, cId: string) { + async deleteCategory(token: AccessToken, cId: string) { const url = new URL(`private/categories/${cId}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -1179,7 +1184,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-products */ async addProduct( - token: AccessToken | undefined, + token: AccessToken, body: TalerMerchantApi.ProductAddDetailRequest, ) { const url = new URL(`private/products`, this.baseUrl); @@ -1216,7 +1221,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-products-$PRODUCT_ID */ async updateProduct( - token: AccessToken | undefined, + token: AccessToken, productId: string, body: TalerMerchantApi.ProductPatchDetailRequest, ) { @@ -1254,7 +1259,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products */ async listProducts( - token: AccessToken | undefined, + token: AccessToken, params: PaginationParams & { category?: string; name?: string; @@ -1323,7 +1328,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products-$PRODUCT_ID */ - async getProductDetails(token: AccessToken | undefined, productId: string) { + async getProductDetails(token: AccessToken, productId: string) { const url = new URL(`private/products/${productId}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -1351,7 +1356,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-products-$PRODUCT_ID-lock */ async lockProduct( - token: AccessToken | undefined, + token: AccessToken, productId: string, body: TalerMerchantApi.LockRequest, ) { @@ -1388,7 +1393,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-products-$PRODUCT_ID */ - async deleteProduct(token: AccessToken | undefined, productId: string) { + async deleteProduct(token: AccessToken, productId: string) { const url = new URL(`private/products/${productId}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -1426,7 +1431,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-orders */ async createOrder( - token: AccessToken | undefined, + token: AccessToken, body: TalerMerchantApi.PostOrderRequest, ) { const url = new URL(`private/orders`, this.baseUrl); @@ -1478,7 +1483,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#inspecting-orders */ async listOrders( - token: AccessToken | undefined, + token: AccessToken, params: TalerMerchantApi.ListOrdersRequestParams = {}, ) { const url = new URL(`private/orders`, this.baseUrl); @@ -1535,7 +1540,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-orders-$ORDER_ID */ async getOrderDetails( - token: AccessToken | undefined, + token: AccessToken, orderId: string, params: TalerMerchantApi.GetOrderRequestParams = {}, ) { @@ -1591,7 +1596,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#private-order-data-cleanup */ async forgetOrder( - token: AccessToken | undefined, + token: AccessToken, orderId: string, body: TalerMerchantApi.ForgetRequest, ) { @@ -1633,7 +1638,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-orders-$ORDER_ID */ async deleteOrder( - token: AccessToken | undefined, + token: AccessToken, orderId: string, force: boolean = false, ) { @@ -1677,7 +1682,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-orders-$ORDER_ID-refund */ async addRefund( - token: AccessToken | undefined, + token: AccessToken, orderId: string, body: TalerMerchantApi.RefundRequest, ) { @@ -1729,7 +1734,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-transfers */ async informWireTransfer( - token: AccessToken | undefined, + token: AccessToken, body: TalerMerchantApi.TransferInformation, ) { const url = new URL(`private/transfers`, this.baseUrl); @@ -1766,7 +1771,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-transfers */ async listConfirmedWireTransfers( - token: AccessToken | undefined, + token: AccessToken, params: TalerMerchantApi.ListConfirmedWireTransferRequestParams = {}, ) { const url = new URL(`private/transfers`, this.baseUrl); @@ -1810,7 +1815,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-incoming */ async listIncomingWireTransfers( - token: AccessToken | undefined, + token: AccessToken, params: TalerMerchantApi.ListIncomingWireTransferRequestParams = {}, ) { const url = new URL(`private/incoming`, this.baseUrl); @@ -1857,7 +1862,7 @@ 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) { + // async deleteWireTransfer(token: AccessToken, transferId: string) { // const url = new URL(`private/transfers/${transferId}`, this.baseUrl); // const headers: Record<string, string> = {}; @@ -1895,7 +1900,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-otp-devices */ async addOtpDevice( - token: AccessToken | undefined, + token: AccessToken, body: TalerMerchantApi.OtpDeviceAddDetails, ) { const url = new URL(`private/otp-devices`, this.baseUrl); @@ -1930,7 +1935,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID */ async updateOtpDevice( - token: AccessToken | undefined, + token: AccessToken, deviceId: string, body: TalerMerchantApi.OtpDevicePatchDetails, ) { @@ -1966,10 +1971,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices */ - async listOtpDevices( - token: AccessToken | undefined, - params?: PaginationParams, - ) { + async listOtpDevices(token: AccessToken, params?: PaginationParams) { const url = new URL(`private/otp-devices`, this.baseUrl); addPaginationParams(url, params); @@ -1998,7 +2000,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID */ async getOtpDeviceDetails( - token: AccessToken | undefined, + token: AccessToken, deviceId: string, params: TalerMerchantApi.GetOtpDeviceRequestParams = {}, ) { @@ -2034,7 +2036,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID */ - async deleteOtpDevice(token: AccessToken | undefined, deviceId: string) { + async deleteOtpDevice(token: AccessToken, deviceId: string) { const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -2069,7 +2071,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-templates */ async addTemplate( - token: AccessToken | undefined, + token: AccessToken, body: TalerMerchantApi.TemplateAddDetails, ) { const url = new URL(`private/templates`, this.baseUrl); @@ -2105,7 +2107,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID */ async updateTemplate( - token: AccessToken | undefined, + token: AccessToken, templateId: string, body: TalerMerchantApi.TemplatePatchDetails, ) { @@ -2141,10 +2143,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#inspecting-template */ - async listTemplates( - token: AccessToken | undefined, - params?: PaginationParams, - ) { + async listTemplates(token: AccessToken, params?: PaginationParams) { const url = new URL(`private/templates`, this.baseUrl); addPaginationParams(url, params); @@ -2172,7 +2171,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID */ - async getTemplateDetails(token: AccessToken | undefined, templateId: string) { + async getTemplateDetails(token: AccessToken, templateId: string) { const url = new URL(`private/templates/${templateId}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -2198,7 +2197,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID */ - async deleteTemplate(token: AccessToken | undefined, templateId: string) { + async deleteTemplate(token: AccessToken, templateId: string) { const url = new URL(`private/templates/${templateId}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -2269,7 +2268,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-webhooks */ async addWebhook( - token: AccessToken | undefined, + token: AccessToken, body: TalerMerchantApi.WebhookAddDetails, ) { const url = new URL(`private/webhooks`, this.baseUrl); @@ -2304,7 +2303,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID */ async updateWebhook( - token: AccessToken | undefined, + token: AccessToken, webhookId: string, body: TalerMerchantApi.WebhookPatchDetails, ) { @@ -2341,10 +2340,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-webhooks */ - async listWebhooks( - token: AccessToken | undefined, - params?: PaginationParams, - ) { + async listWebhooks(token: AccessToken, params?: PaginationParams) { const url = new URL(`private/webhooks`, this.baseUrl); const headers: Record<string, string> = {}; @@ -2371,7 +2367,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID */ - async getWebhookDetails(token: AccessToken | undefined, webhookId: string) { + async getWebhookDetails(token: AccessToken, webhookId: string) { const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -2397,7 +2393,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID */ - async deleteWebhook(token: AccessToken | undefined, webhookId: string) { + async deleteWebhook(token: AccessToken, webhookId: string) { const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -2432,7 +2428,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-tokenfamilies */ async createTokenFamily( - token: AccessToken | undefined, + token: AccessToken, body: TalerMerchantApi.TokenFamilyCreateRequest, ) { const url = new URL(`private/tokenfamilies`, this.baseUrl); @@ -2467,7 +2463,7 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG */ async updateTokenFamily( - token: AccessToken | undefined, + token: AccessToken, tokenSlug: string, body: TalerMerchantApi.TokenFamilyUpdateRequest, ) { @@ -2507,10 +2503,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-tokenfamilies */ - async listTokenFamilies( - token: AccessToken | undefined, - params?: PaginationParams, - ) { + async listTokenFamilies(token: AccessToken, params?: PaginationParams) { const url = new URL(`private/tokenfamilies`, this.baseUrl); const headers: Record<string, string> = {}; @@ -2537,10 +2530,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG */ - async getTokenFamilyDetails( - token: AccessToken | undefined, - tokenSlug: string, - ) { + async getTokenFamilyDetails(token: AccessToken, tokenSlug: string) { const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -2567,7 +2557,7 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG */ - async deleteTokenFamily(token: AccessToken | undefined, tokenSlug: string) { + async deleteTokenFamily(token: AccessToken, tokenSlug: string) { const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -2770,6 +2760,10 @@ export class TalerMerchantInstanceHttpClient { } } + // + // Reports + // + /** * https://docs.taler.net/core/api-merchant.html#post--reports-$REPORT_ID */ @@ -2801,11 +2795,13 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-reports */ async createScheduledReport( + token: AccessToken, body: TalerMerchantApi.ReportAddRequest, ) { const url = new URL(`private/reports`, this.baseUrl); const headers: Record<string, string> = {}; + headers.Authorization = makeBearerTokenAuthHeader(token); const resp = await this.httpLib.fetch(url.href, { method: "POST", body, @@ -2813,8 +2809,14 @@ export class TalerMerchantInstanceHttpClient { }); switch (resp.status) { - case HttpStatusCode.Ok: + case HttpStatusCode.Ok: { + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.CREATE_REPORTS, + ); return opSuccessFromHttp(resp, codecForReportAddedResponse()); + } + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: @@ -2826,12 +2828,14 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-reports-$REPORT_ID */ async updateScheduledReport( + token: AccessToken, id: string, body: TalerMerchantApi.ReportAddRequest, ) { const url = new URL(`private/reports/${id}`, this.baseUrl); const headers: Record<string, string> = {}; + headers.Authorization = makeBearerTokenAuthHeader(token); const resp = await this.httpLib.fetch(url.href, { method: "PATCH", body, @@ -2840,7 +2844,12 @@ export class TalerMerchantInstanceHttpClient { switch (resp.status) { case HttpStatusCode.Ok: + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_REPORTS, + ); return opSuccessFromHttp(resp, codecForReportAddedResponse()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: @@ -2852,12 +2861,14 @@ export class TalerMerchantInstanceHttpClient { * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-reports */ async listScheduledReports( + token: AccessToken, params: PaginationParams = {}, ) { const url = new URL(`private/reports`, this.baseUrl); addPaginationParams(url, params); const headers: Record<string, string> = {}; + headers.Authorization = makeBearerTokenAuthHeader(token); const resp = await this.httpLib.fetch(url.href, { method: "GET", headers, @@ -2866,6 +2877,8 @@ export class TalerMerchantInstanceHttpClient { switch (resp.status) { case HttpStatusCode.Ok: return opSuccessFromHttp(resp, codecForReportsSummaryResponse()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: @@ -2876,10 +2889,11 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-reports-$REPORT_SERIAL */ - async getScheduledReport(id: string) { + async getScheduledReportDetails(token: AccessToken, id: string) { const url = new URL(`private/reports/${id}`, this.baseUrl); const headers: Record<string, string> = {}; + headers.Authorization = makeBearerTokenAuthHeader(token); const resp = await this.httpLib.fetch(url.href, { method: "GET", headers, @@ -2888,6 +2902,8 @@ export class TalerMerchantInstanceHttpClient { switch (resp.status) { case HttpStatusCode.Ok: return opSuccessFromHttp(resp, codecForReportDetailResponse()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: @@ -2898,10 +2914,161 @@ export class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-reports-$REPORT_SERIAL */ - async deleteScheduledReport(serial: string) { + async deleteScheduledReport(token: AccessToken, serial: string) { const url = new URL(`private/reports/${serial}`, this.baseUrl); const headers: Record<string, string> = {}; + 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_REPORTS, + ); + return opEmptySuccess(); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + // + // Money Pots + // + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-pots + */ + async createMoneyPot( + token: AccessToken, + body: TalerMerchantApi.PotAddRequest, + ) { + const url = new URL(`private/pots`, this.baseUrl); + + const headers: Record<string, string> = {}; + headers.Authorization = makeBearerTokenAuthHeader(token); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.CREATE_POTS, + ); + return opSuccessFromHttp(resp, codecForPotAddedResponse()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-pots-$POT_ID + */ + async updateMoneyPot( + token: AccessToken, + id: string, + body: TalerMerchantApi.PotModifyRequest, + ) { + const url = new URL(`private/pots/${id}`, this.baseUrl); + + const headers: Record<string, string> = {}; + headers.Authorization = makeBearerTokenAuthHeader(token); + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_POTS, + ); + return opEmptySuccess(); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-pots + */ + async listMoneyPots(token: AccessToken, params: PaginationParams = {}) { + const url = new URL(`private/pots`, this.baseUrl); + addPaginationParams(url, params); + + const headers: Record<string, string> = {}; + headers.Authorization = makeBearerTokenAuthHeader(token); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForPotsSummaryResponse()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-pots-$POT_SERIAL + */ + async getMoneyPotDetails(token: AccessToken, id: string) { + const url = new URL(`private/pots/${id}`, this.baseUrl); + + const headers: Record<string, string> = {}; + headers.Authorization = makeBearerTokenAuthHeader(token); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForPotDetailResponse()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-pots-$POT_SERIAL + */ + async deleteMoneyPot(token: AccessToken, serial: string) { + const url = new URL(`private/pots/${serial}`, this.baseUrl); + + const headers: Record<string, string> = {}; + headers.Authorization = makeBearerTokenAuthHeader(token); const resp = await this.httpLib.fetch(url.href, { method: "DELETE", headers, @@ -2909,13 +3076,145 @@ export class TalerMerchantInstanceHttpClient { switch (resp.status) { case HttpStatusCode.NoContent: + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.DELETE_POTS, + ); return opEmptySuccess(); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + // + // Product groups + // + + /** + * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-groups + */ + async createProductGroup( + token: AccessToken, + body: TalerMerchantApi.GroupAddRequest, + ) { + const url = new URL(`private/groups`, this.baseUrl); + + const headers: Record<string, string> = {}; + headers.Authorization = makeBearerTokenAuthHeader(token); + const resp = await this.httpLib.fetch(url.href, { + method: "POST", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.CREATE_GROUPS, + ); + return opSuccessFromHttp(resp, codecForGroupAddedResponse()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: return opKnownHttpFailure(resp.status, resp); default: return opUnknownHttpFailure(resp); } } + + /** + * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-groups-$GROUP_ID + */ + async updateProductGroup( + token: AccessToken, + id: string, + body: TalerMerchantApi.GroupAddRequest, + ) { + const url = new URL(`private/reports/${id}`, this.baseUrl); + + const headers: Record<string, string> = {}; + headers.Authorization = makeBearerTokenAuthHeader(token); + const resp = await this.httpLib.fetch(url.href, { + method: "PATCH", + body, + headers, + }); + + switch (resp.status) { + case HttpStatusCode.NoContent: + this.cacheEvictor.notifySuccess( + TalerMerchantInstanceCacheEviction.UPDATE_GROUPS, + ); + return opEmptySuccess(); + case HttpStatusCode.Unauthorized: + 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#get-[-instances-$INSTANCES]-private-groups + */ + async listProductGroups(token: AccessToken, params: PaginationParams = {}) { + const url = new URL(`private/groups`, this.baseUrl); + addPaginationParams(url, params); + + const headers: Record<string, string> = {}; + headers.Authorization = makeBearerTokenAuthHeader(token); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + headers, + }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForGroupsSummaryResponse()); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + + /** + * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-groups-$GROUP_SERIAL + */ + async deleteProductGroup(token: AccessToken, serial: string) { + const url = new URL(`private/groups/${serial}`, this.baseUrl); + + const headers: Record<string, string> = {}; + 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_GROUPS, + ); + + return opEmptySuccess(); + case HttpStatusCode.Unauthorized: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownHttpFailure(resp); + } + } + /** * Get the auth api against the current instance * @@ -3021,7 +3320,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp * https://docs.taler.net/core/api-merchant.html#post--management-instances */ async createInstance( - token: AccessToken | undefined, + token: AccessToken, body: TalerMerchantApi.InstanceConfigurationMessage, params: { challengeIds?: string[]; @@ -3068,7 +3367,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp * https://docs.taler.net/core/api-merchant.html#post--management-instances-$INSTANCE-auth */ async updateInstanceAuthentication( - token: AccessToken | undefined, + token: AccessToken, instanceId: string, body: TalerMerchantApi.InstanceAuthConfigurationMessage, params: { @@ -3115,7 +3414,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp * https://docs.taler.net/core/api-merchant.html#patch--management-instances-$INSTANCE */ async updateInstance( - token: AccessToken | undefined, + token: AccessToken, instanceId: string, body: TalerMerchantApi.InstanceReconfigurationMessage, ) { @@ -3149,10 +3448,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp /** * https://docs.taler.net/core/api-merchant.html#get--management-instances */ - async listInstances( - token: AccessToken | undefined, - params?: PaginationParams, - ) { + async listInstances(token: AccessToken, params?: PaginationParams) { const url = new URL(`management/instances`, this.baseUrl); const headers: Record<string, string> = {}; @@ -3178,7 +3474,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE * */ - async getInstanceDetails(token: AccessToken | undefined, instanceId: string) { + async getInstanceDetails(token: AccessToken, instanceId: string) { const url = new URL(`management/instances/${instanceId}`, this.baseUrl); const headers: Record<string, string> = {}; @@ -3206,7 +3502,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp * https://docs.taler.net/core/api-merchant.html#delete--management-instances-$INSTANCE */ async deleteInstance( - token: AccessToken | undefined, + token: AccessToken, instanceId: string, params: { purge?: boolean; challengeIds?: string[] } = {}, ) { @@ -3256,7 +3552,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE-kyc */ async getIntanceKycStatus( - token: AccessToken | undefined, + token: AccessToken, instanceId: string, params: TalerMerchantApi.GetKycStatusRequestParams, ) { @@ -3304,7 +3600,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE-statistics-counter-$SLUG */ async getStatisticsCounter( - token: AccessToken | undefined, + token: AccessToken, statSlug: string, params: TalerMerchantApi.GetStatisticsRequestParams = {}, ) { @@ -3342,7 +3638,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE-statistics-amount-$SLUG */ async getStatisticsAmount( - token: AccessToken | undefined, + token: AccessToken, statSlug: string, params: TalerMerchantApi.GetStatisticsRequestParams = {}, ) { @@ -3380,7 +3676,7 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE-statistics-report-$NAME */ async getStatisticsReport( - token: AccessToken | undefined, + token: AccessToken, name: "transactions" | "money-pots" | "taxes" | "sales-funnel", params: TalerMerchantApi.GetStatisticsReportParams = {}, ) { diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts @@ -1241,7 +1241,7 @@ export interface MerchantVersionResponse { // Maps available report generator configuration section names // to descriptions of the respective report generator. // Since **v25**. - report_generators: { [section_name: string]: string }; + report_generators?: { [section_name: string]: string }; // Array of exchanges trusted by the merchant. // Since protocol **v6**. @@ -4052,7 +4052,10 @@ export const codecForTalerMerchantConfigResponse = ) .property("version", codecForString()) .property("currencies", codecForMap(codecForCurrencySpecificiation())) - .property("report_generators", codecForMap(codecForString())) + .property( + "report_generators", + codecOptionalDefault(codecForMap(codecForString()), {}), + ) .property("exchanges", codecForList(codecForExchangeConfigInfo())) .property("implementation", codecOptional(codecForString())) .property( @@ -5243,3 +5246,135 @@ export const codecForReportEntry = (): Codec<ReportEntry> => .property("description", codecForString()) .property("report_frequency", codecForDuration) .build("TalerMerchantApi.ReportEntry"); + +export interface GroupsSummaryResponse { + // Return groups that are present in our backend. + groups: GroupEntry[]; +} + +export interface GroupEntry { + // Group identifier + group_serial: Integer; + + // Unique name for the group (unique per instance). + group_name: string; + + // Description for the group. + description: string; +} + +export const codecForGroupsSummaryResponse = (): Codec<GroupsSummaryResponse> => + buildCodecForObject<GroupsSummaryResponse>() + .property("groups", codecForList(codecForGroupEntry())) + .build("TalerMerchantApi.GroupsSummaryResponse"); + +export const codecForGroupEntry = (): Codec<GroupEntry> => + buildCodecForObject<GroupEntry>() + .property("group_name", codecForString()) + .property("group_serial", codecForNumber()) + .property("description", codecForString()) + .build("TalerMerchantApi.GroupEntry"); + +export interface GroupAddRequest { + // Unique name for the group (unique per instance). + group_name: string; + + // Description of the group. + description: string; +} + +export interface GroupAddedResponse { + // Unique ID for the group. + group_serial_id: Integer; +} + +export const codecForGroupAddedResponse = (): Codec<GroupAddedResponse> => + buildCodecForObject<GroupAddedResponse>() + .property("group_serial_id", codecForNumber()) + .build("TalerMerchantApi.GroupAddedResponse"); + +export interface PotAddRequest { + // Description of the pot. Possibly included + // in the pot message. + description: string; + + // Name of the pot. Must be unique per instance. + pot_name: string; +} + +export interface PotAddedResponse { + // Unique ID for the pot. + pot_serial_id: Integer; +} + +export const codecForPotAddedResponse = (): Codec<PotAddedResponse> => + buildCodecForObject<PotAddedResponse>() + .property("pot_serial_id", codecForNumber()) + .build("TalerMerchantApi.PotAddedResponse"); + +export interface PotModifyRequest { + // Description of the pot. Possibly included + // in the pot message. + description: string; + + // Name of the pot. Must be unique per instance. + pot_name: string; + + // Expected current totals amount in the pot. + // Should be given if new_pot_total is specified + // as this allows checking that the pot total did + // not change in the meantime. However, this is + // not enforced server-side, the client may choose + // to not use this safety-measure. + expected_pot_totals?: AmountString[]; + + // Expected new total amounts to store in the pot. + // Does **not** have to be in the same currencies as + // the existing amounts in the pot. Used to reset + // the pot and/or change the amounts. + new_pot_totals?: AmountString[]; +} + +export interface PotsSummaryResponse { + // Return pots that are present in our backend. + pots: PotEntry[]; +} + +export interface PotEntry { + // Pot identifier + pot_serial: Integer; + // Name of the pot. Must be unique per instance. + pot_name: string; + // Current total amounts in the pot. + pot_totals: AmountString[]; +} + +export const codecForPotsSummaryResponse = (): Codec<PotsSummaryResponse> => + buildCodecForObject<PotsSummaryResponse>() + .property("pots", codecForList(codecForPotEntry())) + .build("TalerMerchantApi.PotsSummaryResponse"); + +export const codecForPotEntry = (): Codec<PotEntry> => + buildCodecForObject<PotEntry>() + .property("pot_serial", codecForNumber()) + .property("pot_name", codecForString()) + .property("pot_totals", codecForList(codecForAmountString())) + .build("TalerMerchantApi.PotEntry"); + +export interface PotDetailResponse { + // Description of the pot. Possibly included + // in the pot message. + description: string; + // Name of the pot. Must be unique per instance. + pot_name: string; + // Current total amount in the pot. + pot_totals: AmountString[]; + +} + +export const codecForPotDetailResponse = (): Codec<PotDetailResponse> => + buildCodecForObject<PotDetailResponse>() + .property("description", codecForString()) + .property("pot_name", codecForString()) + .property("pot_totals", codecForList(codecForAmountString())) + .build("TalerMerchantApi.PotDetailResponse");