taler-typescript-core

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

commit 89bc314de7aa7613c2f54a4c2b9ca3d13a5f1e78
parent 27ffa807c38fa41145d9036fe3b9adbf83d4b493
Author: Iván Ávalos <avalos@disroot.org>
Date:   Wed, 12 Nov 2025 09:11:46 +0100

wallet-core: complete subscription and discount requests

Diffstat:
Mpackages/taler-util/src/types-taler-wallet.ts | 26++++++++++++++++++++++++++
Mpackages/taler-wallet-core/src/tokenFamilies.ts | 119++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Mpackages/taler-wallet-core/src/wallet-api-types.ts | 25++++++++++++++++++++++++-
Mpackages/taler-wallet-core/src/wallet.ts | 38+++++++++++++++++++++++++++++++++++++-
4 files changed, 190 insertions(+), 18 deletions(-)

diff --git a/packages/taler-util/src/types-taler-wallet.ts b/packages/taler-util/src/types-taler-wallet.ts @@ -2751,6 +2751,21 @@ export interface DeleteDiscountRequest { tokenFamilyHash: string; } +export type ListSubscriptionsRequest = ListDiscountsRequest; + +export interface ListSubscriptionsResponse { + subscriptions: SubscriptionListDetail[]; +} + +export type SubscriptionListDetail = Omit<DiscountListDetail, 'tokensAvailable'>; + +export interface DeleteSubscriptionRequest { + /** + * Hash of token family info. + */ + tokenFamilyHash: string; +} + export const codecForListDiscountsRequest = (): Codec<ListDiscountsRequest> => buildCodecForObject<ListDiscountsRequest>() .property("tokenIssuePubHash", codecOptional(codecForString())) @@ -2762,6 +2777,17 @@ export const codecForDeleteDiscountRequest = (): Codec<DeleteDiscountRequest> => .property("tokenFamilyHash", codecForString()) .build("DeleteDiscount"); +export const codecForListSubscriptionsRequest = (): Codec<ListSubscriptionsRequest> => + buildCodecForObject<ListSubscriptionsRequest>() + .property("tokenIssuePubHash", codecOptional(codecForString())) + .property("merchantBaseUrl", codecOptional(codecForCanonBaseUrl())) + .build("ListSubscriptions"); + +export const codecForDeleteSubscriptionRequest = (): Codec<DeleteSubscriptionRequest> => + buildCodecForObject<DeleteSubscriptionRequest>() + .property("tokenFamilyHash", codecForString()) + .build("DeleteSubscription"); + export interface CoreApiRequestEnvelope { id: string; operation: string; diff --git a/packages/taler-wallet-core/src/tokenFamilies.ts b/packages/taler-wallet-core/src/tokenFamilies.ts @@ -23,7 +23,10 @@ import { DiscountListDetail, EmptyObject, ListDiscountsResponse, + ListSubscriptionsResponse, Logger, + MerchantContractTokenKind, + SubscriptionListDetail, } from "@gnu-taler/taler-util"; import { WalletExecutionContext } from "./index.js"; import { TokenRecord } from "./db.js"; @@ -34,17 +37,13 @@ import { const logger = new Logger("tokenFamilies.ts"); -// FIXME: unit test discount grouping +// FIXME: unit test for discount grouping function groupDiscounts(tokens: TokenRecord[]): DiscountListDetail[] { const groupedIdx: number[] = []; const items: DiscountListDetail[] = []; - - tokens = tokens.map(t => { - t.tokenFamilyHash = (t.tokenFamilyHash) - ? t.tokenFamilyHash - : TokenRecord.hashInfo(t); - return t; - }); + tokens = tokens + .filter(t => t.tokenFamilyHash) + .sort((a, b) => a.validBefore - b.validBefore); // compare tokens against each other, // except the ones already in a group. @@ -80,6 +79,46 @@ function groupDiscounts(tokens: TokenRecord[]): DiscountListDetail[] { return items; } +// FIXME: unit test for subscription grouping +function groupSubscriptions(tokens: TokenRecord[]): SubscriptionListDetail[] { + const groupedIdx: number[] = []; + const items: SubscriptionListDetail[] = []; + tokens = tokens + .filter(t => t.tokenFamilyHash) + .sort((a, b) => a.validBefore - b.validBefore); + + // compare tokens against each other, + // except the ones already in a group. + for (let a = 0; a < tokens.length; a++) { + if (groupedIdx.includes(a)) continue; + let tokenA = tokens[a]; + const item: SubscriptionListDetail = { + tokenFamilyHash: tokenA.tokenFamilyHash!, + tokenIssuePubHash: tokenA.tokenIssuePubHash, + merchantBaseUrl: tokenA.merchantBaseUrl, + name: tokenA.name, + description: tokenA.description, + descriptionI18n: tokenA.descriptionI18n, + validityStart: tokenA.tokenIssuePub.signature_validity_start, + validityEnd: tokenA.tokenIssuePub.signature_validity_end, + }; + + for (let b = 0; b < tokens.length; b++) { + if (b === a) continue; + if (groupedIdx.includes(b)) continue; + let tokenB = tokens[b]; + if (tokenA.tokenFamilyHash === tokenB.tokenFamilyHash) { + groupedIdx.push(b); + } + } + + groupedIdx.push(a); + items.push(item); + } + + return items; +} + export async function listDiscounts( wex: WalletExecutionContext, tokenIssuePubHash?: string, @@ -90,8 +129,9 @@ export async function listDiscounts( }, async (tx) => { return (await tx.tokens.getAll()) .filter(t => isTokenValid(t)) + .filter(t => t.kind === MerchantContractTokenKind.Discount) .filter(t => !tokenIssuePubHash || t.tokenIssuePubHash === tokenIssuePubHash) - .filter(t => !merchantBaseUrl || t.merchantBaseUrl === merchantBaseUrl); + .filter(t => !merchantBaseUrl || t.merchantBaseUrl === merchantBaseUrl) }); if (tokens.length === 0) { @@ -103,6 +143,30 @@ export async function listDiscounts( }; } +export async function listSubscriptions( + wex: WalletExecutionContext, + tokenIssuePubHash?: string, + merchantBaseUrl?: string, +): Promise<ListSubscriptionsResponse> { + const tokens: TokenRecord[] = await wex.db.runReadOnlyTx({ + storeNames: ["tokens"], + }, async (tx) => { + return (await tx.tokens.getAll()) + .filter(t => isTokenValid(t)) + .filter(t => t.kind === MerchantContractTokenKind.Subscription) + .filter(t => !tokenIssuePubHash || t.tokenIssuePubHash === tokenIssuePubHash) + .filter(t => !merchantBaseUrl || t.merchantBaseUrl === merchantBaseUrl); + }); + + if (tokens.length === 0) { + return { subscriptions: [] }; + } + + return { + subscriptions: groupSubscriptions(tokens), + }; +} + export async function deleteDiscount( wex: WalletExecutionContext, tokenFamilyHash: string, @@ -111,13 +175,7 @@ export async function deleteDiscount( storeNames: ["tokens"], }, async (tx) => { const tokens = (await tx.tokens.getAll()) - .map(t => { - // FIXME: write proper DB fixup to generate hashes - t.tokenFamilyHash = (t.tokenFamilyHash) - ? t.tokenFamilyHash - : TokenRecord.hashInfo(t); - return t; - }) + .filter(t => t.kind === MerchantContractTokenKind.Discount) .filter(t => t.tokenFamilyHash === tokenFamilyHash); let inUse: boolean = false; @@ -145,3 +203,32 @@ export async function deleteDiscount( return {}; } + +export async function deleteSubscription( + wex: WalletExecutionContext, + tokenFamilyHash: string, +): Promise<EmptyObject> { + await wex.db.runReadWriteTx({ + storeNames: ["tokens"], + }, async (tx) => { + const tokens = (await tx.tokens.getAll()) + .filter(t => t.kind === MerchantContractTokenKind.Subscription) + .filter(t => t.tokenFamilyHash === tokenFamilyHash) + .sort((a, b) => a.validBefore - b.validBefore); + + let inUse: boolean = false; + for (const token of tokens) { + if (isTokenInUse(token)) { + inUse = true; + return; + } + } + + // FIXME: proper GANA error + if (inUse) { + throw Error("One or more tokens in this family are in use"); + } + }); + + return {}; +} diff --git a/packages/taler-wallet-core/src/wallet-api-types.ts b/packages/taler-wallet-core/src/wallet-api-types.ts @@ -193,6 +193,9 @@ import { MailboxConfiguration, SendTalerUriMailboxMessageRequest, MailboxMessageRecord, + ListSubscriptionsRequest, + ListSubscriptionsResponse, + DeleteSubscriptionRequest, } from "@gnu-taler/taler-util"; import { AddBackupProviderRequest, @@ -315,6 +318,8 @@ export enum WalletApiOperation { // Tokens and token families ListDiscounts = "listDiscounts", DeleteDiscount = "deleteDiscount", + ListSubscriptions = "listSubscriptions", + DeleteSubscription = "deleteSubscription", // Donau SetDonau = "setDonau", @@ -832,7 +837,7 @@ export type StartRefundQueryOp = { /** * List discount tokens stored in the wallet. Listed tokens - * will be grouped by expiration date (@e validityEnd) + * will be grouped based on token family details. */ export type ListDiscountsOp = { op: WalletApiOperation.ListDiscounts; @@ -846,6 +851,22 @@ export type DeleteDiscountOp = { response: EmptyObject; }; +/** + * List subscription tokens stored in the wallet. Listed tokens + * will be grouped based on token family details. + */ +export type ListSubscriptionsOp = { + op: WalletApiOperation.ListSubscriptions; + request: ListSubscriptionsRequest; + response: ListSubscriptionsResponse; +}; + +export type DeleteSubscriptionOp = { + op: WalletApiOperation.DeleteSubscription; + request: DeleteSubscriptionRequest; + response: EmptyObject; +}; + // group: Global Currency management export type ListGlobalCurrencyAuditorsOp = { @@ -1720,6 +1741,8 @@ export type WalletOperations = { [WalletApiOperation.GetBankingChoicesForPayto]: GetBankingChoicesForPaytoOp; [WalletApiOperation.ListDiscounts]: ListDiscountsOp; [WalletApiOperation.DeleteDiscount]: DeleteDiscountOp; + [WalletApiOperation.ListSubscriptions]: ListSubscriptionsOp; + [WalletApiOperation.DeleteSubscription]: DeleteSubscriptionOp; [WalletApiOperation.StartExchangeWalletKyc]: StartExchangeWalletKycOp; [WalletApiOperation.TestingWaitExchangeWalletKyc]: TestingWaitExchangeWalletKycOp; [WalletApiOperation.TestingPlanMigrateExchangeBaseUrl]: TestingPlanMigrateExchangeBaseUrlOp; diff --git a/packages/taler-wallet-core/src/wallet.ts b/packages/taler-wallet-core/src/wallet.ts @@ -258,6 +258,11 @@ import { validateIban, codecForString, codecForMailboxConfiguration, + codecForListSubscriptionsRequest, + codecForDeleteSubscriptionRequest, + ListSubscriptionsResponse, + ListSubscriptionsRequest, + DeleteSubscriptionRequest, } from "@gnu-taler/taler-util"; import { readSuccessResponseJsonOrThrow, @@ -405,7 +410,12 @@ import { waitUntilRefreshesDone, withdrawTestBalance, } from "./testing.js"; -import { deleteDiscount, listDiscounts } from "./tokenFamilies.js"; +import { + deleteDiscount, + deleteSubscription, + listDiscounts, + listSubscriptions, +} from "./tokenFamilies.js"; import { abortTransaction, deleteTransaction, @@ -1309,6 +1319,24 @@ async function handleDeleteDiscount( return await deleteDiscount(wex, req.tokenFamilyHash); } +async function handleListSubscriptions( + wex: WalletExecutionContext, + req: ListSubscriptionsRequest, +): Promise<ListSubscriptionsResponse> { + return await listSubscriptions(wex, + req.tokenIssuePubHash, + req.merchantBaseUrl); +} + +async function handleDeleteSubscription( + wex: WalletExecutionContext, + req: DeleteSubscriptionRequest, +): Promise<EmptyObject> { + return await deleteSubscription(wex, + req.tokenFamilyHash, + ); +} + async function handleAbortTransaction( wex: WalletExecutionContext, req: AbortTransactionRequest, @@ -2206,6 +2234,14 @@ const handlers: { [T in WalletApiOperation]: HandlerWithValidator<T> } = { codec: codecForDeleteDiscountRequest(), handler: handleDeleteDiscount, }, + [WalletApiOperation.ListSubscriptions]: { + codec: codecForListSubscriptionsRequest(), + handler: handleListSubscriptions, + }, + [WalletApiOperation.DeleteSubscription]: { + codec: codecForDeleteSubscriptionRequest(), + handler: handleDeleteSubscription, + }, [WalletApiOperation.SuspendTransaction]: { codec: codecForSuspendTransaction(), handler: handleSuspendTransaction,