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:
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,