/*
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 {
createPlatformHttpLib,
expectSuccessResponseOrThrow,
readSuccessResponseJsonOrThrow,
} from "./http.js";
import { FacadeCredentials } from "./libeufin-api-types.js";
import { Logger } from "./logging.js";
import {
MerchantReserveCreateConfirmation,
codecForMerchantReserveCreateConfirmation,
TippingReserveStatus,
MerchantInstancesResponse,
MerchantPostOrderRequest,
MerchantPostOrderResponse,
codecForMerchantPostOrderResponse,
MerchantOrderPrivateStatusResponse,
codecForMerchantOrderPrivateStatusResponse,
RewardCreateRequest,
RewardCreateConfirmation,
MerchantTemplateAddDetails,
} from "./merchant-api-types.js";
import { AmountString } from "./taler-types.js";
import { TalerProtocolDuration } from "./time.js";
const logger = new Logger("MerchantApiClient.ts");
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;
}
/**
* 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;
constructor(baseUrl: string, auth?: MerchantAuthConfiguration) {
this.baseUrl = baseUrl;
this.auth = 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 deleteTippingReserve(req: DeleteTippingReserveArgs): Promise {
const url = new URL(`private/reserves/${req.reservePub}`, this.baseUrl);
if (req.purge) {
url.searchParams.set("purge", "YES");
}
const resp = await this.httpClient.fetch(url.href, {
method: "DELETE",
headers: this.makeAuthHeader(),
});
logger.info(`delete status: ${resp.status}`);
return;
}
async createTippingReserve(
req: CreateMerchantTippingReserveRequest,
): Promise {
const url = new URL("private/reserves", this.baseUrl);
const resp = await this.httpClient.fetch(url.href, {
method: "POST",
body: req,
headers: this.makeAuthHeader(),
});
const respData = readSuccessResponseJsonOrThrow(
resp,
codecForMerchantReserveCreateConfirmation(),
);
return respData;
}
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 getPrivateTipReserves(): Promise {
const url = new URL("private/reserves", this.baseUrl);
const resp = await this.httpClient.fetch(url.href, {
method: "GET",
headers: this.makeAuthHeader(),
});
// FIXME: Validate!
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 giveTip(req: RewardCreateRequest): Promise {
const reqUrl = new URL(`private/rewards`, this.baseUrl);
const resp = await this.httpClient.fetch(reqUrl.href, {
method: "POST",
body: req,
});
// FIXME: validate
return resp.json();
}
async queryTippingReserves(): Promise {
const reqUrl = new URL(`private/reserves`, this.baseUrl);
const resp = await this.httpClient.fetch(reqUrl.href, {
headers: this.makeAuthHeader(),
});
// FIXME: validate
return resp.json();
}
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(),
});
if (resp.status !== 204) {
throw Error("failed to create template");
}
}
private makeAuthHeader(): Record {
switch (this.auth.method) {
case "external":
return {};
case "token":
return {
Authorization: `Bearer ${this.auth.token}`,
};
}
}
}