commit 5f0d7e308a9ed0535ccebaea269d2a3615654acf
parent 8a9cbe2cfa1953f0e271b942a354eb3af29485c6
Author: Florian Dold <dold@taler.net>
Date: Sat, 8 Aug 2026 08:18:27 +0200
util: merchant client additions and fixes
Diffstat:
1 file changed, 217 insertions(+), 147 deletions(-)
diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts
@@ -293,14 +293,23 @@ export class TalerMerchantInstanceHttpClient {
body: LoginTokenRequest,
params: {
challengeIds?: string[];
+ token?: AccessToken;
} = {},
) {
const url = new URL(`private/token`, this.baseUrl);
- const headers = authHeaders({
- type: "basic",
- username: instance,
- password,
- });
+ const headers: Record<string, string> = {};
+ if (params.token) {
+ headers.Authorization = makeBearerTokenAuthHeader(params.token);
+ } else {
+ Object.assign(
+ headers,
+ authHeaders({
+ type: "basic",
+ username: instance,
+ password,
+ }),
+ );
+ }
if (params.challengeIds && params.challengeIds.length > 0) {
headers["Taler-Challenge-Ids"] = params.challengeIds.join(", ");
}
@@ -957,6 +966,43 @@ export class TalerMerchantInstanceHttpClient {
}
/**
+ * Record early acceptance of exchange Terms of Service.
+ * POST /private/accept-tos-early
+ * @since protocol **v31**
+ */
+ async acceptTosEarly(
+ token: AccessToken,
+ body: {
+ exchange_url: string;
+ tos_version: string;
+ },
+ ) {
+ const url = new URL("private/accept-tos-early", this.baseUrl);
+ const headers: Record<string, string> = {};
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+
+ const cancellationToken = this.cancellationToken;
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "POST",
+ headers,
+ cancellationToken,
+ body,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent:
+ case HttpStatusCode.Ok:
+ return opEmptySuccess(resp);
+ case HttpStatusCode.Unauthorized:
+ case HttpStatusCode.BadRequest:
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ /**
* https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-kyc
*/
async getCurrentInstanceKycStatus(
@@ -990,19 +1036,27 @@ export class TalerMerchantInstanceHttpClient {
if (params.longpoll) {
switch (params.longpoll.type) {
case "state-enter":
- url.searchParams.set("lp_status", params.longpoll.status);
+ if (params.longpoll.status) {
+ url.searchParams.set("lp_status", params.longpoll.status);
+ url.searchParams.set("timeout_ms", String(params.longpoll.timeout));
+ }
break;
case "state-exit":
- url.searchParams.set("lp_not_status", params.longpoll.status);
+ if (params.longpoll.status) {
+ url.searchParams.set("lp_not_status", params.longpoll.status);
+ url.searchParams.set("timeout_ms", String(params.longpoll.timeout));
+ }
break;
case "state-change":
- url.searchParams.set("lp_not_etag", params.longpoll.etag);
- headers["If-none-match"] = `"${params.longpoll.etag}"`;
+ if (params.longpoll.etag && params.longpoll.etag.trim()) {
+ url.searchParams.set("lp_not_etag", params.longpoll.etag);
+ headers["If-none-match"] = `"${params.longpoll.etag}"`;
+ url.searchParams.set("timeout_ms", String(params.longpoll.timeout));
+ }
break;
default:
assertUnreachable(params.longpoll);
}
- url.searchParams.set("timeout_ms", String(params.longpoll.timeout));
}
if (token) {
@@ -1485,6 +1539,10 @@ export class TalerMerchantInstanceHttpClient {
);
return opEmptySuccess(resp);
}
+ case HttpStatusCode.BadRequest: {
+ const details = await readTalerErrorResponse(resp);
+ return opKnownTalerFailure(resp, details.code, details);
+ }
case HttpStatusCode.Unauthorized: // FIXME: missing in docs
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
@@ -1710,6 +1768,10 @@ export class TalerMerchantInstanceHttpClient {
);
return opSuccessFromHttp(resp, codecForPostOrderResponse());
}
+ case HttpStatusCode.BadRequest: {
+ const details = await readTalerErrorResponse(resp);
+ return opKnownTalerFailure(resp, details.code, details);
+ }
case HttpStatusCode.NotFound: {
const details = await readTalerErrorResponse(resp);
switch (details.code) {
@@ -1727,8 +1789,10 @@ export class TalerMerchantInstanceHttpClient {
resp.status,
codecForPaymentDeniedLegallyResponse(),
);
- case HttpStatusCode.Conflict:
- return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict: {
+ const details = await readTalerErrorResponse(resp);
+ return opKnownTalerFailure(resp, details.code, details);
+ }
case HttpStatusCode.Gone:
return opKnownAlternativeHttpFailure(
resp,
@@ -1891,9 +1955,13 @@ export class TalerMerchantInstanceHttpClient {
const headers: Record<string, string> = {};
if (params.longpoll) {
- url.searchParams.set("lp_not_etag", params.longpoll.etag);
- headers["If-none-match"] = `"${params.longpoll.etag}"`;
- url.searchParams.set("timeout_ms", String(params.longpoll.timeout));
+ if (params.longpoll.etag && params.longpoll.etag.trim()) {
+ url.searchParams.set("lp_not_etag", params.longpoll.etag);
+ headers["If-none-match"] = `"${params.longpoll.etag}"`;
+ }
+ if (params.longpoll.timeout) {
+ url.searchParams.set("timeout_ms", String(params.longpoll.timeout));
+ }
}
if (token) {
@@ -2230,39 +2298,38 @@ export class TalerMerchantInstanceHttpClient {
}
}
- // /**
- // * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-transfers-$TID
- // * @deprecated
- // */
- // async deleteWireTransfer(token: AccessToken, transferId: string) {
- // const url = new URL(`private/transfers/${pathSegment(transferId)}`, this.baseUrl);
-
- // const headers: Record<string, string> = {};
- // if (token) {
- // headers.Authorization = makeBearerTokenAuthHeader(token);
- // }
- // const resp = await this.httpLib.fetch(url.href, {
- // method: "DELETE",
- // headers,
- // });
-
- // switch (resp.status) {
- // case HttpStatusCode.NoContent: {
- // this.cacheEvictor.notifySuccess(
- // TalerMerchantInstanceCacheEviction.DELETE_TRANSFER,
- // );
- // return opEmptySuccess();
- // }
- // case HttpStatusCode.Unauthorized: // FIXME: missing in docs
- // return opKnownHttpFailure(resp.status, resp);
- // case HttpStatusCode.NotFound:
- // return opKnownHttpFailure(resp.status, resp);
- // case HttpStatusCode.Conflict:
- // return opKnownHttpFailure(resp.status, resp);
- // default:
- // return opUnknownHttpFailure(resp);
- // }
- // }
+ /**
+ * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-transfers-$TID
+ */
+ async deleteWireTransfer(token: AccessToken, transferId: string | number) {
+ const url = new URL(`private/transfers/${pathSegment(String(transferId))}`, this.baseUrl);
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "DELETE",
+ headers,
+ });
+
+ switch (resp.status) {
+ case HttpStatusCode.NoContent: {
+ this.cacheEvictor.notifySuccess(
+ TalerMerchantInstanceCacheEviction.DELETE_TRANSFER,
+ );
+ return opEmptySuccess(resp);
+ }
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Conflict:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
//
// OTP Devices
@@ -2464,6 +2531,10 @@ export class TalerMerchantInstanceHttpClient {
);
return opEmptySuccess(resp);
}
+ case HttpStatusCode.BadRequest: {
+ const details = await readTalerErrorResponse(resp);
+ return opKnownTalerFailure(resp, details.code, details);
+ }
case HttpStatusCode.Unauthorized: // FIXME: missing in docs
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
@@ -2501,6 +2572,10 @@ export class TalerMerchantInstanceHttpClient {
);
return opEmptySuccess(resp);
}
+ case HttpStatusCode.BadRequest: {
+ const details = await readTalerErrorResponse(resp);
+ return opKnownTalerFailure(resp, details.code, details);
+ }
case HttpStatusCode.Unauthorized: // FIXME: missing in docs
return opKnownHttpFailure(resp.status, resp);
case HttpStatusCode.NotFound:
@@ -3612,6 +3687,100 @@ export class TalerMerchantInstanceHttpClient {
}
}
+ async getStatisticsReport(
+ token: AccessToken,
+ name: "transactions" | "money-pots" | "taxes" | "sales-funnel",
+ params: TalerMerchantApi.GetStatisticsReportParams = {},
+ ): Promise<
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.Gone>
+ | OperationFail<HttpStatusCode.Unauthorized>
+ | OperationOk<TalerMerchantApi.MerchantStatisticsReportResponse>
+ | OperationFail<HttpStatusCode.NotImplemented>
+ > {
+ const url = new URL(`private/statistics-report/${pathSegment(name)}`, this.baseUrl);
+
+ if (params.count !== undefined) {
+ url.searchParams.set("count", String(params.count));
+ }
+ if (params.granularity) {
+ url.searchParams.set("granularity", params.granularity);
+ }
+
+ const headers: Record<string, string> = {
+ Accept: "application/json",
+ };
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opSuccessFromHttp(
+ resp,
+ codecForMerchantStatisticsReportResponse(),
+ );
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Gone:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
+ async getStatisticsReportPdf(
+ token: AccessToken,
+ name: "transactions" | "money-pots" | "taxes" | "sales-funnel",
+ params: TalerMerchantApi.GetStatisticsReportParams = {},
+ ): Promise<
+ | OperationOk<Uint8Array>
+ | OperationFail<HttpStatusCode.NotFound>
+ | OperationFail<HttpStatusCode.Gone>
+ | OperationFail<HttpStatusCode.Unauthorized>
+ | OperationFail<HttpStatusCode.NotImplemented>
+ > {
+ const url = new URL(`private/statistics-report/${pathSegment(name)}`, this.baseUrl);
+
+ if (params.count !== undefined) {
+ url.searchParams.set("count", String(params.count));
+ }
+ if (params.granularity) {
+ url.searchParams.set("granularity", params.granularity);
+ }
+
+ const headers: Record<string, string> = {};
+ if (token) {
+ headers.Authorization = makeBearerTokenAuthHeader(token);
+ }
+ headers["Accept"] = "application/pdf";
+ const resp = await this.httpLib.fetch(url.href, {
+ method: "GET",
+ headers,
+ });
+ switch (resp.status) {
+ case HttpStatusCode.Ok:
+ return opFixedSuccess(resp, await resp.bytes());
+ case HttpStatusCode.NotFound:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Unauthorized:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.Gone:
+ return opKnownHttpFailure(resp.status, resp);
+ case HttpStatusCode.NotImplemented:
+ return opKnownHttpFailure(resp.status, resp);
+ default:
+ return opUnknownHttpFailure(resp);
+ }
+ }
+
/**
* Get the auth api against the current instance
*
@@ -4066,103 +4235,4 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp
return opUnknownHttpFailure(resp);
}
}
-
- // FIXME: This should not go into the management API, but
- // the instance API.
- /**
- * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE-statistics-report-$NAME
- */
- async getStatisticsReport(
- token: AccessToken,
- name: "transactions" | "money-pots" | "taxes" | "sales-funnel",
- params: TalerMerchantApi.GetStatisticsReportParams = {},
- ): Promise<
- | OperationFail<HttpStatusCode.NotFound>
- | OperationFail<HttpStatusCode.Gone>
- | OperationFail<HttpStatusCode.Unauthorized>
- | OperationOk<TalerMerchantApi.MerchantStatisticsReportResponse>
- | OperationFail<HttpStatusCode.NotImplemented>
- > {
- const url = new URL(`private/statistics-report/${pathSegment(name)}`, this.baseUrl);
-
- if (params.count !== undefined) {
- url.searchParams.set("count", String(params.count));
- }
- if (params.granularity) {
- url.searchParams.set("granularity", params.granularity);
- }
-
- const headers: Record<string, string> = {};
- if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token);
- }
- const resp = await this.httpLib.fetch(url.href, {
- method: "GET",
- headers,
- });
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opSuccessFromHttp(
- resp,
- codecForMerchantStatisticsReportResponse(),
- );
- case HttpStatusCode.NotFound:
- return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.Unauthorized:
- return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.Gone:
- return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.NotImplemented:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
-
- // FIXME: This should not go into the management API, but
- // the instance API.
- async getStatisticsReportPdf(
- token: AccessToken,
- name: "transactions" | "money-pots" | "taxes" | "sales-funnel",
- params: TalerMerchantApi.GetStatisticsReportParams = {},
- ): Promise<
- | OperationOk<Uint8Array>
- | OperationFail<HttpStatusCode.NotFound>
- | OperationFail<HttpStatusCode.Gone>
- | OperationFail<HttpStatusCode.Unauthorized>
- | OperationFail<HttpStatusCode.NotImplemented>
- > {
- const url = new URL(`private/statistics-report/${pathSegment(name)}`, this.baseUrl);
-
- if (params.count !== undefined) {
- url.searchParams.set("count", String(params.count));
- }
- if (params.granularity) {
- url.searchParams.set("granularity", params.granularity);
- }
-
- const headers: Record<string, string> = {};
- if (token) {
- headers.Authorization = makeBearerTokenAuthHeader(token);
- }
- headers["Accept"] = "application/pdf";
- const resp = await this.httpLib.fetch(url.href, {
- method: "GET",
- headers,
- });
- switch (resp.status) {
- case HttpStatusCode.Ok:
- return opFixedSuccess(resp, await resp.bytes());
- case HttpStatusCode.NotFound:
- return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.Unauthorized:
- return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.Gone:
- return opKnownHttpFailure(resp.status, resp);
- case HttpStatusCode.NotImplemented:
- return opKnownHttpFailure(resp.status, resp);
- default:
- return opUnknownHttpFailure(resp);
- }
- }
}