taler-typescript-core

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

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:
Mpackages/taler-util/src/http-client/merchant.ts | 364+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
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); - } - } }