From 7c7086e11f641100e0dd06364a97503df348e2b2 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Tue, 19 Mar 2024 15:39:06 -0300 Subject: wip --- packages/taler-util/src/http-client/merchant.ts | 499 ++++++++++++++++++------ packages/taler-util/src/http-client/types.ts | 281 +++++++++++-- packages/taler-util/src/http-client/utils.ts | 15 + 3 files changed, 646 insertions(+), 149 deletions(-) (limited to 'packages/taler-util/src') diff --git a/packages/taler-util/src/http-client/merchant.ts b/packages/taler-util/src/http-client/merchant.ts index 00efc5a71..b054a04eb 100644 --- a/packages/taler-util/src/http-client/merchant.ts +++ b/packages/taler-util/src/http-client/merchant.ts @@ -17,20 +17,33 @@ import { HttpStatusCode, LibtoolVersion, + PaginationParams, TalerMerchantApi, + codecForAbortResponse, + codecForAccountAddDetails, + codecForAccountAddResponse, + codecForAccountKycRedirects, + codecForAccountsSummaryResponse, + codecForBankAccountEntry, codecForClaimResponse, + codecForInventorySummaryResponse, codecForMerchantConfig, - opKnownHttpFailure + codecForPaidRefundStatusResponse, + codecForPaymentResponse, + codecForQueryInstancesResponse, + codecForStatusGoto, + codecForStatusPaid, + codecForStatusStatusUnpaid, + codecForWalletRefundResponse, + opEmptySuccess, + opKnownHttpFailure, } from "@gnu-taler/taler-util"; import { HttpRequestLibrary, createPlatformHttpLib, } from "@gnu-taler/taler-util/http"; -import { - opSuccessFromHttp, - opUnknownFailure -} from "../operation.js"; -import { CacheEvictor, nullEvictor } from "./utils.js"; +import { opSuccessFromHttp, opUnknownFailure } from "../operation.js"; +import { CacheEvictor, addMerchantPaginationParams, addPaginationParams, nullEvictor } from "./utils.js"; export enum TalerMerchantCacheEviction { CREATE_ORDER, @@ -44,7 +57,8 @@ export enum TalerMerchantCacheEviction { * * Uses libtool's current:revision:age versioning. */ -class TalerMerchantInstanceHttpClient { +export class TalerMerchantInstanceHttpClient { + public readonly PROTOCOL_VERSION = "10:0:6"; readonly httpLib: HttpRequestLibrary; readonly cacheEvictor: CacheEvictor; @@ -58,6 +72,28 @@ class TalerMerchantInstanceHttpClient { this.cacheEvictor = cacheEvictor ?? nullEvictor; } + isCompatible(version: string): boolean { + const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); + return compare?.compatible ?? false; + } + + /** + * https://docs.taler.net/core/api-merchant.html#get--config + * + */ + async getConfig() { + const url = new URL(`config`, this.baseUrl); + const resp = await this.httpLib.fetch(url.href, { + method: "GET", + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForMerchantConfig()); + default: + return opUnknownFailure(resp, await resp.text()); + } + } + // // Wallet API // @@ -77,9 +113,9 @@ class TalerMerchantInstanceHttpClient { case HttpStatusCode.Ok: return opSuccessFromHttp(resp, codecForClaimResponse()); case HttpStatusCode.Conflict: - return opKnownHttpFailure(resp.status, resp) + return opKnownHttpFailure(resp.status, resp); case HttpStatusCode.NotFound: - return opKnownHttpFailure(resp.status, resp) + return opKnownHttpFailure(resp.status, resp); default: return opUnknownFailure(resp, await resp.text()); } @@ -96,36 +132,70 @@ class TalerMerchantInstanceHttpClient { body, }); - /// + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForPaymentResponse()); + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.PaymentRequired: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.RequestTimeout: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Gone: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.PreconditionFailed: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.BadGateway: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.GatewayTimeout: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-orders-$ORDER_ID */ - async getPaymentStatus(orderId: string, params: TalerMerchantApi.PaymentStatusRequestParams = {}) { + async getPaymentStatus( + orderId: string, + params: TalerMerchantApi.PaymentStatusRequestParams = {}, + ) { const url = new URL(`orders/${orderId}`, this.baseUrl); if (params.allowRefundedForRepurchase !== undefined) { - url.searchParams.set("allow_refunded_for_repurchase", params.allowRefundedForRepurchase ? "YES" : "NO") + url.searchParams.set( + "allow_refunded_for_repurchase", + params.allowRefundedForRepurchase ? "YES" : "NO", + ); } if (params.awaitRefundObtained !== undefined) { - url.searchParams.set("await_refund_obtained", params.allowRefundedForRepurchase ? "YES" : "NO") + url.searchParams.set( + "await_refund_obtained", + params.allowRefundedForRepurchase ? "YES" : "NO", + ); } if (params.claimToken !== undefined) { - url.searchParams.set("token", params.claimToken) + url.searchParams.set("token", params.claimToken); } if (params.contractTermHash !== undefined) { - url.searchParams.set("h_contract", params.contractTermHash) + url.searchParams.set("h_contract", params.contractTermHash); } if (params.refund !== undefined) { - url.searchParams.set("refund", params.refund) + url.searchParams.set("refund", params.refund); } if (params.sessionId !== undefined) { - url.searchParams.set("session_id", params.sessionId) + url.searchParams.set("session_id", params.sessionId); } if (params.timeout !== undefined) { - url.searchParams.set("timeout_ms", String(params.timeout)) + url.searchParams.set("timeout_ms", String(params.timeout)); } const resp = await this.httpLib.fetch(url.href, { @@ -133,7 +203,23 @@ class TalerMerchantInstanceHttpClient { // body, }); - /// + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForStatusPaid()); + case HttpStatusCode.Accepted: + return opSuccessFromHttp(resp, codecForStatusGoto()); + // case HttpStatusCode.Found: not possible since content is not HTML + case HttpStatusCode.PaymentRequired: + return opSuccessFromHttp(resp, codecForStatusStatusUnpaid()); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotAcceptable: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } /** @@ -146,40 +232,87 @@ class TalerMerchantInstanceHttpClient { method: "POST", body, }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForPaidRefundStatusResponse()); + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } /** * https://docs.taler.net/core/api-merchant.html#aborting-incomplete-payments */ - async abortIncompletePayment(orderId: string, body: TalerMerchantApi.AbortRequest) { + async abortIncompletePayment( + orderId: string, + body: TalerMerchantApi.AbortRequest, + ) { const url = new URL(`orders/${orderId}/abort`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", body, }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAbortResponse()); + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } /** * https://docs.taler.net/core/api-merchant.html#obtaining-refunds */ - async obtainRefund(orderId: string, body: TalerMerchantApi.WalletRefundRequest) { + async obtainRefund( + orderId: string, + body: TalerMerchantApi.WalletRefundRequest, + ) { const url = new URL(`orders/${orderId}/refund`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "POST", body, }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForWalletRefundResponse()); + case HttpStatusCode.BadRequest: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Forbidden: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } - // + // // Management // /** * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-auth */ - async updateCurrentInstanceAuthentication(body: TalerMerchantApi.InstanceAuthConfigurationMessage) { + async updateCurrentInstanceAuthentication( + body: TalerMerchantApi.InstanceAuthConfigurationMessage, + ) { const url = new URL(`private/auth`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -188,11 +321,19 @@ class TalerMerchantInstanceHttpClient { }); // + switch (resp.status) { + case HttpStatusCode.Ok: + return opEmptySuccess(resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } /** * Get the auth api agaisnt the current instance - * + * * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-token * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-token */ @@ -201,20 +342,30 @@ class TalerMerchantInstanceHttpClient { } /** - * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private - */ - async updateCurrentInstance(body: TalerMerchantApi.InstanceReconfigurationMessage) { + * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private + */ + async updateCurrentInstance( + body: TalerMerchantApi.InstanceReconfigurationMessage, + ) { const url = new URL(`private`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "PATCH", body, }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opEmptySuccess(resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private - * + * */ async getCurrentInstance() { const url = new URL(`private`, this.baseUrl); @@ -222,43 +373,79 @@ class TalerMerchantInstanceHttpClient { const resp = await this.httpLib.fetch(url.href, { method: "GET", }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForQueryInstancesResponse()); + default: + return opUnknownFailure(resp, await resp.text()); + } } /** - * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private - */ + * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private + */ async deleteCurrentInstance(params: { purge?: boolean }) { const url = new URL(`private`, this.baseUrl); if (params.purge) { - url.searchParams.set("purge", "YES") + url.searchParams.set("purge", "YES"); } const resp = await this.httpLib.fetch(url.href, { method: "DELETE", }); + + switch (resp.status) { + case HttpStatusCode.NoContent: + 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 opUnknownFailure(resp, await resp.text()); + } } /** * https://docs.taler.net/core/api-merchant.html#get--instances-$INSTANCE-private-kyc */ - async getCurrentIntanceKycStatus(params: TalerMerchantApi.GetKycStatusRequestParams) { + async getCurrentIntanceKycStatus( + params: TalerMerchantApi.GetKycStatusRequestParams, + ) { const url = new URL(`private/kyc`, this.baseUrl); if (params.wireHash) { - url.searchParams.set("h_wire", params.wireHash) + url.searchParams.set("h_wire", params.wireHash); } if (params.exchangeURL) { - url.searchParams.set("exchange_url", params.exchangeURL) + url.searchParams.set("exchange_url", params.exchangeURL); } if (params.timeout) { - url.searchParams.set("timeout_ms", String(params.timeout)) + url.searchParams.set("timeout_ms", String(params.timeout)); } const resp = await this.httpLib.fetch(url.href, { method: "GET", }); + switch (resp.status) { + case HttpStatusCode.Accepted: + return opSuccessFromHttp(resp, codecForAccountKycRedirects()); + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.BadGateway: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.ServiceUnavailable: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } // @@ -275,18 +462,40 @@ class TalerMerchantInstanceHttpClient { method: "POST", body, }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAccountAddResponse()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } /** * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-accounts-$H_WIRE */ - async updateAccount(wireAccount: string, body: TalerMerchantApi.AccountPatchDetails) { + async updateAccount( + wireAccount: string, + body: TalerMerchantApi.AccountPatchDetails, + ) { const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "PATCH", body, }); + switch (resp.status) { + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } /** @@ -298,6 +507,15 @@ class TalerMerchantInstanceHttpClient { const resp = await this.httpLib.fetch(url.href, { method: "GET", }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAccountsSummaryResponse()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } /** @@ -309,6 +527,15 @@ class TalerMerchantInstanceHttpClient { const resp = await this.httpLib.fetch(url.href, { method: "GET", }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForBankAccountEntry()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } /** @@ -320,6 +547,15 @@ class TalerMerchantInstanceHttpClient { const resp = await this.httpLib.fetch(url.href, { method: "DELETE", }); + + switch (resp.status) { + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } // @@ -336,29 +572,63 @@ class TalerMerchantInstanceHttpClient { method: "POST", body, }); + + switch (resp.status) { + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } /** * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-products-$PRODUCT_ID */ - async updateProduct(productId: string, body: TalerMerchantApi.ProductAddDetail) { + async updateProduct( + productId: string, + body: TalerMerchantApi.ProductAddDetail, + ) { const url = new URL(`private/products/${productId}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "PATCH", body, }); + + switch (resp.status) { + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + case HttpStatusCode.Conflict: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products */ - async listProducts() { + async listProducts(params?: PaginationParams) { const url = new URL(`private/products`, this.baseUrl); + addMerchantPaginationParams(url, params); + const resp = await this.httpLib.fetch(url.href, { method: "GET", }); + + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForInventorySummaryResponse()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await resp.text()); + } } /** @@ -417,31 +687,31 @@ class TalerMerchantInstanceHttpClient { const url = new URL(`private/orders`, this.baseUrl); if (params.date) { - url.searchParams.set("date_s", String(params.date)) + url.searchParams.set("date_s", String(params.date)); } if (params.delta) { - url.searchParams.set("delta", String(params.delta)) + url.searchParams.set("delta", String(params.delta)); } if (params.fulfillmentUrl) { - url.searchParams.set("fulfillment_url", params.fulfillmentUrl) + url.searchParams.set("fulfillment_url", params.fulfillmentUrl); } if (params.paid) { - url.searchParams.set("paid", "YES") + url.searchParams.set("paid", "YES"); } if (params.refunded) { - url.searchParams.set("refunded", "YES") + url.searchParams.set("refunded", "YES"); } if (params.sessionId) { - url.searchParams.set("session_id", params.sessionId) + url.searchParams.set("session_id", params.sessionId); } if (params.start) { - url.searchParams.set("start", String(params.start)) + url.searchParams.set("start", String(params.start)); } if (params.timeout) { - url.searchParams.set("timeout", String(params.timeout)) + url.searchParams.set("timeout", String(params.timeout)); } if (params.wired) { - url.searchParams.set("wired", "YES") + url.searchParams.set("wired", "YES"); } const resp = await this.httpLib.fetch(url.href, { @@ -452,17 +722,20 @@ class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-orders-$ORDER_ID */ - async getOrder(orderId: string, params: TalerMerchantApi.GetOrderRequestParams = {}) { + async getOrder( + orderId: string, + params: TalerMerchantApi.GetOrderRequestParams = {}, + ) { const url = new URL(`private/orders/${orderId}`, this.baseUrl); if (params.allowRefundedForRepurchase) { - url.searchParams.set("allow_refunded_for_repurchase", "YES") + url.searchParams.set("allow_refunded_for_repurchase", "YES"); } if (params.sessionId) { - url.searchParams.set("session_id", params.sessionId) + url.searchParams.set("session_id", params.sessionId); } if (params.timeout) { - url.searchParams.set("timeout_ms", String(params.timeout)) + url.searchParams.set("timeout_ms", String(params.timeout)); } const resp = await this.httpLib.fetch(url.href, { @@ -493,7 +766,7 @@ class TalerMerchantInstanceHttpClient { }); } - // + // // Refunds // @@ -528,26 +801,28 @@ class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-transfers */ - async listWireTransfers(params: TalerMerchantApi.ListWireTransferRequestParams = {}) { + async listWireTransfers( + params: TalerMerchantApi.ListWireTransferRequestParams = {}, + ) { const url = new URL(`private/transfers`, this.baseUrl); if (params.after) { - url.searchParams.set("after", String(params.after)) + url.searchParams.set("after", String(params.after)); } if (params.before) { - url.searchParams.set("before", String(params.before)) + url.searchParams.set("before", String(params.before)); } if (params.limit) { - url.searchParams.set("limit", String(params.limit)) + url.searchParams.set("limit", String(params.limit)); } if (params.offset) { - url.searchParams.set("offset", String(params.offset)) + url.searchParams.set("offset", String(params.offset)); } if (params.paytoURI) { - url.searchParams.set("payto_uri", params.paytoURI) + url.searchParams.set("payto_uri", params.paytoURI); } if (params.verified) { - url.searchParams.set("verified", "YES") + url.searchParams.set("verified", "YES"); } const resp = await this.httpLib.fetch(url.href, { @@ -585,7 +860,10 @@ class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID */ - async updateOtpDevice(deviceId: string, body: TalerMerchantApi.OtpDevicePatchDetails) { + async updateOtpDevice( + deviceId: string, + body: TalerMerchantApi.OtpDevicePatchDetails, + ) { const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -608,19 +886,21 @@ class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID */ - async getOtpDevice(deviceId: string, params: TalerMerchantApi.GetOtpDeviceRequestParams = {}) { + async getOtpDevice( + deviceId: string, + params: TalerMerchantApi.GetOtpDeviceRequestParams = {}, + ) { const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl); if (params.faketime) { - url.searchParams.set("faketime", String(params.faketime)) + url.searchParams.set("faketime", String(params.faketime)); } if (params.price) { - url.searchParams.set("price", params.price) + url.searchParams.set("price", params.price); } const resp = await this.httpLib.fetch(url.href, { method: "GET", }); - } /** @@ -653,7 +933,10 @@ class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID */ - async updateTemplate(templateId: string, body: TalerMerchantApi.TemplatePatchDetails) { + async updateTemplate( + templateId: string, + body: TalerMerchantApi.TemplatePatchDetails, + ) { const url = new URL(`private/templates/${templateId}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -704,13 +987,15 @@ class TalerMerchantInstanceHttpClient { const resp = await this.httpLib.fetch(url.href, { method: "GET", }); - } /** * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-templates-$TEMPLATE_ID */ - async useTemplateCreateOrder(templateId: string, body: TalerMerchantApi.UsingTemplateDetails) { + async useTemplateCreateOrder( + templateId: string, + body: TalerMerchantApi.UsingTemplateDetails, + ) { const url = new URL(`templates/${templateId}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -723,7 +1008,6 @@ class TalerMerchantInstanceHttpClient { // Webhooks // - /** * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-webhooks */ @@ -739,7 +1023,10 @@ class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID */ - async updateWebhook(webhookId: string, body: TalerMerchantApi.WebhookPatchDetails) { + async updateWebhook( + webhookId: string, + body: TalerMerchantApi.WebhookPatchDetails, + ) { const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -800,7 +1087,10 @@ class TalerMerchantInstanceHttpClient { /** * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG */ - async updateTokenFamily(tokenSlug: string,body: TalerMerchantApi.TokenFamilyCreateRequest) { + async updateTokenFamily( + tokenSlug: string, + body: TalerMerchantApi.TokenFamilyCreateRequest, + ) { const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -829,7 +1119,6 @@ class TalerMerchantInstanceHttpClient { const resp = await this.httpLib.fetch(url.href, { method: "GET", }); - } /** @@ -842,49 +1131,21 @@ class TalerMerchantInstanceHttpClient { method: "DELETE", }); } - } export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttpClient { - public readonly PROTOCOL_VERSION = "10:0:6"; - - httpLib: HttpRequestLibrary; - cacheEvictor: CacheEvictor; constructor( readonly baseUrl: string, httpClient?: HttpRequestLibrary, cacheEvictor?: CacheEvictor, ) { - super(baseUrl, httpClient, cacheEvictor) - } - - isCompatible(version: string): boolean { - const compare = LibtoolVersion.compare(this.PROTOCOL_VERSION, version); - return compare?.compatible ?? false; + super(baseUrl, httpClient, cacheEvictor); } getSubInstanceApi(instanceId: string) { - return new URL(`instances/${instanceId}`, this.baseUrl) - } - - /** - * https://docs.taler.net/core/api-merchant.html#get--config - * - */ - async getConfig() { - const url = new URL(`config`, this.baseUrl); - const resp = await this.httpLib.fetch(url.href, { - method: "GET", - }); - switch (resp.status) { - case HttpStatusCode.Ok: - return opSuccessFromHttp(resp, codecForMerchantConfig()); - default: - return opUnknownFailure(resp, await resp.text()); - } + return new URL(`instances/${instanceId}`, this.baseUrl); } - // // Instance Management // @@ -906,7 +1167,9 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp /** * https://docs.taler.net/core/api-merchant.html#post--management-instances-$INSTANCE-auth */ - async updateInstanceAuthentication(body: TalerMerchantApi.InstanceAuthConfigurationMessage) { + async updateInstanceAuthentication( + body: TalerMerchantApi.InstanceAuthConfigurationMessage, + ) { const url = new URL(`management/instances`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { @@ -917,19 +1180,19 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp // } - /** * https://docs.taler.net/core/api-merchant.html#patch--management-instances-$INSTANCE */ - async updateInstance(isntanceId: string, body: TalerMerchantApi.InstanceReconfigurationMessage) { + async updateInstance( + isntanceId: string, + body: TalerMerchantApi.InstanceReconfigurationMessage, + ) { const url = new URL(`management/instances/${isntanceId}`, this.baseUrl); const resp = await this.httpLib.fetch(url.href, { method: "PATCH", body, }); - - } /** @@ -941,12 +1204,11 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp const resp = await this.httpLib.fetch(url.href, { method: "GET", }); - } /** * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE - * + * */ async getInstance(instanceId: string) { const url = new URL(`management/instances/${instanceId}`, this.baseUrl); @@ -959,11 +1221,11 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp /** * https://docs.taler.net/core/api-merchant.html#delete--management-instances-$INSTANCE */ - async deleteInstance(instanceId: string, params: {purge?: boolean} = {}) { + async deleteInstance(instanceId: string, params: { purge?: boolean } = {}) { const url = new URL(`management/instances/${instanceId}`, this.baseUrl); if (params.purge) { - url.searchParams.set("purge", "YES") + url.searchParams.set("purge", "YES"); } const resp = await this.httpLib.fetch(url.href, { @@ -974,25 +1236,24 @@ export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttp /** * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE-kyc */ - async getIntanceKycStatus(instanceId: string, params: TalerMerchantApi.GetKycStatusRequestParams) { + async getIntanceKycStatus( + instanceId: string, + params: TalerMerchantApi.GetKycStatusRequestParams, + ) { const url = new URL(`management/instances/${instanceId}/kyc`, this.baseUrl); if (params.wireHash) { - url.searchParams.set("h_wire", params.wireHash) + url.searchParams.set("h_wire", params.wireHash); } if (params.exchangeURL) { - url.searchParams.set("exchange_url", params.exchangeURL) + url.searchParams.set("exchange_url", params.exchangeURL); } if (params.timeout) { - url.searchParams.set("timeout_ms", String(params.timeout)) + url.searchParams.set("timeout_ms", String(params.timeout)); } - + const resp = await this.httpLib.fetch(url.href, { method: "GET", }); } - - - - } diff --git a/packages/taler-util/src/http-client/types.ts b/packages/taler-util/src/http-client/types.ts index 67ac289d6..7e4e2f84c 100644 --- a/packages/taler-util/src/http-client/types.ts +++ b/packages/taler-util/src/http-client/types.ts @@ -6,6 +6,7 @@ import { buildCodecForUnion, codecForAny, codecForBoolean, + codecForConstNumber, codecForConstString, codecForEither, codecForList, @@ -15,12 +16,18 @@ import { codecOptional, } from "../codec.js"; import { PaytoString, codecForPaytoString } from "../payto.js"; -import { AmountString, MerchantContractTerms, codecForMerchantContractTerms } from "../taler-types.js"; +import { + AmountString, + MerchantContractTerms, + codecForLocation, + codecForMerchantContractTerms, +} from "../taler-types.js"; import { TalerActionString, codecForTalerActionString } from "../taleruri.js"; import { AbsoluteTime, TalerProtocolDuration, TalerProtocolTimestamp, + codecForDuration, codecForTimestamp, } from "../time.js"; @@ -315,9 +322,216 @@ export const codecForMerchantConfig = export const codecForClaimResponse = (): Codec => buildCodecForObject() - .property("contract_terms", codecForMerchantContractTerms()) - .property("sig", codecForString()) - .build("TalerMerchantApi.ClaimResponse"); + .property("contract_terms", codecForMerchantContractTerms()) + .property("sig", codecForString()) + .build("TalerMerchantApi.ClaimResponse"); + +export const codecForPaymentResponse = + (): Codec => + buildCodecForObject() + .property("pos_confirmation", codecOptional(codecForString())) + .property("sig", codecForString()) + .build("TalerMerchantApi.PaymentResponse"); + +export const codecForStatusPaid = (): Codec => + buildCodecForObject() + .property("refund_amount", codecForAmountString()) + .property("refund_pending", codecForBoolean()) + .property("refund_taken", codecForAmountString()) + .property("refunded", codecForBoolean()) + .property("type", codecForConstString("paid")) + .build("TalerMerchantApi.StatusPaid"); + +export const codecForStatusGoto = + (): Codec => + buildCodecForObject() + .property("public_reorder_url", codecForURL()) + .property("type", codecForConstString("goto")) + .build("TalerMerchantApi.StatusGotoResponse"); + +export const codecForStatusStatusUnpaid = + (): Codec => + buildCodecForObject() + .property("type", codecForConstString("unpaid")) + .property("already_paid_order_id", codecOptional(codecForString())) + .property("fulfillment_url", codecOptional(codecForString())) + .property("taler_pay_uri", codecForString()) + .build("TalerMerchantApi.PaymentResponse"); + +export const codecForPaidRefundStatusResponse = + (): Codec => + buildCodecForObject() + .property("pos_confirmation", codecOptional(codecForString())) + .property("refunded", codecForBoolean()) + .build("TalerMerchantApi.PaidRefundStatusResponse"); + +export const codecForMerchantAbortPayRefundSuccessStatus = + (): Codec => + buildCodecForObject() + .property("exchange_pub", codecForString()) + .property("exchange_sig", codecForString()) + .property("exchange_status", codecForConstNumber(200)) + .property("type", codecForConstString("success")) + .build("TalerMerchantApi.MerchantAbortPayRefundSuccessStatus"); + +export const codecForMerchantAbortPayRefundFailureStatus = + (): Codec => + buildCodecForObject() + .property("exchange_code", codecForNumber()) + .property("exchange_reply", codecForAny()) + .property("exchange_status", codecForNumber()) + .property("type", codecForConstString("failure")) + .build("TalerMerchantApi.MerchantAbortPayRefundFailureStatus"); + +export const codecForMerchantAbortPayRefundStatus = + (): Codec => + buildCodecForUnion() + .discriminateOn("type") + .alternative("success", codecForMerchantAbortPayRefundSuccessStatus()) + .alternative("failure", codecForMerchantAbortPayRefundFailureStatus()) + .build("TalerMerchantApi.MerchantAbortPayRefundStatus"); + +export const codecForAbortResponse = + (): Codec => + buildCodecForObject() + .property("refunds", codecForList(codecForMerchantAbortPayRefundStatus())) + .build("TalerMerchantApi.AbortResponse"); + +export const codecForWalletRefundResponse = + (): Codec => + buildCodecForObject() + .property("merchant_pub", codecForString()) + .property("refund_amount", codecForAmountString()) + .property("refunds", codecForList(codecForMerchantCoinRefundStatus())) + .build("TalerMerchantApi.AbortResponse"); + +export const codecForMerchantCoinRefundSuccessStatus = + (): Codec => + buildCodecForObject() + .property("type", codecForConstString("success")) + .property("coin_pub", codecForString()) + .property("exchange_status", codecForConstNumber(200)) + .property("exchange_sig", codecForString()) + .property("rtransaction_id", codecForNumber()) + .property("refund_amount", codecForAmountString()) + .property("exchange_pub", codecForString()) + .property("execution_time", codecForTimestamp) + .build("TalerMerchantApi.MerchantCoinRefundSuccessStatus"); + +export const codecForMerchantCoinRefundFailureStatus = + (): Codec => + buildCodecForObject() + .property("type", codecForConstString("failure")) + .property("coin_pub", codecForString()) + .property("exchange_status", codecForNumber()) + .property("rtransaction_id", codecForNumber()) + .property("refund_amount", codecForAmountString()) + .property("exchange_code", codecOptional(codecForNumber())) + .property("exchange_reply", codecOptional(codecForAny())) + .property("execution_time", codecForTimestamp) + .build("TalerMerchantApi.MerchantCoinRefundFailureStatus"); + +export const codecForMerchantCoinRefundStatus = + (): Codec => + buildCodecForUnion() + .discriminateOn("type") + .alternative("success", codecForMerchantCoinRefundSuccessStatus()) + .alternative("failure", codecForMerchantCoinRefundFailureStatus()) + .build("TalerMerchantApi.MerchantCoinRefundStatus"); + +export const codecForQueryInstancesResponse = + (): Codec => + buildCodecForObject() + .property("name", codecForString()) + .property("user_type", codecForString()) + .property("email", codecOptional(codecForString())) + .property("website", codecOptional(codecForString())) + .property("logo", codecOptional(codecForString())) + .property("merchant_pub", codecForString()) + .property("address", codecForLocation()) + .property("jurisdiction", codecForLocation()) + .property("use_stefan", codecForBoolean()) + .property("default_wire_transfer_delay", codecForDuration) + .property("default_pay_delay", codecForDuration) + .property( + "auth", + buildCodecForObject<{ + type: "external" | "token"; + }>() + .property( + "type", + codecForEither( + codecForConstString("token"), + codecForConstString("external"), + ), + ) + .build("TalerMerchantApi.QueryInstancesResponse.auth"), + ) + .build("TalerMerchantApi.QueryInstancesResponse"); + +export const codecForAccountKycRedirects = + (): Codec => + buildCodecForObject() + .property( + "pending_kycs", + codecForList(codecForMerchantAccountKycRedirect()), + ) + .property("timeout_kycs", codecForList(codecForExchangeKycTimeout())) + + .build("TalerMerchantApi.AccountKycRedirects"); + +export const codecForMerchantAccountKycRedirect = + (): Codec => + buildCodecForObject() + .property("kyc_url", codecForURL()) + .property("aml_status", codecForNumber()) + .property("exchange_url", codecForURL()) + .property("payto_uri", codecForPaytoString()) + .build("TalerMerchantApi.MerchantAccountKycRedirect"); + +export const codecForExchangeKycTimeout = + (): Codec => + buildCodecForObject() + .property("exchange_url", codecForURL()) + .property("exchange_code", codecForNumber()) + .property("exchange_http_status", codecForNumber()) + .build("TalerMerchantApi.ExchangeKycTimeout"); + +export const codecForAccountAddResponse = + (): Codec => + buildCodecForObject() + .property("h_wire", codecForString()) + .property("salt", codecForString()) + .build("TalerMerchantApi.AccountAddResponse"); + +export const codecForAccountsSummaryResponse = + (): Codec => + buildCodecForObject() + .property("accounts", codecForList(codecForBankAccountEntry())) + .build("TalerMerchantApi.AccountsSummaryResponse"); + +export const codecForBankAccountEntry = + (): Codec => + buildCodecForObject() + .property("payto_uri", codecForPaytoString()) + .property("h_wire", codecForString()) + .property("salt", codecForString()) + .property("credit_facade_url", codecForURL()) + .property("active", codecForBoolean()) + .build("TalerMerchantApi.BankAccountEntry"); + +export const codecForInventorySummaryResponse = + (): Codec => + buildCodecForObject() + .property("products", codecForList(codecForInventoryEntry())) + .build("TalerMerchantApi.InventorySummaryResponse"); + +export const codecForInventoryEntry = + (): Codec => + buildCodecForObject() + .property("product_id", codecForString()) + .property("product_serial", codecForNumber()) + .build("TalerMerchantApi.InventoryEntry"); export const codecForExchangeConfig = (): Codec => @@ -2477,7 +2691,6 @@ export namespace TalerMerchantApi { export interface ClaimResponse { // Contract terms of the claimed order contract_terms: MerchantContractTerms; - // Signature by the merchant over the contract terms. sig: EddsaSignature; @@ -2647,7 +2860,9 @@ export namespace TalerMerchantApi { exchange_url: string; } - interface StatusPaid { + export interface StatusPaid { + type: "paid"; + // Was the payment refunded (even partially, via refund or abort)? refunded: boolean; @@ -2660,14 +2875,16 @@ export namespace TalerMerchantApi { // Amount that already taken by the wallet. refund_taken: AmountString; } - interface StatusGotoResponse { + export interface StatusGotoResponse { + type: "goto"; // The client should go to the reorder URL, there a fresh // order might be created as this one is taken by another // customer or wallet (or repurchase detection logic may // apply). public_reorder_url: string; } - interface StatusUnpaidResponse { + export interface StatusUnpaidResponse { + type: "unpaid"; // URI that the wallet must process to complete the payment. taler_pay_uri: string; @@ -2680,7 +2897,7 @@ export namespace TalerMerchantApi { already_paid_order_id?: string; } - interface PaidRefundStatusResponse { + export interface PaidRefundStatusResponse { // Text to be shown to the point-of-sale staff as a proof of // payment (present only if reusable OTP algorithm is used). pos_confirmation?: string; @@ -2726,18 +2943,18 @@ export namespace TalerMerchantApi { // URL of the exchange this coin was withdrawn from. exchange_url: string; } - interface AbortResponse { + export interface AbortResponse { // List of refund responses about the coins that the wallet // requested an abort for. In the same order as the coins // from the original request. // The rtransaction_id is implied to be 0. refunds: MerchantAbortPayRefundStatus[]; } - type MerchantAbortPayRefundStatus = + export type MerchantAbortPayRefundStatus = | MerchantAbortPayRefundSuccessStatus | MerchantAbortPayRefundFailureStatus; // Details about why a refund failed. - interface MerchantAbortPayRefundFailureStatus { + export interface MerchantAbortPayRefundFailureStatus { // Used as tag for the sum type RefundStatus sum type. type: "failure"; @@ -2753,7 +2970,7 @@ export namespace TalerMerchantApi { // Additional details needed to verify the refund confirmation signature // (h_contract_terms and merchant_pub) are already known // to the wallet and thus not included. - interface MerchantAbortPayRefundSuccessStatus { + export interface MerchantAbortPayRefundSuccessStatus { // Used as tag for the sum type MerchantCoinRefundStatus sum type. type: "success"; @@ -2777,7 +2994,7 @@ export namespace TalerMerchantApi { // wallet/customer). h_contract: HashCode; } - interface WalletRefundResponse { + export interface WalletRefundResponse { // Amount that was refunded in total. refund_amount: AmountString; @@ -2787,11 +3004,11 @@ export namespace TalerMerchantApi { // Public key of the merchant. merchant_pub: EddsaPublicKey; } - type MerchantCoinRefundStatus = + export type MerchantCoinRefundStatus = | MerchantCoinRefundSuccessStatus | MerchantCoinRefundFailureStatus; // Details about why a refund failed. - interface MerchantCoinRefundFailureStatus { + export interface MerchantCoinRefundFailureStatus { // Used as tag for the sum type RefundStatus sum type. type: "failure"; @@ -2821,7 +3038,7 @@ export namespace TalerMerchantApi { // Additional details needed to verify the refund confirmation signature // (h_contract_terms and merchant_pub) are already known // to the wallet and thus not included. - interface MerchantCoinRefundSuccessStatus { + export interface MerchantCoinRefundSuccessStatus { // Used as tag for the sum type MerchantCoinRefundStatus sum type. type: "success"; @@ -3031,7 +3248,7 @@ export namespace TalerMerchantApi { deleted: boolean; } - interface QueryInstancesResponse { + export interface QueryInstancesResponse { // Merchant name corresponding to this instance. name: string; @@ -3079,7 +3296,7 @@ export namespace TalerMerchantApi { }; } - interface AccountKycRedirects { + export interface AccountKycRedirects { // Array of pending KYCs. pending_kycs: MerchantAccountKycRedirect[]; @@ -3087,7 +3304,7 @@ export namespace TalerMerchantApi { timeout_kycs: ExchangeKycTimeout[]; } - interface MerchantAccountKycRedirect { + export interface MerchantAccountKycRedirect { // URL that the user should open in a browser to // proceed with the KYC process (as returned // by the exchange's /kyc-check/ endpoint). @@ -3105,7 +3322,7 @@ export namespace TalerMerchantApi { payto_uri: PaytoString; } - interface ExchangeKycTimeout { + export interface ExchangeKycTimeout { // Base URL of the exchange this is about. exchange_url: string; @@ -3135,11 +3352,13 @@ export namespace TalerMerchantApi { credit_facade_credentials?: FacadeCredentials; } - type FacadeCredentials = NoFacadeCredentials | BasicAuthFacadeCredentials; - interface NoFacadeCredentials { + export type FacadeCredentials = + | NoFacadeCredentials + | BasicAuthFacadeCredentials; + export interface NoFacadeCredentials { type: "none"; } - interface BasicAuthFacadeCredentials { + export interface BasicAuthFacadeCredentials { type: "basic"; // Username to use to authenticate @@ -3148,7 +3367,7 @@ export namespace TalerMerchantApi { // Password to use to authenticate password: string; } - interface AccountAddResponse { + export interface AccountAddResponse { // Hash over the wire details (including over the salt). h_wire: HashCode; @@ -3171,11 +3390,11 @@ export namespace TalerMerchantApi { credit_facade_credentials?: FacadeCredentials; } - interface AccountsSummaryResponse { + export interface AccountsSummaryResponse { // List of accounts that are known for the instance. accounts: BankAccountEntry[]; } - interface BankAccountEntry { + export interface BankAccountEntry { // payto:// URI of the account. payto_uri: PaytoString; @@ -3278,17 +3497,19 @@ export namespace TalerMerchantApi { minimum_age?: Integer; } - interface InventorySummaryResponse { + export interface InventorySummaryResponse { // List of products that are present in the inventory. products: InventoryEntry[]; } - interface InventoryEntry { + export interface InventoryEntry { // Product identifier, as found in the product. product_id: string; + // product_serial_id of the product in the database. + product_serial: Integer; } - interface ProductDetail { + export interface ProductDetail { // Human-readable product description. description: string; diff --git a/packages/taler-util/src/http-client/utils.ts b/packages/taler-util/src/http-client/utils.ts index f925a5610..dbfe64796 100644 --- a/packages/taler-util/src/http-client/utils.ts +++ b/packages/taler-util/src/http-client/utils.ts @@ -59,6 +59,21 @@ export function addPaginationParams(url: URL, pagination?: PaginationParams) { url.searchParams.set("delta", String(order * limit)); } +export function addMerchantPaginationParams(url: URL, pagination?: PaginationParams) { + if (!pagination) return; + if (pagination.offset) { + url.searchParams.set("offset", pagination.offset); + } + const order = !pagination || pagination.order === "asc" ? 1 : -1; + const limit = + !pagination || !pagination.limit || pagination.limit === 0 + ? 5 + : Math.abs(pagination.limit); + //always send delta + url.searchParams.set("limit", String(order * limit)); +} + + export function addLongPollingParam(url: URL, param?: LongPollParams) { if (!param) return; if (param.timeoutMs) { -- cgit v1.2.3