/* 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 */ 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, } 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 = ResultByMethod; // FIXME: Explain! export type TalerMerchantErrorsByMethod = FailCasesByMethod; 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 { 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 { 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 { const url = new URL("management/instances", this.baseUrl); await this.httpClient.fetch(url.href, { method: "POST", body: req, headers: this.makeAuthHeader(), }); } async getInstances(): Promise { 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 { 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 { 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 { 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 { 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 resp.text()); } } 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 resp.text()); } } 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> { 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 resp.text()); } } async createOtpDevice( req: OtpDeviceAddDetails, ): Promise | OperationFail> { 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 resp.text()); } } private makeAuthHeader(): Record { switch (this.auth.method) { case "external": return {}; case "token": return { Authorization: `Bearer ${this.auth.token}`, }; } } }