diff options
Diffstat (limited to 'packages/taler-util/src/MerchantApiClient.ts')
-rw-r--r-- | packages/taler-util/src/MerchantApiClient.ts | 380 |
1 files changed, 380 insertions, 0 deletions
diff --git a/packages/taler-util/src/MerchantApiClient.ts b/packages/taler-util/src/MerchantApiClient.ts new file mode 100644 index 000000000..c27f1d582 --- /dev/null +++ b/packages/taler-util/src/MerchantApiClient.ts @@ -0,0 +1,380 @@ +/* + This file is part of GNU Taler + (C) 2023 Taler Systems S.A. + + GNU Taler is free software; you can redistribute it and/or modify it under the + terms of the GNU General Public License as published by the Free Software + Foundation; either version 3, or (at your option) any later version. + + GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY + WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR + A PARTICULAR PURPOSE. See the GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along with + GNU Taler; see the file COPYING. If not, see <http://www.gnu.org/licenses/> + */ + +import { codecForAny } from "./codec.js"; +import { + TalerMerchantApi, + codecForMerchantConfig, + codecForMerchantOrderPrivateStatusResponse, +} from "./http-client/types.js"; +import { HttpStatusCode } from "./http-status-codes.js"; +import { + createPlatformHttpLib, + expectSuccessResponseOrThrow, + readSuccessResponseJsonOrThrow, + readTalerErrorResponse, +} from "./http.js"; +import { FacadeCredentials } from "./libeufin-api-types.js"; +import { LibtoolVersion } from "./libtool-version.js"; +import { Logger } from "./logging.js"; +import { + MerchantInstancesResponse, + MerchantPostOrderRequest, + MerchantPostOrderResponse, + MerchantTemplateAddDetails, + codecForMerchantPostOrderResponse, +} from "./merchant-api-types.js"; +import { + FailCasesByMethod, + OperationFail, + OperationOk, + ResultByMethod, + opEmptySuccess, + opKnownHttpFailure, + opSuccessFromHttp, + opUnknownFailure, +} from "./operation.js"; +import { AmountString } from "./taler-types.js"; +import { TalerProtocolDuration } from "./time.js"; + +const logger = new Logger("MerchantApiClient.ts"); + +// FIXME: Explain! +export type TalerMerchantResultByMethod<prop extends keyof MerchantApiClient> = + ResultByMethod<MerchantApiClient, prop>; + +// FIXME: Explain! +export type TalerMerchantErrorsByMethod<prop extends keyof MerchantApiClient> = + FailCasesByMethod<MerchantApiClient, prop>; + +export interface MerchantAuthConfiguration { + method: "external" | "token"; + token?: string; +} + +// FIXME: Why do we need this? Describe / fix! +export interface PartialMerchantInstanceConfig { + auth?: MerchantAuthConfiguration; + id: string; + name: string; + paytoUris: string[]; + address?: unknown; + jurisdiction?: unknown; + defaultWireTransferDelay?: TalerProtocolDuration; + defaultPayDelay?: TalerProtocolDuration; +} + +export interface CreateMerchantTippingReserveRequest { + // Amount that the merchant promises to put into the reserve + initial_balance: AmountString; + + // Exchange the merchant intends to use for tipping + exchange_url: string; + + // Desired wire method, for example "iban" or "x-taler-bank" + wire_method: string; +} + +export interface DeleteTippingReserveArgs { + reservePub: string; + purge?: boolean; +} + +interface MerchantBankAccount { + // The payto:// URI where the wallet will send coins. + payto_uri: string; + + // Optional base URL for a facade where the + // merchant backend can see incoming wire + // transfers to reconcile its accounting + // with that of the exchange. Used by + // taler-merchant-wirewatch. + credit_facade_url?: string; + + // Credentials for accessing the credit facade. + credit_facade_credentials?: FacadeCredentials; +} + +export interface MerchantInstanceConfig { + auth: MerchantAuthConfiguration; + id: string; + name: string; + address: unknown; + jurisdiction: unknown; + use_stefan: boolean; + default_wire_transfer_delay: TalerProtocolDuration; + default_pay_delay: TalerProtocolDuration; +} + +export interface PrivateOrderStatusQuery { + instance?: string; + orderId: string; + sessionId?: string; +} + +export interface OtpDeviceAddDetails { + // Device ID to use. + otp_device_id: string; + + // Human-readable description for the device. + otp_device_description: string; + + // A base64-encoded key + otp_key: string; + + // Algorithm for computing the POS confirmation. + otp_algorithm: number; + + // Counter for counter-based OTP devices. + otp_ctr?: number; +} + +/** + * Client for the GNU Taler merchant backend. + */ +export class MerchantApiClient { + /** + * Base URL for the particular instance that this merchant API client + * is for. + */ + private baseUrl: string; + + readonly auth: MerchantAuthConfiguration; + + public readonly PROTOCOL_VERSION = "6:0:2"; + + constructor( + baseUrl: string, + options: { auth?: MerchantAuthConfiguration } = {}, + ) { + this.baseUrl = baseUrl; + + this.auth = options?.auth ?? { + method: "external", + }; + } + + httpClient = createPlatformHttpLib(); + + async changeAuth(auth: MerchantAuthConfiguration): Promise<void> { + const url = new URL("private/auth", this.baseUrl); + const res = await this.httpClient.fetch(url.href, { + method: "POST", + body: auth, + headers: this.makeAuthHeader(), + }); + await expectSuccessResponseOrThrow(res); + } + + async getPrivateInstanceInfo(): Promise<any> { + const url = new URL("private", this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + method: "GET", + headers: this.makeAuthHeader(), + }); + return await resp.json(); + } + + async deleteInstance(instanceId: string) { + const url = new URL(`management/instances/${instanceId}`, this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + method: "DELETE", + headers: this.makeAuthHeader(), + }); + await expectSuccessResponseOrThrow(resp); + } + + async createInstance(req: MerchantInstanceConfig): Promise<void> { + const url = new URL("management/instances", this.baseUrl); + await this.httpClient.fetch(url.href, { + method: "POST", + body: req, + headers: this.makeAuthHeader(), + }); + } + + async getInstances(): Promise<MerchantInstancesResponse> { + const url = new URL("management/instances", this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + headers: this.makeAuthHeader(), + }); + return readSuccessResponseJsonOrThrow(resp, codecForAny()); + } + + async getInstanceFullDetails(instanceId: string): Promise<any> { + const url = new URL(`management/instances/${instanceId}`, this.baseUrl); + try { + const resp = await this.httpClient.fetch(url.href, { + headers: this.makeAuthHeader(), + }); + return resp.json(); + } catch (e) { + throw e; + } + } + + async createOrder( + req: MerchantPostOrderRequest, + ): Promise<MerchantPostOrderResponse> { + let url = new URL("private/orders", this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + method: "POST", + body: req, + headers: this.makeAuthHeader(), + }); + return readSuccessResponseJsonOrThrow( + resp, + codecForMerchantPostOrderResponse(), + ); + } + + async deleteOrder(req: { orderId: string; force?: boolean }): Promise<void> { + let url = new URL(`private/orders/${req.orderId}`, this.baseUrl); + if (req.force) { + url.searchParams.set("force", "yes"); + } + const resp = await this.httpClient.fetch(url.href, { + method: "DELETE", + body: req, + headers: this.makeAuthHeader(), + }); + if (resp.status !== 204) { + throw Error(`failed to delete order (status ${resp.status})`); + } + } + + async queryPrivateOrderStatus( + query: PrivateOrderStatusQuery, + ): Promise<TalerMerchantApi.MerchantOrderStatusResponse> { + const reqUrl = new URL(`private/orders/${query.orderId}`, this.baseUrl); + if (query.sessionId) { + reqUrl.searchParams.set("session_id", query.sessionId); + } + const resp = await this.httpClient.fetch(reqUrl.href, { + headers: this.makeAuthHeader(), + }); + return readSuccessResponseJsonOrThrow( + resp, + codecForMerchantOrderPrivateStatusResponse(), + ); + } + + async giveRefund(r: { + instance: string; + orderId: string; + amount: string; + justification: string; + }): Promise<{ talerRefundUri: string }> { + const reqUrl = new URL(`private/orders/${r.orderId}/refund`, this.baseUrl); + const resp = await this.httpClient.fetch(reqUrl.href, { + method: "POST", + body: { + refund: r.amount, + reason: r.justification, + }, + }); + const respBody = await resp.json(); + return { + talerRefundUri: respBody.taler_refund_uri, + }; + } + + async createTemplate(req: MerchantTemplateAddDetails) { + let url = new URL("private/templates", this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + method: "POST", + body: req, + headers: this.makeAuthHeader(), + }); + switch (resp.status) { + case HttpStatusCode.Ok: + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + async getTemplate(templateId: string) { + let url = new URL(`private/templates/${templateId}`, this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + method: "GET", + headers: this.makeAuthHeader(), + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForAny()); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + 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(): Promise<OperationOk<TalerMerchantApi.VersionResponse>> { + const url = new URL(`config`, this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + method: "GET", + }); + switch (resp.status) { + case HttpStatusCode.Ok: + return opSuccessFromHttp(resp, codecForMerchantConfig()); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + async createOtpDevice( + req: OtpDeviceAddDetails, + ): Promise<OperationOk<void> | OperationFail<HttpStatusCode.NotFound>> { + let url = new URL("private/otp-devices", this.baseUrl); + const resp = await this.httpClient.fetch(url.href, { + method: "POST", + body: req, + headers: this.makeAuthHeader(), + }); + switch (resp.status) { + case HttpStatusCode.Ok: + case HttpStatusCode.NoContent: + return opEmptySuccess(resp); + case HttpStatusCode.NotFound: + return opKnownHttpFailure(resp.status, resp); + default: + return opUnknownFailure(resp, await readTalerErrorResponse(resp)); + } + } + + private makeAuthHeader(): Record<string, string> { + switch (this.auth.method) { + case "external": + return {}; + case "token": + return { + Authorization: `Bearer ${this.auth.token}`, + }; + } + } +} |