/*
 This file is part of GNU Taler
 (C) 2022-2024 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 {
  AbsoluteTime,
  AccessToken,
  CancellationToken,
  ChallengeRequestResponse,
  ChallengeSolveRequest,
  Duration,
  FailCasesByMethod,
  HttpStatusCode,
  LibtoolVersion,
  LoginTokenRequest,
  MerchantPostDonauBody,
  OperationAlternative,
  OperationFail,
  OperationOk,
  PaginationParams,
  ResultByMethod,
  TalerErrorCode,
  TalerMerchantApi,
  carefullyParseConfig,
  codecForAbortResponse,
  codecForAccountAddResponse,
  codecForAccountKycRedirects,
  codecForAccountsSummaryResponse,
  codecForBankAccountDetail,
  codecForCategoryListResponse,
  codecForCategoryProductList,
  codecForChallengeRequestResponse,
  codecForChallengeResponse,
  codecForClaimResponse,
  codecForIncomingTansferList,
  codecForInstancesResponse,
  codecForInventorySummaryResponse,
  codecForLoginTokenSuccessResponse,
  codecForMerchantOrderPrivateStatusResponse,
  codecForMerchantPosProductDetail,
  codecForMerchantRefundResponse,
  codecForOrderHistory,
  codecForOtpDeviceDetails,
  codecForOtpDeviceSummaryResponse,
  codecForOutOfStockResponse,
  codecForPaidRefundStatusResponse,
  codecForPaymentDeniedLegallyResponse,
  codecForPaymentResponse,
  codecForPostOrderResponse,
  codecForProductDetail,
  codecForQueryInstancesResponse,
  codecForStatisticsAmountResponse,
  codecForStatisticsCounterResponse,
  codecForStatusGoto,
  codecForStatusPaid,
  codecForStatusStatusUnpaid,
  codecForTalerErrorDetail,
  codecForTalerMerchantConfigResponse,
  codecForTansferList,
  codecForTemplateDetails,
  codecForTemplateSummaryResponse,
  codecForTokenFamiliesList,
  codecForTokenFamilyDetails,
  codecForTokenInfoList,
  codecForWalletRefundResponse,
  codecForWalletTemplateDetails,
  codecForWebhookDetails,
  codecForWebhookSummaryResponse,
  opEmptySuccess,
  opFixedSuccess,
  opKnownAlternativeHttpFailure,
  opKnownHttpFailure,
  opKnownTalerFailure,
  opUnknownHttpFailure,
} from "@gnu-taler/taler-util";
import {
  HttpRequestLibrary,
  HttpResponse,
  createPlatformHttpLib,
  readTalerErrorResponse,
} from "@gnu-taler/taler-util/http";
import { opSuccessFromHttp } from "../operation.js";
import {
  CacheEvictor,
  addPaginationParams,
  makeBasicAuthHeader,
  makeBearerTokenAuthHeader,
  nullEvictor,
} from "./utils.js";

export type TalerMerchantInstanceResultByMethod<
  prop extends keyof TalerMerchantInstanceHttpClient,
> = ResultByMethod<TalerMerchantInstanceHttpClient, prop>;
export type TalerMerchantInstanceErrorsByMethod<
  prop extends keyof TalerMerchantInstanceHttpClient,
> = FailCasesByMethod<TalerMerchantInstanceHttpClient, prop>;

/**
 * FIXME: This should probably not be part of the core merchant HTTP client.
 */
export enum TalerMerchantInstanceCacheEviction {
  CREATE_ORDER,
  UPDATE_ORDER,
  DELETE_ORDER,
  UPDATE_CURRENT_INSTANCE,
  DELETE_CURRENT_INSTANCE,
  CREATE_BANK_ACCOUNT,
  UPDATE_BANK_ACCOUNT,
  DELETE_BANK_ACCOUNT,
  CREATE_PRODUCT,
  UPDATE_PRODUCT,
  DELETE_PRODUCT,
  CREATE_CATEGORY,
  UPDATE_CATEGORY,
  DELETE_CATEGORY,
  CREATE_TRANSFER,
  DELETE_TRANSFER,
  CREATE_DEVICE,
  UPDATE_DEVICE,
  DELETE_DEVICE,
  CREATE_TEMPLATE,
  UPDATE_TEMPLATE,
  DELETE_TEMPLATE,
  CREATE_WEBHOOK,
  UPDATE_WEBHOOK,
  DELETE_WEBHOOK,
  CREATE_TOKENFAMILY,
  UPDATE_TOKENFAMILY,
  DELETE_TOKENFAMILY,
  CREATE_ACCESSTOKEN,
  DELETE_ACCESSTOKEN,
  LAST,
}

export enum TalerMerchantManagementCacheEviction {
  CREATE_INSTANCE = TalerMerchantInstanceCacheEviction.LAST + 1,
  UPDATE_INSTANCE,
  DELETE_INSTANCE,
}

/**
 * Protocol version spoken with the core bank.
 *
 * Endpoint must be ordered in the same way that in the docs
 * Response code (http and taler) must have the same order that in the docs
 * That way is easier to see changes
 *
 * Uses libtool's current:revision:age versioning.
 */
export class TalerMerchantInstanceHttpClient {
  public static readonly PROTOCOL_VERSION = "24:0:1";

  readonly httpLib: HttpRequestLibrary;
  readonly cacheEvictor: CacheEvictor<TalerMerchantInstanceCacheEviction>;
  readonly cancellationToken: CancellationToken | undefined;

  constructor(
    readonly baseUrl: string,
    httpClient?: HttpRequestLibrary,
    cacheEvictor?: CacheEvictor<TalerMerchantInstanceCacheEviction>,
    cancellationToken?: CancellationToken,
  ) {
    this.httpLib = httpClient ?? createPlatformHttpLib();
    this.cacheEvictor = cacheEvictor ?? nullEvictor;
    this.cancellationToken = cancellationToken;
  }

  static isCompatible(version: string): boolean {
    const compare = LibtoolVersion.compare(
      TalerMerchantInstanceHttpClient.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 carefullyParseConfig(
          "taler-merchant",
          TalerMerchantInstanceHttpClient.PROTOCOL_VERSION,
          resp,
          codecForTalerMerchantConfigResponse(),
        );
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  //
  // Auth
  //

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-token
   *
   */
  async createAccessToken(
    instance: string,
    password: string,
    body: LoginTokenRequest,
    params: {
      challengeIds?: string[];
    } = {},
  ): Promise<
    | OperationFail<HttpStatusCode.NotFound>
    | OperationOk<TalerMerchantApi.LoginTokenSuccessResponse>
    | OperationAlternative<
        HttpStatusCode.Accepted,
        TalerMerchantApi.ChallengeResponse
      >
    | OperationFail<HttpStatusCode.Unauthorized>
  > {
    const url = new URL(`private/token`, this.baseUrl);
    const headers: Record<string, string> = {};
    headers.Authorization = makeBasicAuthHeader(instance, password);
    if (params.challengeIds && params.challengeIds.length > 0) {
      headers["Taler-Challenge-Ids"] = params.challengeIds.join(", ");
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      headers,
      body,
    });
    switch (resp.status) {
      case HttpStatusCode.Ok: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.CREATE_ACCESSTOKEN,
        );
        return opSuccessFromHttp(resp, codecForLoginTokenSuccessResponse());
      }
      case HttpStatusCode.Accepted: {
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForChallengeResponse(),
        );
      }
      case HttpStatusCode.Unauthorized:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-tokens
   *
   */
  async listAccessTokens(token: AccessToken, params: PaginationParams = {}) {
    const url = new URL(`private/tokens`, this.baseUrl);
    addPaginationParams(url, params);
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers: {
        Authorization: makeBearerTokenAuthHeader(token),
      },
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForTokenInfoList());
      case HttpStatusCode.NoContent:
        return opFixedSuccess({ tokens: [] });
      case HttpStatusCode.Unauthorized:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-tokens-$SERIAL
   *
   */
  async deleteAccessToken(token: AccessToken | undefined, serial: number) {
    const url = new URL(`private/tokens/${String(serial)}`, this.baseUrl);
    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "DELETE",
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.DELETE_ACCESSTOKEN,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Unauthorized:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  //
  // Wallet API
  //

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-orders-$ORDER_ID-claim
   */
  async claimOrder(args: {
    orderId: string;
    body: TalerMerchantApi.ClaimRequest;
  }): Promise<
    | OperationOk<TalerMerchantApi.ClaimResponse>
    | OperationFail<HttpStatusCode.Conflict>
    | OperationFail<TalerErrorCode.MERCHANT_POST_ORDERS_ID_CLAIM_NOT_FOUND>
  > {
    const { orderId, body } = args;
    const url = new URL(`orders/${orderId}/claim`, this.baseUrl);

    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      cancellationToken: this.cancellationToken,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
        );
        return opSuccessFromHttp(resp, codecForClaimResponse());
      }
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound: {
        const body = await resp.json();
        const details = codecForTalerErrorDetail().decode(body);
        switch (details.code) {
          case TalerErrorCode.MERCHANT_POST_ORDERS_ID_CLAIM_NOT_FOUND:
            return opKnownTalerFailure(details.code, details);
          default:
            return opUnknownHttpFailure(resp, details);
        }
      }
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-orders-$ORDER_ID-pay
   */
  async makePayment(orderId: string, body: TalerMerchantApi.PayRequest) {
    const url = new URL(`orders/${orderId}/pay`, this.baseUrl);

    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
        );
        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);
      case HttpStatusCode.UnavailableForLegalReasons:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForPaymentDeniedLegallyResponse(),
        );
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-orders-$ORDER_ID
   */

  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",
      );
    }
    if (params.awaitRefundObtained !== undefined) {
      url.searchParams.set(
        "await_refund_obtained",
        params.allowRefundedForRepurchase ? "YES" : "NO",
      );
    }
    if (params.claimToken !== undefined) {
      url.searchParams.set("token", params.claimToken);
    }
    if (params.contractTermHash !== undefined) {
      url.searchParams.set("h_contract", params.contractTermHash);
    }
    if (params.refund !== undefined) {
      url.searchParams.set("refund", params.refund);
    }
    if (params.sessionId !== undefined) {
      url.searchParams.set("session_id", params.sessionId);
    }
    if (params.timeout !== undefined) {
      url.searchParams.set("timeout_ms", String(params.timeout));
    }

    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      // 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 opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#demonstrating-payment
   */
  async demostratePayment(orderId: string, body: TalerMerchantApi.PaidRequest) {
    const url = new URL(`orders/${orderId}/paid`, this.baseUrl);

    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
        );
        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 opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#aborting-incomplete-payments
   */
  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: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
        );
        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 opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#obtaining-refunds
   */
  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: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
        );
        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);
      case HttpStatusCode.UnavailableForLegalReasons:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForPaymentDeniedLegallyResponse(),
        );
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  //
  // Management
  //

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-auth
   */
  async updateCurrentInstanceAuthentication(
    token: AccessToken | undefined,
    body: TalerMerchantApi.InstanceAuthConfigurationMessage,
    params: {
      challengeIds?: string[];
    } = {},
  ) {
    const url = new URL(`private/auth`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    if (params.challengeIds && params.challengeIds.length > 0) {
      headers["Taler-Challenge-Ids"] = params.challengeIds.join(", ");
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok: // FIXME: missing in docs
        return opEmptySuccess();
      case HttpStatusCode.Accepted: {
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForChallengeResponse(),
        );
      }
      case HttpStatusCode.NoContent:
        return opEmptySuccess();
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private
   */
  async updateCurrentInstance(
    token: AccessToken | undefined,
    body: TalerMerchantApi.InstanceReconfigurationMessage,
    params: { challengeIds?: string[] } = {},
  ) {
    const url = new URL(`private`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    if (params.challengeIds && params.challengeIds.length > 0) {
      headers["Taler-Challenge-Ids"] = params.challengeIds.join(", ");
    }

    const resp = await this.httpLib.fetch(url.href, {
      method: "PATCH",
      body,
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_CURRENT_INSTANCE,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Accepted: {
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForChallengeResponse(),
        );
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private
   *
   */
  async getCurrentInstanceDetails(token: AccessToken | undefined) {
    const url = new URL(`private`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForQueryInstancesResponse());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private
   */
  async deleteCurrentInstance(
    token: AccessToken | undefined,
    params: { purge?: boolean; challengeIds?: string[] } = {},
  ) {
    const url = new URL(`private`, this.baseUrl);

    if (params.purge !== undefined) {
      url.searchParams.set("purge", params.purge ? "YES" : "NO");
    }

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    if (params.challengeIds && params.challengeIds.length > 0) {
      headers["Taler-Challenge-Ids"] = params.challengeIds.join(", ");
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "DELETE",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.DELETE_CURRENT_INSTANCE,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Accepted:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForChallengeResponse(),
        );
      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 opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get--instances-$INSTANCE-private-kyc
   */
  async getCurrentInstanceKycStatus(
    token: AccessToken | undefined,
    params: TalerMerchantApi.GetKycStatusRequestParams = {},
  ): Promise<
    | OperationOk<TalerMerchantApi.MerchantAccountKycRedirectsResponse>
    | OperationOk<void>
    | OperationAlternative<
        HttpStatusCode.BadGateway,
        TalerMerchantApi.MerchantAccountKycRedirectsResponse
      >
    | OperationFail<HttpStatusCode.Unauthorized>
    | OperationFail<HttpStatusCode.NotFound>
    | OperationFail<HttpStatusCode.ServiceUnavailable>
    | OperationFail<HttpStatusCode.GatewayTimeout>
  > {
    const url = new URL(`private/kyc`, this.baseUrl);

    if (params.wireHash) {
      url.searchParams.set("h_wire", params.wireHash);
    }
    if (params.exchangeURL) {
      url.searchParams.set("exchange_url", params.exchangeURL);
    }
    if (params.timeout) {
      url.searchParams.set("timeout_ms", String(params.timeout));
    }
    if (params.reason) {
      url.searchParams.set("lpt", String(params.reason));
    }

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForAccountKycRedirects());
      case HttpStatusCode.Accepted:
        return opSuccessFromHttp(resp, codecForAccountKycRedirects());
      case HttpStatusCode.NoContent:
        return opEmptySuccess();
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.BadGateway:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForAccountKycRedirects(),
        );
      case HttpStatusCode.ServiceUnavailable:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.GatewayTimeout:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  //
  // Bank Accounts
  //

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-accounts
   */
  async addBankAccount(
    token: AccessToken | undefined,
    body: TalerMerchantApi.AccountAddDetails,
    params: {
      challengeIds?: string[];
    } = {},
  ) {
    const url = new URL(`private/accounts`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    if (params.challengeIds && params.challengeIds.length > 0) {
      headers["Taler-Challenge-Ids"] = params.challengeIds.join(", ");
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.CREATE_BANK_ACCOUNT,
        );
        return opSuccessFromHttp(resp, codecForAccountAddResponse());
      }
      case HttpStatusCode.Accepted:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForChallengeResponse(),
        );
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-accounts-$H_WIRE
   */
  async updateBankAccount(
    token: AccessToken | undefined,
    wireAccount: string,
    body: TalerMerchantApi.AccountPatchDetails,
  ) {
    const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "PATCH",
      body,
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_BANK_ACCOUNT,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts
   */
  async listBankAccounts(
    token: AccessToken | undefined,
    params?: PaginationParams,
  ) {
    const url = new URL(`private/accounts`, this.baseUrl);

    // addPaginationParams(url, params);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForAccountsSummaryResponse());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-accounts-$H_WIRE
   */
  async getBankAccountDetails(
    token: AccessToken | undefined,
    wireAccount: string,
  ) {
    const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForBankAccountDetail());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-accounts-$H_WIRE
   */
  async deleteBankAccount(token: AccessToken | undefined, wireAccount: string) {
    const url = new URL(`private/accounts/${wireAccount}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "DELETE",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.DELETE_BANK_ACCOUNT,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  //
  // Inventory Management
  //

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-categories
   */
  async listCategories(
    token: AccessToken | undefined,
    params?: PaginationParams,
  ) {
    const url = new URL(`private/categories`, this.baseUrl);

    // addPaginationParams(url, params);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForCategoryListResponse());
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-categories-$CATEGORY_ID
   */
  async getCategoryDetails(token: AccessToken | undefined, cId: string) {
    const url = new URL(`private/categories/${cId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForCategoryProductList());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-categories
   */
  async addCategory(
    token: AccessToken | undefined,
    body: TalerMerchantApi.CategoryCreateRequest,
  ) {
    const url = new URL(`private/categories`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.CREATE_CATEGORY,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      // case HttpStatusCode.Conflict:
      //   return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-categories
   */
  async updateCategory(
    token: AccessToken | undefined,
    cid: string,
    body: TalerMerchantApi.CategoryCreateRequest,
  ) {
    const url = new URL(`private/categories/${cid}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "PATCH",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_CATEGORY,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      // case HttpStatusCode.Conflict:
      //   return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-categories-$CATEGORY_ID
   */
  async deleteCategory(token: AccessToken | undefined, cId: string) {
    const url = new URL(`private/categories/${cId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "DELETE",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.DELETE_CATEGORY,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      // case HttpStatusCode.Conflict:
      //   return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-products
   */
  async addProduct(
    token: AccessToken | undefined,
    body: TalerMerchantApi.ProductAddDetail,
  ) {
    const url = new URL(`private/products`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.CREATE_PRODUCT,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-products-$PRODUCT_ID
   */
  async updateProduct(
    token: AccessToken | undefined,
    productId: string,
    body: TalerMerchantApi.ProductPatchDetail,
  ) {
    const url = new URL(`private/products/${productId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "PATCH",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products
   */
  async listProducts(
    token: AccessToken | undefined,
    params: PaginationParams & {
      category?: string;
      name?: string;
      description?: string;
    } = {},
  ) {
    const url = new URL(`private/products`, this.baseUrl);

    addPaginationParams(url, params);
    if (params.category) {
      url.searchParams.set("category_filter", params.category);
    }
    if (params.name) {
      url.searchParams.set("name_filter", params.name);
    }
    if (params.description) {
      url.searchParams.set("description_filter", params.description);
    }

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForInventorySummaryResponse());
      case HttpStatusCode.Unauthorized: // FIXME: not in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-pos
   */
  async getPointOfSaleInventory(token: AccessToken | undefined) {
    const url = new URL(`private/pos`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForMerchantPosProductDetail());
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-products-$PRODUCT_ID
   */
  async getProductDetails(token: AccessToken | undefined, productId: string) {
    const url = new URL(`private/products/${productId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForProductDetail());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-products-$PRODUCT_ID-lock
   */
  async lockProduct(
    token: AccessToken | undefined,
    productId: string,
    body: TalerMerchantApi.LockRequest,
  ) {
    const url = new URL(`private/products/${productId}/lock`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_PRODUCT,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Gone:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-products-$PRODUCT_ID
   */
  async deleteProduct(token: AccessToken | undefined, productId: string) {
    const url = new URL(`private/products/${productId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "DELETE",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.DELETE_PRODUCT,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  //
  // Payment processing
  //

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-orders
   */
  async createOrder(
    token: AccessToken | undefined,
    body: TalerMerchantApi.PostOrderRequest,
  ) {
    const url = new URL(`private/orders`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });
    return this.procesOrderCreationResponse(resp);
  }

  private async procesOrderCreationResponse(resp: HttpResponse) {
    switch (resp.status) {
      case HttpStatusCode.Ok: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.CREATE_ORDER,
        );
        return opSuccessFromHttp(resp, codecForPostOrderResponse());
      }
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.UnavailableForLegalReasons:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForPaymentDeniedLegallyResponse(),
        );
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Gone:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForOutOfStockResponse(),
        );
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#inspecting-orders
   */
  async listOrders(
    token: AccessToken | undefined,
    params: TalerMerchantApi.ListOrdersRequestParams = {},
  ) {
    const url = new URL(`private/orders`, this.baseUrl);

    if (params.paid !== undefined) {
      url.searchParams.set("paid", params.paid ? "YES" : "NO");
    }
    if (params.refunded !== undefined) {
      url.searchParams.set("refunded", params.refunded ? "YES" : "NO");
    }
    if (params.wired !== undefined) {
      url.searchParams.set("wired", params.wired ? "YES" : "NO");
    }
    if (params.date && !AbsoluteTime.isNever(params.date)) {
      const time = AbsoluteTime.toProtocolTimestamp(params.date);
      url.searchParams.set("date_s", String(time.t_s));
    }
    if (params.timeout) {
      url.searchParams.set("timeout_ms", String(params.timeout));
    }
    if (params.sessionId) {
      url.searchParams.set("session_id", params.sessionId);
    }
    if (params.fulfillmentUrl) {
      url.searchParams.set("fulfillment_url", params.fulfillmentUrl);
    }
    if (params.summary) {
      url.searchParams.set("summary_filter", params.summary);
    }
    addPaginationParams(url, params);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForOrderHistory());
      case HttpStatusCode.NotFound: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-orders-$ORDER_ID
   */
  async getOrderDetails(
    token: AccessToken | undefined,
    orderId: string,
    params: TalerMerchantApi.GetOrderRequestParams = {},
  ) {
    const url = new URL(`private/orders/${orderId}`, this.baseUrl);

    if (params.allowRefundedForRepurchase !== undefined) {
      url.searchParams.set(
        "allow_refunded_for_repurchase",
        params.allowRefundedForRepurchase ? "YES" : "NO",
      );
    }
    if (params.sessionId) {
      url.searchParams.set("session_id", params.sessionId);
    }
    if (params.timeout) {
      url.searchParams.set("timeout_ms", String(params.timeout));
    }

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(
          resp,
          codecForMerchantOrderPrivateStatusResponse(),
        );
      case HttpStatusCode.NotFound: {
        const details = await readTalerErrorResponse(resp);
        switch (details.code) {
          case TalerErrorCode.MERCHANT_GENERIC_ORDER_UNKNOWN:
            return opKnownTalerFailure(details.code, details);
          case TalerErrorCode.MERCHANT_GENERIC_INSTANCE_UNKNOWN:
            return opKnownTalerFailure(details.code, details);
          default:
            return opUnknownHttpFailure(resp, details);
        }
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#private-order-data-cleanup
   */
  async forgetOrder(
    token: AccessToken | undefined,
    orderId: string,
    body: TalerMerchantApi.ForgetRequest,
  ) {
    const url = new URL(`private/orders/${orderId}/forget`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "PATCH",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.NoContent:
        return opEmptySuccess();
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.BadRequest:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-orders-$ORDER_ID
   */
  async deleteOrder(
    token: AccessToken | undefined,
    orderId: string,
    force: boolean = false,
  ) {
    const url = new URL(`private/orders/${orderId}`, this.baseUrl);
    if (force) {
      url.searchParams.set("force", "yes");
    }

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "DELETE",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.DELETE_ORDER,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  //
  // Refunds
  //

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-orders-$ORDER_ID-refund
   */
  async addRefund(
    token: AccessToken | undefined,
    orderId: string,
    body: TalerMerchantApi.RefundRequest,
  ) {
    const url = new URL(`private/orders/${orderId}/refund`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_ORDER,
        );
        return opSuccessFromHttp(resp, codecForMerchantRefundResponse());
      }
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Gone:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.UnavailableForLegalReasons:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForPaymentDeniedLegallyResponse(),
        );
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  //
  // Wire Transfer
  //

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-transfers
   */
  async informWireTransfer(
    token: AccessToken | undefined,
    body: TalerMerchantApi.TransferInformation,
  ) {
    const url = new URL(`private/transfers`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.CREATE_TRANSFER,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-transfers
   */
  async listConfirmedWireTransfers(
    token: AccessToken | undefined,
    params: TalerMerchantApi.ListConfirmedWireTransferRequestParams = {},
  ) {
    const url = new URL(`private/transfers`, this.baseUrl);

    if (params.paytoURI) {
      url.searchParams.set("payto_uri", params.paytoURI);
    }
    if (params.before) {
      url.searchParams.set("before", String(params.before));
    }
    if (params.after) {
      url.searchParams.set("after", String(params.after));
    }
    if (params.expected !== undefined) {
      url.searchParams.set("expected", params.expected ? "YES" : "NO");
    }
    addPaginationParams(url, params);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForTansferList());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-incoming
   */
  async listIncomingWireTransfers(
    token: AccessToken | undefined,
    params: TalerMerchantApi.ListIncomingWireTransferRequestParams = {},
  ) {
    const url = new URL(`private/incoming`, this.baseUrl);

    if (params.paytoURI) {
      url.searchParams.set("payto_uri", params.paytoURI);
    }
    if (params.before) {
      url.searchParams.set("before", String(params.before));
    }
    if (params.after) {
      url.searchParams.set("after", String(params.after));
    }
    if (params.verified !== undefined) {
      url.searchParams.set("verified", params.verified ? "YES" : "NO");
    }
    if (params.confirmed !== undefined) {
      url.searchParams.set("verified", params.confirmed ? "YES" : "NO");
    }
    addPaginationParams(url, params);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForIncomingTansferList());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  // /**
  //  * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-transfers-$TID
  //  * @deprecated
  //  */
  // async deleteWireTransfer(token: AccessToken | undefined, transferId: string) {
  //   const url = new URL(`private/transfers/${transferId}`, this.baseUrl);

  //   const headers: Record<string, string> = {};
  //   if (token) {
  //     headers.Authorization = makeBearerTokenAuthHeader(token);
  //   }
  //   const resp = await this.httpLib.fetch(url.href, {
  //     method: "DELETE",
  //     headers,
  //   });

  //   switch (resp.status) {
  //     case HttpStatusCode.NoContent: {
  //       this.cacheEvictor.notifySuccess(
  //         TalerMerchantInstanceCacheEviction.DELETE_TRANSFER,
  //       );
  //       return opEmptySuccess();
  //     }
  //     case HttpStatusCode.Unauthorized: // FIXME: missing in docs
  //       return opKnownHttpFailure(resp.status, resp);
  //     case HttpStatusCode.NotFound:
  //       return opKnownHttpFailure(resp.status, resp);
  //     case HttpStatusCode.Conflict:
  //       return opKnownHttpFailure(resp.status, resp);
  //     default:
  //       return opUnknownHttpFailure(resp);
  //   }
  // }

  //
  // OTP Devices
  //

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-otp-devices
   */
  async addOtpDevice(
    token: AccessToken | undefined,
    body: TalerMerchantApi.OtpDeviceAddDetails,
  ) {
    const url = new URL(`private/otp-devices`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.CREATE_DEVICE,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID
   */
  async updateOtpDevice(
    token: AccessToken | undefined,
    deviceId: string,
    body: TalerMerchantApi.OtpDevicePatchDetails,
  ) {
    const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "PATCH",
      body,
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_DEVICE,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices
   */
  async listOtpDevices(
    token: AccessToken | undefined,
    params?: PaginationParams,
  ) {
    const url = new URL(`private/otp-devices`, this.baseUrl);

    addPaginationParams(url, params);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForOtpDeviceSummaryResponse());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID
   */
  async getOtpDeviceDetails(
    token: AccessToken | undefined,
    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));
    }
    if (params.price) {
      url.searchParams.set("price", params.price);
    }
    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForOtpDeviceDetails());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-otp-devices-$DEVICE_ID
   */
  async deleteOtpDevice(token: AccessToken | undefined, deviceId: string) {
    const url = new URL(`private/otp-devices/${deviceId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "DELETE",
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.DELETE_DEVICE,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  //
  // Templates
  //

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-private-templates
   */
  async addTemplate(
    token: AccessToken | undefined,
    body: TalerMerchantApi.TemplateAddDetails,
  ) {
    const url = new URL(`private/templates`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.CREATE_TEMPLATE,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID
   */
  async updateTemplate(
    token: AccessToken | undefined,
    templateId: string,
    body: TalerMerchantApi.TemplatePatchDetails,
  ) {
    const url = new URL(`private/templates/${templateId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "PATCH",
      body,
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_TEMPLATE,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#inspecting-template
   */
  async listTemplates(
    token: AccessToken | undefined,
    params?: PaginationParams,
  ) {
    const url = new URL(`private/templates`, this.baseUrl);

    addPaginationParams(url, params);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForTemplateSummaryResponse());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID
   */
  async getTemplateDetails(token: AccessToken | undefined, templateId: string) {
    const url = new URL(`private/templates/${templateId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForTemplateDetails());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCE]-private-templates-$TEMPLATE_ID
   */
  async deleteTemplate(token: AccessToken | undefined, templateId: string) {
    const url = new URL(`private/templates/${templateId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "DELETE",
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.DELETE_TEMPLATE,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCE]-templates-$TEMPLATE_ID
   */
  async useTemplateGetInfo(templateId: string) {
    const url = new URL(`templates/${templateId}`, this.baseUrl);

    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForWalletTemplateDetails());
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-templates-$TEMPLATE_ID
   */
  async useTemplateCreateOrder(
    templateId: string,
    body: TalerMerchantApi.UsingTemplateDetails,
  ) {
    const url = new URL(`templates/${templateId}`, this.baseUrl);

    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
    });

    return this.procesOrderCreationResponse(resp);
  }

  //
  // Webhooks
  //

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-webhooks
   */
  async addWebhook(
    token: AccessToken | undefined,
    body: TalerMerchantApi.WebhookAddDetails,
  ) {
    const url = new URL(`private/webhooks`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.CREATE_WEBHOOK,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID
   */
  async updateWebhook(
    token: AccessToken | undefined,
    webhookId: string,
    body: TalerMerchantApi.WebhookPatchDetails,
  ) {
    const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "PATCH",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_WEBHOOK,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-webhooks
   */
  async listWebhooks(
    token: AccessToken | undefined,
    params?: PaginationParams,
  ) {
    const url = new URL(`private/webhooks`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForWebhookSummaryResponse());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID
   */
  async getWebhookDetails(token: AccessToken | undefined, webhookId: string) {
    const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForWebhookDetails());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-webhooks-$WEBHOOK_ID
   */
  async deleteWebhook(token: AccessToken | undefined, webhookId: string) {
    const url = new URL(`private/webhooks/${webhookId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "DELETE",
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.DELETE_WEBHOOK,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  //
  // token families
  //

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCES]-private-tokenfamilies
   */
  async createTokenFamily(
    token: AccessToken | undefined,
    body: TalerMerchantApi.TokenFamilyCreateRequest,
  ) {
    const url = new URL(`private/tokenfamilies`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.CREATE_TOKENFAMILY,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#patch-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG
   */
  async updateTokenFamily(
    token: AccessToken | undefined,
    tokenSlug: string,
    body: TalerMerchantApi.TokenFamilyUpdateRequest,
  ) {
    const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "PATCH",
      body,
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_TOKENFAMILY,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Ok: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.UPDATE_TOKENFAMILY,
        );
        return opSuccessFromHttp(resp, codecForTokenFamilyDetails());
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-tokenfamilies
   */
  async listTokenFamilies(
    token: AccessToken | undefined,
    params?: PaginationParams,
  ) {
    const url = new URL(`private/tokenfamilies`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForTokenFamiliesList());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG
   */
  async getTokenFamilyDetails(
    token: AccessToken | undefined,
    tokenSlug: string,
  ) {
    const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForTokenFamilyDetails());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#delete-[-instances-$INSTANCES]-private-tokenfamilies-$TOKEN_FAMILY_SLUG
   */
  async deleteTokenFamily(token: AccessToken | undefined, tokenSlug: string) {
    const url = new URL(`private/tokenfamilies/${tokenSlug}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "DELETE",
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.DELETE_TOKENFAMILY,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }
  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-challenge-$CHALLENGE_ID
   *
   */
  async sendChallenge(cid: string) {
    const url = new URL(`challenge/${cid}`, this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      // FIXME: this should be removed
      body: {},
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForChallengeRequestResponse());
      case HttpStatusCode.NoContent:
        return opFixedSuccess<ChallengeRequestResponse>({});
      case HttpStatusCode.Unauthorized:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound: {
        const details = await readTalerErrorResponse(resp);
        switch (details.code) {
          case TalerErrorCode.MERCHANT_TAN_CHALLENGE_UNKNOWN:
            return opKnownTalerFailure(details.code, details);
          default:
            return opUnknownHttpFailure(resp, details);
        }
      }
      case HttpStatusCode.Gone: {
        const details = await readTalerErrorResponse(resp);
        switch (details.code) {
          case TalerErrorCode.MERCHANT_TAN_CHALLENGE_SOLVED:
            return opKnownTalerFailure(details.code, details);
          default:
            return opUnknownHttpFailure(resp, details);
        }
      }
      case HttpStatusCode.TooManyRequests: {
        const details = await readTalerErrorResponse(resp);
        switch (details.code) {
          case TalerErrorCode.MERCHANT_TAN_TOO_EARLY:
            return opKnownTalerFailure(details.code, details);
          default:
            return opUnknownHttpFailure(resp, details);
        }
      }
      case HttpStatusCode.BadGateway: {
        const details = await readTalerErrorResponse(resp);
        switch (details.code) {
          case TalerErrorCode.MERCHANT_TAN_MFA_HELPER_EXEC_FAILED:
            return opKnownTalerFailure(details.code, details);
          default:
            return opUnknownHttpFailure(resp, details);
        }
      }
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#post-[-instances-$INSTANCE]-challenge-$CHALLENGE_ID-confirm
   *
   */
  async confirmChallenge(cid: string, body: ChallengeSolveRequest) {
    const url = new URL(`challenge/${cid}/confirm`, this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent:
        return opEmptySuccess();
      case HttpStatusCode.Unauthorized:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound: {
        const details = await readTalerErrorResponse(resp);
        switch (details.code) {
          case TalerErrorCode.MERCHANT_TAN_CHALLENGE_UNKNOWN:
            return opKnownTalerFailure(details.code, details);
          default:
            return opUnknownHttpFailure(resp, details);
        }
      }
      case HttpStatusCode.Conflict: {
        const details = await readTalerErrorResponse(resp);
        switch (details.code) {
          case TalerErrorCode.MERCHANT_TAN_CHALLENGE_FAILED:
            return opKnownTalerFailure(details.code, details);
          default:
            return opUnknownHttpFailure(resp, details);
        }
      }
      case HttpStatusCode.TooManyRequests: {
        const details = await readTalerErrorResponse(resp);
        switch (details.code) {
          case TalerErrorCode.MERCHANT_TAN_TOO_MANY_ATTEMPTS:
            return opKnownTalerFailure(details.code, details);
          default:
            return opUnknownHttpFailure(resp, details);
        }
      }
      default:
        return opUnknownHttpFailure(resp);
    }
  }
  /**
   * https://docs.taler.net/core/api-merchant.html#post--instances-$INSTANCE-forgot-password
   */
  async forgotPasswordSelfProvision(
    body: TalerMerchantApi.InstanceAuthConfigurationMessage,
    params: {
      challengeIds?: string[];
    } = {},
  ) {
    const url = new URL(`forgot-password`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (params.challengeIds && params.challengeIds.length > 0) {
      headers["Taler-Challenge-Ids"] = params.challengeIds.join(", ");
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        return opEmptySuccess();
      }
      case HttpStatusCode.Accepted: {
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForChallengeResponse(),
        );
      }
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Forbidden:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Unauthorized:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  async postDonau(args: {
    body: MerchantPostDonauBody;
    token?: AccessToken;
  }): Promise<OperationOk<void> | OperationFail<HttpStatusCode.BadGateway>> {
    const headers: Record<string, string> = {};
    if (args.token) {
      headers.Authorization = makeBearerTokenAuthHeader(args.token);
    }
    const url = new URL(`private/donau`, this.baseUrl);
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      headers,
      body: args.body,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent:
      case HttpStatusCode.Created:
      case HttpStatusCode.Ok: {
        return opEmptySuccess();
      }
      case HttpStatusCode.BadGateway:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * Get the auth api against 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
   */
  getAuthenticationAPI(): URL {
    return new URL(`private/`, this.baseUrl);
  }
}

export type TalerMerchantManagementResultByMethod<
  prop extends keyof TalerMerchantManagementHttpClient,
> = ResultByMethod<TalerMerchantManagementHttpClient, prop>;
export type TalerMerchantManagementErrorsByMethod<
  prop extends keyof TalerMerchantManagementHttpClient,
> = FailCasesByMethod<TalerMerchantManagementHttpClient, prop>;

export class TalerMerchantManagementHttpClient extends TalerMerchantInstanceHttpClient {
  readonly cacheManagementEvictor: CacheEvictor<
    TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction
  >;
  constructor(
    readonly baseUrl: string,
    httpClient?: HttpRequestLibrary,
    // cacheManagementEvictor?: CacheEvictor<TalerMerchantManagementCacheEviction>,
    cacheEvictor?: CacheEvictor<
      TalerMerchantInstanceCacheEviction | TalerMerchantManagementCacheEviction
    >,
  ) {
    super(baseUrl, httpClient, cacheEvictor);
    this.cacheManagementEvictor = cacheEvictor ?? nullEvictor;
  }

  getSubInstanceAPI(instanceId: string): string {
    return new URL(`instances/${instanceId}/`, this.baseUrl).href;
  }

  //
  // Instance Management
  //

  /**
   * https://docs.taler.net/core/api-merchant.html#post--instances
   */
  async createInstanceSelfProvision(
    body: TalerMerchantApi.InstanceConfigurationMessage,
    params: {
      tokenValidity?: Duration;
      challengeIds?: string[];
    } = {},
  ) {
    const url = new URL(`instances`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (params.challengeIds && params.challengeIds.length > 0) {
      headers["Taler-Challenge-Ids"] = params.challengeIds.join(", ");
    }
    if (params.tokenValidity) {
      url.searchParams.append(
        "token_validity_ms",
        String(params.tokenValidity.d_ms),
      );
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok: {
        this.cacheManagementEvictor.notifySuccess(
          TalerMerchantManagementCacheEviction.CREATE_INSTANCE,
        );
        this.cacheEvictor.notifySuccess(
          TalerMerchantInstanceCacheEviction.CREATE_ACCESSTOKEN,
        );
        return opSuccessFromHttp(resp, codecForLoginTokenSuccessResponse());
      }
      case HttpStatusCode.NoContent: {
        this.cacheManagementEvictor.notifySuccess(
          TalerMerchantManagementCacheEviction.CREATE_INSTANCE,
        );
        return opFixedSuccess(undefined);
      }
      case HttpStatusCode.Accepted:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForChallengeResponse(),
        );
      case HttpStatusCode.Unauthorized:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#post--management-instances
   */
  async createInstance(
    token: AccessToken | undefined,
    body: TalerMerchantApi.InstanceConfigurationMessage,
    params: {
      challengeIds?: string[];
    } = {},
  ) {
    const url = new URL(`management/instances`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    if (params.challengeIds && params.challengeIds.length > 0) {
      headers["Taler-Challenge-Ids"] = params.challengeIds.join(", ");
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheManagementEvictor.notifySuccess(
          TalerMerchantManagementCacheEviction.CREATE_INSTANCE,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Accepted:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForChallengeResponse(),
        );
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#post--management-instances-$INSTANCE-auth
   */
  async updateInstanceAuthentication(
    token: AccessToken | undefined,
    instanceId: string,
    body: TalerMerchantApi.InstanceAuthConfigurationMessage,
    params: {
      challengeIds?: string[];
    } = {},
  ) {
    const url = new URL(
      `management/instances/${instanceId}/auth`,
      this.baseUrl,
    );

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    if (params.challengeIds && params.challengeIds.length > 0) {
      headers["Taler-Challenge-Ids"] = params.challengeIds.join(", ");
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "POST",
      body,
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Accepted:
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForChallengeResponse(),
        );
      case HttpStatusCode.NoContent:
        return opEmptySuccess();
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#patch--management-instances-$INSTANCE
   */
  async updateInstance(
    token: AccessToken | undefined,
    instanceId: string,
    body: TalerMerchantApi.InstanceReconfigurationMessage,
  ) {
    const url = new URL(`management/instances/${instanceId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "PATCH",
      body,
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheManagementEvictor.notifySuccess(
          TalerMerchantManagementCacheEviction.UPDATE_INSTANCE,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get--management-instances
   */
  async listInstances(
    token: AccessToken | undefined,
    params?: PaginationParams,
  ) {
    const url = new URL(`management/instances`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForInstancesResponse());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE
   *
   */
  async getInstanceDetails(token: AccessToken | undefined, instanceId: string) {
    const url = new URL(`management/instances/${instanceId}`, this.baseUrl);

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });

    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForQueryInstancesResponse());
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#delete--management-instances-$INSTANCE
   */
  async deleteInstance(
    token: AccessToken | undefined,
    instanceId: string,
    params: { purge?: boolean; challengeIds?: string[] } = {},
  ) {
    const url = new URL(`management/instances/${instanceId}`, this.baseUrl);

    if (params.purge !== undefined) {
      url.searchParams.set("purge", params.purge ? "YES" : "NO");
    }

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    if (params.challengeIds && params.challengeIds.length > 0) {
      headers["Taler-Challenge-Ids"] = params.challengeIds.join(", ");
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "DELETE",
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.NoContent: {
        this.cacheManagementEvictor.notifySuccess(
          TalerMerchantManagementCacheEviction.DELETE_INSTANCE,
        );
        return opEmptySuccess();
      }
      case HttpStatusCode.Accepted: {
        return opKnownAlternativeHttpFailure(
          resp,
          resp.status,
          codecForChallengeResponse(),
        );
      }
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Conflict:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE-kyc
   */
  async getIntanceKycStatus(
    token: AccessToken | undefined,
    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);
    }
    if (params.exchangeURL) {
      url.searchParams.set("exchange_url", params.exchangeURL);
    }
    if (params.timeout) {
      url.searchParams.set("timeout_ms", String(params.timeout));
    }

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.Accepted:
        return opSuccessFromHttp(resp, codecForAccountKycRedirects());
      case HttpStatusCode.NoContent:
        return opEmptySuccess();
      case HttpStatusCode.NotFound:
        return opEmptySuccess();
      case HttpStatusCode.Unauthorized: // FIXME: missing in docs
        return opKnownHttpFailure(resp.status, 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 opUnknownHttpFailure(resp);
    }
  }

  /**
   * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE-statistics-counter-$SLUG
   */
  async getStatisticsCounter(
    token: AccessToken | undefined,
    statSlug: string,
    params: TalerMerchantApi.GetStatisticsRequestParams = {},
  ) {
    const url = new URL(`private/statistics-counter/${statSlug}`, this.baseUrl);

    if (params.by) {
      url.searchParams.set("by", params.by);
    }

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForStatisticsCounterResponse());
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Unauthorized:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.BadGateway:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.ServiceUnavailable:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }
  /**
   * https://docs.taler.net/core/api-merchant.html#get--management-instances-$INSTANCE-statistics-amount-$SLUG
   */
  async getStatisticsAmount(
    token: AccessToken | undefined,
    statSlug: string,
    params: TalerMerchantApi.GetStatisticsRequestParams = {},
  ) {
    const url = new URL(`private/statistics-amount/${statSlug}`, this.baseUrl);

    if (params.by) {
      url.searchParams.set("by", params.by);
    }

    const headers: Record<string, string> = {};
    if (token) {
      headers.Authorization = makeBearerTokenAuthHeader(token);
    }
    const resp = await this.httpLib.fetch(url.href, {
      method: "GET",
      headers,
    });
    switch (resp.status) {
      case HttpStatusCode.Ok:
        return opSuccessFromHttp(resp, codecForStatisticsAmountResponse());
      case HttpStatusCode.NotFound:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.Unauthorized:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.BadGateway:
        return opKnownHttpFailure(resp.status, resp);
      case HttpStatusCode.ServiceUnavailable:
        return opKnownHttpFailure(resp.status, resp);
      default:
        return opUnknownHttpFailure(resp);
    }
  }
}
