commit 5995765e34f9a59ec0a3d659549411b9cfa6bc96
parent 6cf375d47e91c94d985d4e626fde6fea10147067
Author: Sebastian <sebasjm@taler-systems.com>
Date: Fri, 13 Mar 2026 11:27:14 -0300
fix #11235
Diffstat:
8 files changed, 170 insertions(+), 96 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx b/packages/merchant-backoffice-ui/src/components/form/InputCurrency.tsx
@@ -45,6 +45,8 @@ export function InputCurrency<T>({
side,
}: Props<keyof T>): VNode {
const { config } = useSessionContext();
+ const id = config.currencies[config.currency].num_fractional_input_digits;
+ const step = Math.pow(10, -1 * (id ?? 0));
return (
<InputWithAddon<T>
name={name}
@@ -60,7 +62,7 @@ export function InputCurrency<T>({
expand={expand}
toStr={(v?: AmountString) => v?.split(":")[1] || ""}
fromStr={(v: string) => (!v ? undefined : `${config.currency}:${v}`)}
- inputExtra={{ min: 0, step: 0.001 }}
+ inputExtra={{ min: 0, step }}
>
{children}
</InputWithAddon>
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -20,18 +20,16 @@
*/
import {
- getMerchantAccountKycStatusSimplified,
MerchantAccountKycStatusSimplified,
- MerchantPersona,
- TalerError,
+ MerchantPersona
} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { ComponentChildren, Fragment, h, VNode } from "preact";
import { useSessionContext } from "../../context/session.js";
import { useSettingsContext } from "../../context/settings.js";
-import { useInstanceKYCDetailsLongPolling } from "../../hooks/instance.js";
import { UIElement, usePreference } from "../../hooks/preference.js";
import { LangSelector } from "./LangSelector.js";
+import { useInstanceKYCSimplifiedWorstStatusLongPolling } from "../../hooks/instance.js";
const TALER_SCREEN_ID = 17;
// const GIT_HASH = typeof __GIT_HASH__ !== "undefined" ? __GIT_HASH__ : undefined;
@@ -41,27 +39,15 @@ interface Props {
mobile?: boolean;
}
+
export function Sidebar({ mobile }: Props): VNode {
const { i18n } = useTranslationContext();
const { state, logOut, config } = useSessionContext();
- const kycStatus = useInstanceKYCDetailsLongPolling();
-
- const allKycData =
- kycStatus !== undefined &&
- !(kycStatus instanceof TalerError) &&
- kycStatus.type === "ok"
- ? kycStatus.body.kyc_data
- : [];
-
- const simplifiedKycStatus = allKycData.reduce((prev, cur) => {
- const st = getMerchantAccountKycStatusSimplified(cur.status);
- if (st > prev) return st;
- return prev;
- }, MerchantAccountKycStatusSimplified.OK);
+ const worstKycStatus = useInstanceKYCSimplifiedWorstStatusLongPolling();
const [{ persona }] = usePreference();
const hideKycMenuItem =
- simplifiedKycStatus === MerchantAccountKycStatusSimplified.OK &&
+ worstKycStatus === MerchantAccountKycStatusSimplified.OK &&
persona !== "expert";
const isLoggedIn = state.status === "loggedIn";
@@ -239,13 +225,13 @@ export function Sidebar({ mobile }: Props): VNode {
htmlElement="li"
point={UIElement.sidebar_kycStatus}
class={
- simplifiedKycStatus ===
+ worstKycStatus ===
MerchantAccountKycStatusSimplified.WARNING
? "is-warning"
- : simplifiedKycStatus ===
+ : worstKycStatus ===
MerchantAccountKycStatusSimplified.ERROR
? "is-error"
- : simplifiedKycStatus ===
+ : worstKycStatus ===
MerchantAccountKycStatusSimplified.ACTION_REQUIRED
? "is-warning"
: undefined
@@ -255,19 +241,19 @@ export function Sidebar({ mobile }: Props): VNode {
href={"#/kyc"}
class="has-icon"
style={
- simplifiedKycStatus ===
+ worstKycStatus ===
MerchantAccountKycStatusSimplified.WARNING
? {
backgroundColor: "darkorange",
color: "black",
}
- : simplifiedKycStatus ===
+ : worstKycStatus ===
MerchantAccountKycStatusSimplified.ERROR
? {
backgroundColor: "#e93c3c",
color: "black",
}
- : simplifiedKycStatus ===
+ : worstKycStatus ===
MerchantAccountKycStatusSimplified.ACTION_REQUIRED
? {
backgroundColor: "darkorange",
@@ -365,7 +351,9 @@ export function Sidebar({ mobile }: Props): VNode {
</li>
<li>
<div class="has-icon">
- <span class="icon" style={{ width: "3rem" }}>ID</span>
+ <span class="icon" style={{ width: "3rem" }}>
+ ID
+ </span>
<span class="menu-item-label">{state.instance}</span>
</div>
</li>
diff --git a/packages/merchant-backoffice-ui/src/hooks/instance.ts b/packages/merchant-backoffice-ui/src/hooks/instance.ts
@@ -16,6 +16,9 @@
import {
AccessToken,
+ getMerchantAccountKycStatusSimplified,
+ MerchantAccountKycStatusSimplified,
+ TalerError,
TalerHttpError,
TalerMerchantManagementResultByMethod,
} from "@gnu-taler/taler-util";
@@ -103,6 +106,40 @@ export function useInstanceKYCDetailsLongPolling() {
return result;
}
+export function useInstanceKYCSimplifiedWorstStatusLongPolling() {
+ const kycStatus = useInstanceKYCDetailsLongPolling();
+
+ const allKycData =
+ kycStatus !== undefined &&
+ !(kycStatus instanceof TalerError) &&
+ kycStatus.type === "ok"
+ ? kycStatus.body.kyc_data
+ : [];
+
+ return allKycData.reduce((prev, cur) => {
+ const st = getMerchantAccountKycStatusSimplified(cur.status);
+ if (st > prev) return st;
+ return prev;
+ }, MerchantAccountKycStatusSimplified.OK);
+}
+
+export function useInstanceKYCSimplifiedBestStatusLongPolling() {
+ const kycStatus = useInstanceKYCDetailsLongPolling();
+
+ const allKycData =
+ kycStatus !== undefined &&
+ !(kycStatus instanceof TalerError) &&
+ kycStatus.type === "ok"
+ ? kycStatus.body.kyc_data
+ : [];
+
+ return allKycData.reduce((prev, cur) => {
+ const st = getMerchantAccountKycStatusSimplified(cur.status);
+ if (st < prev) return st;
+ return prev;
+ }, MerchantAccountKycStatusSimplified.ERROR);
+}
+
export function useInstanceKYCDetails() {
const { state, lib } = useSessionContext();
const token = state.token;
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/list/index.tsx
@@ -21,10 +21,14 @@
import {
HttpStatusCode,
+ MerchantAccountKycStatusSimplified,
TalerError,
- assertUnreachable
+ assertUnreachable,
} from "@gnu-taler/taler-util";
-import { NotificationCardBulma, useTranslationContext } from "@gnu-taler/web-util/browser";
+import {
+ NotificationCardBulma,
+ useTranslationContext,
+} from "@gnu-taler/web-util/browser";
import { Fragment, VNode, h } from "preact";
import { ErrorLoadingMerchant } from "../../../../components/ErrorLoadingMerchant.js";
import { Loading } from "../../../../components/exception/loading.js";
@@ -32,6 +36,7 @@ import { useInstanceBankAccounts } from "../../../../hooks/bank.js";
import { LoginPage } from "../../../login/index.js";
import { NotFoundPageOrAdminCreate } from "../../../notfound/index.js";
import { CardTable } from "./Table.js";
+import { useInstanceKYCSimplifiedBestStatusLongPolling } from "../../../../hooks/instance.js";
const TALER_SCREEN_ID = 34;
@@ -40,9 +45,66 @@ interface Props {
onSelect: (id: string) => void;
}
-export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode {
+export function MissingBankAccountsWarning(): VNode {
+ const { i18n } = useTranslationContext();
+ const result = useInstanceBankAccounts();
+
+ if (!result) return <Loading />;
+ if (result instanceof TalerError) {
+ return <Fragment />;
+ }
+ if (result.type === "fail") {
+ return <Fragment />;
+ }
+ if (!result.body.accounts.length) {
+ return (
+ <NotificationCardBulma
+ notification={{
+ type: "WARN",
+ message: i18n.str`You must provide a bank account to receive payments.`,
+ description: i18n.str`Without this information, you cannot create new payment orders that are transferred to a bank account.`,
+ }}
+ />
+ );
+ }
+ return <Fragment />;
+}
+
+export function LimitedKycActionWarning(): VNode {
const { i18n } = useTranslationContext();
+ const status = useInstanceKYCSimplifiedBestStatusLongPolling();
+ switch (status) {
+ case MerchantAccountKycStatusSimplified.ACTION_REQUIRED:
+ return (
+ <NotificationCardBulma
+ notification={{
+ type: "WARN",
+ message: i18n.str`You must complete kyc requirements to receive payments.`,
+ description: i18n.str`Without this information, you cannot create new payment orders that are transferred to a bank account.`,
+ }}
+ />
+ );
+ case MerchantAccountKycStatusSimplified.WARNING:
+ case MerchantAccountKycStatusSimplified.ERROR:
+ return (
+ <NotificationCardBulma
+ notification={{
+ type: "WARN",
+ message: i18n.str`You must must pass KYC to receive payments.`,
+ description: i18n.str`Without this information, you cannot create new payment orders that are transferred to a bank account.`,
+ }}
+ />
+ );
+ case MerchantAccountKycStatusSimplified.OK:
+ return <Fragment />;
+ default:
+ assertUnreachable(status);
+ }
+ return <Fragment />;
+}
+
+export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode {
const result = useInstanceBankAccounts();
if (!result) return <Loading />;
@@ -65,15 +127,7 @@ export default function ListOtpDevices({ onCreate, onSelect }: Props): VNode {
return (
<Fragment>
- {result.body.accounts.length < 1 && (
- <NotificationCardBulma
- notification={{
- type: "WARN",
- message: i18n.str`You must provide a bank account to receive payments.`,
- description: i18n.str`Without this information, you cannot create new payment orders that are transferred to a bank account.`,
- }}
- />
- )}
+ <MissingBankAccountsWarning />
<section class="section is-main-section">
<CardTable
accounts={result.body.accounts.map((o) => ({
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/kyc/list/ListPage.tsx
@@ -21,10 +21,9 @@
import {
assertUnreachable,
- getMerchantAccountKycStatusSimplified,
Paytos,
Result,
- TalerMerchantApi,
+ TalerMerchantApi
} from "@gnu-taler/taler-util";
import { useTranslationContext } from "@gnu-taler/web-util/browser";
import { h, VNode } from "preact";
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/orders/create/CreatePage.tsx
@@ -65,6 +65,7 @@ import { rate } from "../../../../utils/amount.js";
import { undefinedIfEmpty } from "../../../../utils/table.js";
import { FragmentPersonaFlag } from "../../../../components/menu/SideBar.js";
import { Tooltip } from "../../../../components/Tooltip.js";
+import { MissingBankAccountsWarning } from "../../accounts/list/index.js";
const TALER_SCREEN_ID = 42;
@@ -443,39 +444,38 @@ export function CreatePage({
return (
<div>
<LocalNotificationBannerBulma notification={notification} />
+ <MissingBankAccountsWarning />
<section class="section is-main-section">
<div class="columns">
<div class="column" />
<div class="column is-four-fifths">
{/* // FIXME: translating plural singular */}
- <InputGroup
- name="inventory_products"
- label={i18n.str`Manage products in order`}
- alternative={
- allProducts.length > 0 && (
- <p>
- <i18n.Translate>
- {allProducts.length} products with a total price of{" "}
- <RenderAmountBulma
- value={totalPrice.amount}
- specMap={config.currencies}
- />
- .
- </i18n.Translate>
- </p>
- )
- }
- tooltip={i18n.str`Manage list of products in the order.`}
- >
- <InventoryProductForm
- currentProducts={value.inventoryProducts || {}}
- onAddProduct={addProductToTheInventoryList}
- inventory={instanceInventory}
- />
-
- <FragmentPersonaFlag
- point={UIElement.option_advanceOrderCreation}
+ <FragmentPersonaFlag point={UIElement.option_advanceOrderCreation}>
+ <InputGroup
+ name="inventory_products"
+ label={i18n.str`Manage products in order`}
+ alternative={
+ allProducts.length > 0 && (
+ <p>
+ <i18n.Translate>
+ {allProducts.length} products with a total price of{" "}
+ <RenderAmountBulma
+ value={totalPrice.amount}
+ specMap={config.currencies}
+ />
+ .
+ </i18n.Translate>
+ </p>
+ )
+ }
+ tooltip={i18n.str`Manage list of products in the order.`}
>
+ <InventoryProductForm
+ currentProducts={value.inventoryProducts || {}}
+ onAddProduct={addProductToTheInventoryList}
+ inventory={instanceInventory}
+ />
+
<NonInventoryProductFrom
productToEdit={editingProduct}
onAddProduct={(p) => {
@@ -483,28 +483,28 @@ export function CreatePage({
return addNewProduct(p);
}}
/>
- </FragmentPersonaFlag>
- {allProducts.length > 0 && (
- <ProductList
- list={allProducts}
- actions={[
- {
- name: i18n.str`Remove`,
- tooltip: i18n.str`Remove this product from the order.`,
- handler: (e, index) => {
- if (e.product_id) {
- removeProductFromTheInventoryList(e.product_id);
- } else {
- removeFromNewProduct(index);
- setEditingProduct(e);
- }
+ {allProducts.length > 0 && (
+ <ProductList
+ list={allProducts}
+ actions={[
+ {
+ name: i18n.str`Remove`,
+ tooltip: i18n.str`Remove this product from the order.`,
+ handler: (e, index) => {
+ if (e.product_id) {
+ removeProductFromTheInventoryList(e.product_id);
+ } else {
+ removeFromNewProduct(index);
+ setEditingProduct(e);
+ }
+ },
},
- },
- ]}
- />
- )}
- </InputGroup>
+ ]}
+ />
+ )}
+ </InputGroup>
+ </FragmentPersonaFlag>
<FormProvider<Entity>
errors={errors}
diff --git a/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx b/packages/merchant-backoffice-ui/src/paths/newAccount/index.tsx
@@ -15,7 +15,6 @@
*/
import {
- AbsoluteTime,
assertUnreachable,
buildCodecForObject,
Codec,
@@ -26,7 +25,7 @@ import {
HttpStatusCode,
InstanceConfigurationMessage,
MerchantAuthMethod,
- TanChannel,
+ TanChannel
} from "@gnu-taler/taler-util";
import {
buildStorageKey,
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
@@ -1726,12 +1726,7 @@ export class TalerMerchantInstanceHttpClient {
token: AccessToken,
orderId: string,
params: TalerMerchantApi.GetOrderRequestParams = {},
- ): Promise<
- | OperationOk<TalerMerchantApi.MerchantOrderStatusResponse>
- | OperationFail<TalerErrorCode.MERCHANT_GENERIC_ORDER_UNKNOWN>
- | OperationFail<TalerErrorCode.MERCHANT_GENERIC_INSTANCE_UNKNOWN>
- | OperationFail<HttpStatusCode.Unauthorized>
- > {
+ ) {
const url = new URL(`private/orders/${orderId}`, this.baseUrl);
if (params.allowRefundedForRepurchase !== undefined) {