/*
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,
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 =
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 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> {
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 | 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 readTalerErrorResponse(resp));
}
}
private makeAuthHeader(): Record {
switch (this.auth.method) {
case "external":
return {};
case "token":
return {
Authorization: `Bearer ${this.auth.token}`,
};
}
}
}