taler-typescript-core

Wallet core logic and WebUIs for various components
Log | Files | Refs | Submodules | README | LICENSE

commit 50207a52c0e6478c2a97343766a6f815ae39d2fd
parent d0c4560e8aaa93f7fd1de784b8e8aac36b6d9e39
Author: Sebastian <sebasjm@taler-systems.com>
Date:   Mon, 23 Feb 2026 12:34:36 -0300

fix #11120

Diffstat:
Mpackages/merchant-backoffice-ui/src/components/menu/SideBar.tsx | 5+++++
Mpackages/merchant-backoffice-ui/src/hooks/preference.ts | 1+
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/create/CreatePage.tsx | 16++++++++++++++++
Mpackages/merchant-backoffice-ui/src/paths/instance/accounts/update/UpdatePage.tsx | 19+++++++++++++++++++
Mpackages/taler-util/src/http-client/merchant.ts | 6+++++-
Mpackages/taler-util/src/time.ts | 4++++
Mpackages/taler-util/src/types-taler-merchant.ts | 121+++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------------------
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> =>