commit 50207a52c0e6478c2a97343766a6f815ae39d2fd
parent d0c4560e8aaa93f7fd1de784b8e8aac36b6d9e39
Author: Sebastian <sebasjm@taler-systems.com>
Date: Mon, 23 Feb 2026 12:34:36 -0300
fix #11120
Diffstat:
7 files changed, 138 insertions(+), 34 deletions(-)
diff --git a/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx b/packages/merchant-backoffice-ui/src/components/menu/SideBar.tsx
@@ -489,6 +489,7 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap {
[UIElement.option_ageRestriction]: false,
[UIElement.option_refreshableScopes]: false,
[UIElement.option_advanceProductTaxes]: false,
+ [UIElement.option_exraWireSubject]: true,
};
case "offline-vending-machine":
return {
@@ -522,6 +523,7 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap {
[UIElement.sidebar_statistics]: false,
[UIElement.option_refreshableScopes]: false,
[UIElement.option_advanceProductTaxes]: false,
+ [UIElement.option_exraWireSubject]: false,
};
// case "inperson-vending-with-inventory":
case "point-of-sale":
@@ -556,6 +558,7 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap {
[UIElement.sidebar_statistics]: false,
[UIElement.option_refreshableScopes]: false,
[UIElement.option_advanceProductTaxes]: false,
+ [UIElement.option_exraWireSubject]: false,
};
case "digital-publishing":
return {
@@ -589,6 +592,7 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap {
[UIElement.option_inventoryTaxes]: false,
[UIElement.option_refreshableScopes]: false,
[UIElement.option_advanceProductTaxes]: false,
+ [UIElement.option_exraWireSubject]: false,
};
case "e-commerce":
return {
@@ -622,6 +626,7 @@ export function getAvailableForPersona(p: MerchantPersona): ElementMap {
[UIElement.option_inventoryTaxes]: false,
[UIElement.option_refreshableScopes]: false,
[UIElement.option_advanceProductTaxes]: false,
+ [UIElement.option_exraWireSubject]: false,
};
}
}
diff --git a/packages/merchant-backoffice-ui/src/hooks/preference.ts b/packages/merchant-backoffice-ui/src/hooks/preference.ts
@@ -58,6 +58,7 @@ export enum UIElement {
option_inventoryTaxes,
option_refreshableScopes,
option_advanceProductTaxes,
+ option_exraWireSubject,
}
export interface Preferences {
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx
@@ -68,6 +68,8 @@ interface Props {
onBack?: () => void;
}
+const SUBJECT_METADATA_REGEX = /^[a-zA-Z0-9-.:]{1,40}$/;
+
export function CreatePage({ onCreated, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
@@ -93,6 +95,11 @@ export function CreatePage({ onCreated, onBack }: Props): VNode {
const safeParsed = parsed?.type === "fail" ? undefined : parsed?.body;
const errors = undefinedIfEmpty<FormErrors<Entity>>({
payto_uri: !state.payto_uri ? i18n.str`Required` : undefined,
+ extra_wire_subject_metadata: !state.extra_wire_subject_metadata
+ ? undefined
+ : !SUBJECT_METADATA_REGEX.test(state.extra_wire_subject_metadata)
+ ? i18n.str`The extra data should be between 1 and 40 characters of letters, numbers, dot, dash and colon.`
+ : undefined,
credit_facade_credentials: undefinedIfEmpty(
!state.credit_facade_credentials || !state.credit_facade_url
@@ -148,6 +155,7 @@ export function CreatePage({ onCreated, onBack }: Props): VNode {
payto_uri: state.payto_uri!,
credit_facade_credentials,
credit_facade_url,
+ extra_wire_subject_metadata: state.extra_wire_subject_metadata
};
const [notification, safeFunctionHandler] = useLocalNotificationBetter();
@@ -250,6 +258,14 @@ export function CreatePage({ onCreated, onBack }: Props): VNode {
name="payto_uri"
label={i18n.str`Account details`}
/>
+ <FragmentPersonaFlag point={UIElement.option_exraWireSubject}>
+ <Input<Entity>
+ name="extra_wire_subject_metadata"
+ label={i18n.str`Extra subject`}
+ expand
+ tooltip={i18n.str`Additional text to include in the wire transfer subject when settling the payment`}
+ />
+ </FragmentPersonaFlag>
<FragmentPersonaFlag point={UIElement.action_useRevenueApi}>
<div class="message-body" style={{ marginBottom: 10 }}>
<p>
diff --git a/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx b/packages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx
@@ -70,6 +70,8 @@ interface Props {
account: Entity;
}
+const SUBJECT_METADATA_REGEX = /^[a-zA-Z0-9-.:]{1,40}$/;
+
export function UpdatePage({ account, onUpdated, onBack }: Props): VNode {
const { i18n } = useTranslationContext();
@@ -82,6 +84,7 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode {
const [state, setState] = useState<Partial<FormType>>({
payto_uri: account.payto_uri,
credit_facade_url: account.credit_facade_url,
+ extra_wire_subject_metadata: account.extra_wire_subject_metadata,
credit_facade_credentials: {
// @ts-expect-error unofficial unedited value
type: "unedit",
@@ -103,6 +106,11 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode {
const errors = undefinedIfEmpty<FormErrors<FormType>>({
payto_uri: !state.payto_uri ? i18n.str`Required` : undefined,
+ extra_wire_subject_metadata: !state.extra_wire_subject_metadata
+ ? undefined
+ : !SUBJECT_METADATA_REGEX.test(state.extra_wire_subject_metadata)
+ ? i18n.str`The extra data should be between 1 and 40 characters of letters, numbers, dot, dash and colon.`
+ : undefined,
credit_facade_url: !state.credit_facade_url
? undefined
@@ -218,6 +226,9 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode {
{
credit_facade_credentials,
credit_facade_url,
+ extra_wire_subject_metadata: !state.extra_wire_subject_metadata
+ ? undefined
+ : state.extra_wire_subject_metadata,
},
[],
],
@@ -335,6 +346,14 @@ export function UpdatePage({ account, onUpdated, onBack }: Props): VNode {
name="payto_uri"
label={i18n.str`Account`}
/>
+ <FragmentPersonaFlag point={UIElement.option_exraWireSubject}>
+ <Input<Entity>
+ name="extra_wire_subject_metadata"
+ label={i18n.str`Extra subject`}
+ expand
+ tooltip={i18n.str`Additional text to include in the wire transfer subject when settling the payment`}
+ />
+ </FragmentPersonaFlag>
<FragmentPersonaFlag point={UIElement.action_useRevenueApi}>
<div class="message-body" style={{ marginBottom: 10 }}>
<p>
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
@@ -1566,7 +1566,7 @@ export class TalerMerchantInstanceHttpClient {
}
/**
- * https://docs.taler.net/core/api-merchant.html#inspecting-orders
+ * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-orders
*/
async listOrders(
token: AccessToken,
@@ -1587,6 +1587,10 @@ export class TalerMerchantInstanceHttpClient {
const time = AbsoluteTime.toProtocolTimestamp(params.date);
url.searchParams.set("date_s", String(time.t_s));
}
+ if (params.maxAge && !Duration.isForever(params.maxAge)) {
+ const time = Duration.toTalerProtocolDuration(params.maxAge);
+ url.searchParams.set("max_age", String(time.d_us));
+ }
if (params.timeout) {
url.searchParams.set("timeout_ms", String(params.timeout));
}
diff --git a/packages/taler-util/src/time.ts b/packages/taler-util/src/time.ts
@@ -398,6 +398,10 @@ export namespace Duration {
return { d_ms: "forever" };
}
+ export function isForever(d: Duration): boolean {
+ return d.d_ms === "forever";
+ }
+
export function getZero(): Duration {
return { d_ms: 0 };
}
diff --git a/packages/taler-util/src/types-taler-merchant.ts b/packages/taler-util/src/types-taler-merchant.ts
@@ -52,6 +52,7 @@ import {
} from "./index.js";
import {
AbsoluteTime,
+ Duration,
TalerProtocolDuration,
TalerProtocolTimestamp,
codecForDuration,
@@ -1627,6 +1628,12 @@ export interface ListOrdersRequestParams {
*/
date?: AbsoluteTime;
/**
+ * Relative time. Only return orders younger than the specified
+ * age. Only applicable if delta is positive. If both max_age
+ * and date_s are given, the larger of the two applies
+ */
+ maxAge?: Duration;
+ /**
* Starting product_serial_id for an iteration.
* Since protocol v12.
*/
@@ -2314,6 +2321,22 @@ export interface AccountAddDetails {
// or PATCH requests to update (or delete) credentials.
// To really delete credentials, set them to the type: "none".
credit_facade_credentials?: FacadeCredentials;
+
+ // Additional text to include in the wire transfer subject when
+ // settling the payment. Note that the merchant MUST use this
+ // consistently for the same merchant_pub and merchant_payto_uri
+ // as during aggregation *any* of these values may be selected
+ // for the actual aggregated wire transfer. If a merchant wants
+ // to use different extra_subject values for the same IBAN,
+ // it should thus create multiple instances (with different
+ // merchant_pub values). When changing the extra_subject,
+ // the change may thus not be immediately reflected in the
+ // settlements.
+ //
+ // Must match [a-zA-Z0-9-.:]{1, 40}
+ //
+ // Optional. Since **v27**.
+ extra_wire_subject_metadata?: string;
}
// Fixed-point decimal string in the form "<integer>[.<fraction>]".
@@ -2369,6 +2392,22 @@ export interface AccountPatchDetails {
// If the argument is omitted, the old credentials
// are simply preserved.
credit_facade_credentials?: FacadeCredentials;
+
+ // Additional text to include in the wire transfer subject when
+ // settling the payment. Note that the merchant MUST use this
+ // consistently for the same merchant_pub and merchant_payto_uri
+ // as during aggregation *any* of these values may be selected
+ // for the actual aggregated wire transfer. If a merchant wants
+ // to use different extra_subject values for the same IBAN,
+ // it should thus create multiple instances (with different
+ // merchant_pub values). When changing the extra_subject,
+ // the change may thus not be immediately reflected in the
+ // settlements.
+ //
+ // Must match [a-zA-Z0-9-.:]{1, 40}
+ //
+ // Optional. Since **v27**.
+ extra_wire_subject_metadata?: string;
}
export interface AccountsSummaryResponse {
@@ -2405,6 +2444,22 @@ export interface BankAccountDetail {
// true if this account is active,
// false if it is historic.
active?: boolean;
+
+ // Additional text to include in the wire transfer subject when
+ // settling the payment. Note that the merchant MUST use this
+ // consistently for the same merchant_pub and merchant_payto_uri
+ // as during aggregation *any* of these values may be selected
+ // for the actual aggregated wire transfer. If a merchant wants
+ // to use different extra_subject values for the same IBAN,
+ // it should thus create multiple instances (with different
+ // merchant_pub values). When changing the extra_subject,
+ // the change may thus not be immediately reflected in the
+ // settlements.
+ //
+ // Must match [a-zA-Z0-9-.:]{1, 40}
+ //
+ // Optional. Since **v27**.
+ extra_wire_subject_metadata?: string;
}
export interface CategoryListResponse {
@@ -3195,7 +3250,6 @@ export interface ExpectedTransferEntry {
}
export interface ExpectedTransferDetails {
-
// List of orders that are settled by this wire
// transfer according to the exchange. Only
// available if last_http_status is 200.
@@ -3207,7 +3261,6 @@ export interface ExpectedTransferDetails {
// wire fee for the execution_time from the exchange.
// (If missing, this is thus indicative of a minor error.)
wire_fee?: AmountString;
-
}
export interface ExchangeTransferReconciliationDetails {
@@ -4184,10 +4237,7 @@ export const codecForTalerMerchantConfigResponse =
"report_generators",
codecOptionalDefault(codecForList(codecForString()), []),
)
- .property(
- "phone_regex",
- codecOptional(codecForString()),
- )
+ .property("phone_regex", codecOptional(codecForString()))
.property("exchanges", codecForList(codecForExchangeConfigInfo()))
.property("implementation", codecOptional(codecForString()))
.property(
@@ -4500,6 +4550,7 @@ export const codecForBankAccountDetail = (): Codec<BankAccountDetail> =>
buildCodecForObject<BankAccountDetail>()
.property("payto_uri", codecForPaytoString())
.property("h_wire", codecForString())
+ .property("extra_wire_subject_metadata", codecOptional(codecForString()))
.property("salt", codecForString())
.property("credit_facade_url", codecOptional(codecForURLString()))
.property("active", codecOptional(codecForBoolean()))
@@ -4881,18 +4932,23 @@ export const codecForExpectedTansferList = (): Codec<ExpectedTransferList> =>
.property("incoming", codecForList(codecForExpectedTransferEntry()))
.build("TalerMerchantApi.ExpectedTransferList");
-export const codecForExchangeTransferReconciliationDetails = (): Codec<ExchangeTransferReconciliationDetails> =>
- buildCodecForObject<ExchangeTransferReconciliationDetails>()
- .property("deposit_fee", (codecForAmountString()))
- .property("order_id", (codecForString()))
- .property("remaining_deposit", (codecForAmountString()))
- .build("TalerMerchantApi.ExchangeTransferReconciliationDetails");
-
-export const codecForExpectedTransferDetails = (): Codec<ExpectedTransferDetails> =>
- buildCodecForObject<ExpectedTransferDetails>()
- .property("reconciliation_details", codecForList(codecForExchangeTransferReconciliationDetails()))
- .property("wire_fee", codecForAmountString())
- .build("TalerMerchantApi.ExpectedTransferDetails");
+export const codecForExchangeTransferReconciliationDetails =
+ (): Codec<ExchangeTransferReconciliationDetails> =>
+ buildCodecForObject<ExchangeTransferReconciliationDetails>()
+ .property("deposit_fee", codecForAmountString())
+ .property("order_id", codecForString())
+ .property("remaining_deposit", codecForAmountString())
+ .build("TalerMerchantApi.ExchangeTransferReconciliationDetails");
+
+export const codecForExpectedTransferDetails =
+ (): Codec<ExpectedTransferDetails> =>
+ buildCodecForObject<ExpectedTransferDetails>()
+ .property(
+ "reconciliation_details",
+ codecForList(codecForExchangeTransferReconciliationDetails()),
+ )
+ .property("wire_fee", codecForAmountString())
+ .build("TalerMerchantApi.ExpectedTransferDetails");
export const codecForTransferDetails = (): Codec<TransferDetails> =>
buildCodecForObject<TransferDetails>()
@@ -4905,21 +4961,20 @@ export const codecForTransferDetails = (): Codec<TransferDetails> =>
.property("expected", codecOptional(codecForBoolean()))
.build("TalerMerchantApi.TransferDetails");
-export const codecForExpectedTransferEntry =
- (): Codec<ExpectedTransferEntry> =>
- buildCodecForObject<ExpectedTransferEntry>()
- .property("expected_credit_amount", codecOptional(codecForAmountString()))
- .property("wtid", codecForString())
- .property("payto_uri", codecForPaytoString())
- .property("exchange_url", codecForURLString())
- .property("expected_transfer_serial_id", codecOptional(codecForNumber()))
- .property("execution_time", codecOptional(codecForTimestamp))
- .property("validated", codecForBoolean())
- .property("confirmed", codecForBoolean())
- .property("last_http_status", codecForNumber())
- .property("last_ec", codecForNumber())
- .property("last_error_detail", codecOptional(codecForAny()))
- .build("TalerMerchantApi.ExpectedTransferDetails");
+export const codecForExpectedTransferEntry = (): Codec<ExpectedTransferEntry> =>
+ buildCodecForObject<ExpectedTransferEntry>()
+ .property("expected_credit_amount", codecOptional(codecForAmountString()))
+ .property("wtid", codecForString())
+ .property("payto_uri", codecForPaytoString())
+ .property("exchange_url", codecForURLString())
+ .property("expected_transfer_serial_id", codecOptional(codecForNumber()))
+ .property("execution_time", codecOptional(codecForTimestamp))
+ .property("validated", codecForBoolean())
+ .property("confirmed", codecForBoolean())
+ .property("last_http_status", codecForNumber())
+ .property("last_ec", codecForNumber())
+ .property("last_error_detail", codecOptional(codecForAny()))
+ .build("TalerMerchantApi.ExpectedTransferDetails");
export const codecForOtpDeviceSummaryResponse =
(): Codec<OtpDeviceSummaryResponse> =>