/*
 This file is part of GNU Taler
 (C) 2022-2023 Taler Systems S.A.

 GNU Taler is free software; you can redistribute it and/or modify it under the
 terms of the GNU General Public License as published by the Free Software
 Foundation; either version 3, or (at your option) any later version.

 GNU Taler is distributed in the hope that it will be useful, but WITHOUT ANY
 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 A PARTICULAR PURPOSE.  See the GNU General Public License for more details.

 You should have received a copy of the GNU General Public License along with
 GNU Taler; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
 */

import {
  AbsoluteTime,
  Amounts,
  CheckPeerPullCreditRequest,
  CheckPeerPullCreditResponse,
  ContractTermsUtil,
  ExchangeReservePurseRequest,
  HttpStatusCode,
  InitiatePeerPullCreditRequest,
  InitiatePeerPullCreditResponse,
  Logger,
  NotificationType,
  PeerContractTerms,
  TalerErrorCode,
  TalerPreciseTimestamp,
  TalerProtocolTimestamp,
  TalerUriAction,
  Transaction,
  TransactionAction,
  TransactionIdStr,
  TransactionMajorState,
  TransactionMinorState,
  TransactionState,
  TransactionType,
  WalletAccountMergeFlags,
  WalletKycUuid,
  assertUnreachable,
  checkDbInvariant,
  codecForAny,
  codecForWalletKycUuid,
  encodeCrock,
  getRandomBytes,
  j2s,
  makeErrorDetail,
  stringifyPayPullUri,
  stringifyTalerUri,
  talerPaytoFromExchangeReserve,
} from "@gnu-taler/taler-util";
import { readSuccessResponseJsonOrThrow } from "@gnu-taler/taler-util/http";
import {
  PendingTaskType,
  TaskIdStr,
  TaskIdentifiers,
  TaskRunResult,
  TaskRunResultType,
  TombstoneTag,
  TransactionContext,
  constructTaskIdentifier,
  requireExchangeTosAcceptedOrThrow,
  runWithClientCancellation,
} from "./common.js";
import {
  KycPendingInfo,
  KycUserType,
  OperationRetryRecord,
  PeerPullCreditRecord,
  PeerPullPaymentCreditStatus,
  WalletDbAllStoresReadOnlyTransaction,
  WithdrawalGroupRecord,
  WithdrawalGroupStatus,
  WithdrawalRecordType,
  timestampOptionalPreciseFromDb,
  timestampPreciseFromDb,
  timestampPreciseToDb,
} from "./db.js";
import { fetchFreshExchange } from "./exchanges.js";
import {
  codecForExchangePurseStatus,
  getMergeReserveInfo,
} from "./pay-peer-common.js";
import {
  constructTransactionIdentifier,
  isUnsuccessfulTransaction,
  notifyTransition,
} from "./transactions.js";
import { WalletExecutionContext } from "./wallet.js";
import {
  getExchangeWithdrawalInfo,
  internalCreateWithdrawalGroup,
  waitWithdrawalFinal,
} from "./withdraw.js";

const logger = new Logger("pay-peer-pull-credit.ts");

export class PeerPullCreditTransactionContext implements TransactionContext {
  readonly transactionId: TransactionIdStr;
  readonly taskId: TaskIdStr;

  constructor(
    public wex: WalletExecutionContext,
    public pursePub: string,
  ) {
    this.taskId = constructTaskIdentifier({
      tag: PendingTaskType.PeerPullCredit,
      pursePub,
    });
    this.transactionId = constructTransactionIdentifier({
      tag: TransactionType.PeerPullCredit,
      pursePub,
    });
  }

  /**
   * Get the full transaction details for the transaction.
   *
   * Returns undefined if the transaction is in a state where we do not have a
   * transaction item (e.g. if it was deleted).
   */
  async lookupFullTransaction(
    tx: WalletDbAllStoresReadOnlyTransaction,
  ): Promise<Transaction | undefined> {
    const pullCredit = await tx.peerPullCredit.get(this.pursePub);
    if (!pullCredit) {
      return undefined;
    }
    const ct = await tx.contractTerms.get(pullCredit.contractTermsHash);
    checkDbInvariant(!!ct, `no contract terms for p2p push ${this.pursePub}`);

    const peerContractTerms = ct.contractTermsRaw;

    let wsr: WithdrawalGroupRecord | undefined = undefined;
    let wsrOrt: OperationRetryRecord | undefined = undefined;
    if (pullCredit.withdrawalGroupId) {
      wsr = await tx.withdrawalGroups.get(pullCredit.withdrawalGroupId);
      if (wsr) {
        const withdrawalOpId = TaskIdentifiers.forWithdrawal(wsr);
        wsrOrt = await tx.operationRetries.get(withdrawalOpId);
      }
    }
    const pullCreditOpId =
      TaskIdentifiers.forPeerPullPaymentInitiation(pullCredit);
    let pullCreditOrt = await tx.operationRetries.get(pullCreditOpId);

    if (wsr) {
      if (wsr.wgInfo.withdrawalType !== WithdrawalRecordType.PeerPullCredit) {
        throw Error(`Unexpected withdrawalType: ${wsr.wgInfo.withdrawalType}`);
      }
      /**
       * FIXME: this should be handled in the withdrawal process.
       * PeerPull withdrawal fails until reserve have funds but it is not
       * an error from the user perspective.
       */
      const silentWithdrawalErrorForInvoice =
        wsrOrt?.lastError &&
        wsrOrt.lastError.code ===
          TalerErrorCode.WALLET_WITHDRAWAL_GROUP_INCOMPLETE &&
        Object.values(wsrOrt.lastError.errorsPerCoin ?? {}).every((e) => {
          return (
            e.code === TalerErrorCode.WALLET_UNEXPECTED_REQUEST_ERROR &&
            e.httpStatusCode === 409
          );
        });
      const txState = computePeerPullCreditTransactionState(pullCredit);
      checkDbInvariant(wsr.instructedAmount !== undefined, "wg uninitialized");
      checkDbInvariant(wsr.denomsSel !== undefined, "wg uninitialized");
      checkDbInvariant(wsr.exchangeBaseUrl !== undefined, "wg uninitialized");
      return {
        type: TransactionType.PeerPullCredit,
        txState,
        txActions: computePeerPullCreditTransactionActions(pullCredit),
        amountEffective: isUnsuccessfulTransaction(txState)
          ? Amounts.stringify(Amounts.zeroOfAmount(wsr.instructedAmount))
          : Amounts.stringify(wsr.denomsSel.totalCoinValue),
        amountRaw: Amounts.stringify(wsr.instructedAmount),
        exchangeBaseUrl: wsr.exchangeBaseUrl,
        timestamp: timestampPreciseFromDb(pullCredit.mergeTimestamp),
        info: {
          expiration: peerContractTerms.purse_expiration,
          summary: peerContractTerms.summary,
        },
        talerUri: stringifyPayPullUri({
          exchangeBaseUrl: wsr.exchangeBaseUrl,
          contractPriv: wsr.wgInfo.contractPriv,
        }),
        transactionId: constructTransactionIdentifier({
          tag: TransactionType.PeerPullCredit,
          pursePub: pullCredit.pursePub,
        }),
        kycUrl: pullCredit.kycUrl,
        ...(wsrOrt?.lastError
          ? {
              error: silentWithdrawalErrorForInvoice
                ? undefined
                : wsrOrt.lastError,
            }
          : {}),
      };
    }

    const txState = computePeerPullCreditTransactionState(pullCredit);
    return {
      type: TransactionType.PeerPullCredit,
      txState,
      txActions: computePeerPullCreditTransactionActions(pullCredit),
      amountEffective: isUnsuccessfulTransaction(txState)
        ? Amounts.stringify(Amounts.zeroOfAmount(peerContractTerms.amount))
        : Amounts.stringify(pullCredit.estimatedAmountEffective),
      amountRaw: Amounts.stringify(peerContractTerms.amount),
      exchangeBaseUrl: pullCredit.exchangeBaseUrl,
      timestamp: timestampPreciseFromDb(pullCredit.mergeTimestamp),
      info: {
        expiration: peerContractTerms.purse_expiration,
        summary: peerContractTerms.summary,
      },
      talerUri: stringifyPayPullUri({
        exchangeBaseUrl: pullCredit.exchangeBaseUrl,
        contractPriv: pullCredit.contractPriv,
      }),
      transactionId: constructTransactionIdentifier({
        tag: TransactionType.PeerPullCredit,
        pursePub: pullCredit.pursePub,
      }),
      kycUrl: pullCredit.kycUrl,
      ...(pullCreditOrt?.lastError ? { error: pullCreditOrt.lastError } : {}),
    };
  }

  async deleteTransaction(): Promise<void> {
    const { wex: ws, pursePub } = this;
    await ws.db.runReadWriteTx(
      { storeNames: ["withdrawalGroups", "peerPullCredit", "tombstones"] },
      async (tx) => {
        const pullIni = await tx.peerPullCredit.get(pursePub);
        if (!pullIni) {
          return;
        }
        if (pullIni.withdrawalGroupId) {
          const withdrawalGroupId = pullIni.withdrawalGroupId;
          const withdrawalGroupRecord =
            await tx.withdrawalGroups.get(withdrawalGroupId);
          if (withdrawalGroupRecord) {
            await tx.withdrawalGroups.delete(withdrawalGroupId);
            await tx.tombstones.put({
              id: TombstoneTag.DeleteWithdrawalGroup + ":" + withdrawalGroupId,
            });
          }
        }
        await tx.peerPullCredit.delete(pursePub);
        await tx.tombstones.put({
          id: TombstoneTag.DeletePeerPullCredit + ":" + pursePub,
        });
      },
    );

    return;
  }

  async suspendTransaction(): Promise<void> {
    const { wex, pursePub, taskId: retryTag, transactionId } = this;
    const transitionInfo = await wex.db.runReadWriteTx(
      { storeNames: ["peerPullCredit"] },
      async (tx) => {
        const pullCreditRec = await tx.peerPullCredit.get(pursePub);
        if (!pullCreditRec) {
          logger.warn(`peer pull credit ${pursePub} not found`);
          return;
        }
        let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
        switch (pullCreditRec.status) {
          case PeerPullPaymentCreditStatus.PendingCreatePurse:
            newStatus = PeerPullPaymentCreditStatus.SuspendedCreatePurse;
            break;
          case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
            newStatus = PeerPullPaymentCreditStatus.SuspendedMergeKycRequired;
            break;
          case PeerPullPaymentCreditStatus.PendingWithdrawing:
            newStatus = PeerPullPaymentCreditStatus.SuspendedWithdrawing;
            break;
          case PeerPullPaymentCreditStatus.PendingReady:
            newStatus = PeerPullPaymentCreditStatus.SuspendedReady;
            break;
          case PeerPullPaymentCreditStatus.AbortingDeletePurse:
            newStatus =
              PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse;
            break;
          case PeerPullPaymentCreditStatus.Done:
          case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
          case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
          case PeerPullPaymentCreditStatus.SuspendedReady:
          case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
          case PeerPullPaymentCreditStatus.Aborted:
          case PeerPullPaymentCreditStatus.Failed:
          case PeerPullPaymentCreditStatus.Expired:
          case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
            break;
          default:
            assertUnreachable(pullCreditRec.status);
        }
        if (newStatus != null) {
          const oldTxState =
            computePeerPullCreditTransactionState(pullCreditRec);
          pullCreditRec.status = newStatus;
          const newTxState =
            computePeerPullCreditTransactionState(pullCreditRec);
          await tx.peerPullCredit.put(pullCreditRec);
          return {
            oldTxState,
            newTxState,
          };
        }
        return undefined;
      },
    );
    wex.taskScheduler.stopShepherdTask(retryTag);
    notifyTransition(wex, transactionId, transitionInfo);
  }

  async failTransaction(): Promise<void> {
    const { wex, pursePub, taskId: retryTag, transactionId } = this;
    const transitionInfo = await wex.db.runReadWriteTx(
      { storeNames: ["peerPullCredit"] },
      async (tx) => {
        const pullCreditRec = await tx.peerPullCredit.get(pursePub);
        if (!pullCreditRec) {
          logger.warn(`peer pull credit ${pursePub} not found`);
          return;
        }
        let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
        switch (pullCreditRec.status) {
          case PeerPullPaymentCreditStatus.PendingCreatePurse:
          case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
          case PeerPullPaymentCreditStatus.PendingWithdrawing:
          case PeerPullPaymentCreditStatus.PendingReady:
          case PeerPullPaymentCreditStatus.Done:
          case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
          case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
          case PeerPullPaymentCreditStatus.SuspendedReady:
          case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
          case PeerPullPaymentCreditStatus.Aborted:
          case PeerPullPaymentCreditStatus.Failed:
          case PeerPullPaymentCreditStatus.Expired:
            break;
          case PeerPullPaymentCreditStatus.AbortingDeletePurse:
          case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
            newStatus = PeerPullPaymentCreditStatus.Failed;
            break;
          default:
            assertUnreachable(pullCreditRec.status);
        }
        if (newStatus != null) {
          const oldTxState =
            computePeerPullCreditTransactionState(pullCreditRec);
          pullCreditRec.status = newStatus;
          const newTxState =
            computePeerPullCreditTransactionState(pullCreditRec);
          await tx.peerPullCredit.put(pullCreditRec);
          return {
            oldTxState,
            newTxState,
          };
        }
        return undefined;
      },
    );
    notifyTransition(wex, transactionId, transitionInfo);
    wex.taskScheduler.stopShepherdTask(retryTag);
  }

  async resumeTransaction(): Promise<void> {
    const { wex, pursePub, taskId: retryTag, transactionId } = this;
    const transitionInfo = await wex.db.runReadWriteTx(
      { storeNames: ["peerPullCredit"] },
      async (tx) => {
        const pullCreditRec = await tx.peerPullCredit.get(pursePub);
        if (!pullCreditRec) {
          logger.warn(`peer pull credit ${pursePub} not found`);
          return;
        }
        let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
        switch (pullCreditRec.status) {
          case PeerPullPaymentCreditStatus.PendingCreatePurse:
          case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
          case PeerPullPaymentCreditStatus.PendingWithdrawing:
          case PeerPullPaymentCreditStatus.PendingReady:
          case PeerPullPaymentCreditStatus.AbortingDeletePurse:
          case PeerPullPaymentCreditStatus.Done:
          case PeerPullPaymentCreditStatus.Failed:
          case PeerPullPaymentCreditStatus.Expired:
          case PeerPullPaymentCreditStatus.Aborted:
            break;
          case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
            newStatus = PeerPullPaymentCreditStatus.PendingCreatePurse;
            break;
          case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
            newStatus = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
            break;
          case PeerPullPaymentCreditStatus.SuspendedReady:
            newStatus = PeerPullPaymentCreditStatus.PendingReady;
            break;
          case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
            newStatus = PeerPullPaymentCreditStatus.PendingWithdrawing;
            break;
          case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
            newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
            break;
          default:
            assertUnreachable(pullCreditRec.status);
        }
        if (newStatus != null) {
          const oldTxState =
            computePeerPullCreditTransactionState(pullCreditRec);
          pullCreditRec.status = newStatus;
          const newTxState =
            computePeerPullCreditTransactionState(pullCreditRec);
          await tx.peerPullCredit.put(pullCreditRec);
          return {
            oldTxState,
            newTxState,
          };
        }
        return undefined;
      },
    );
    notifyTransition(wex, transactionId, transitionInfo);
    wex.taskScheduler.startShepherdTask(retryTag);
  }

  async abortTransaction(): Promise<void> {
    const { wex, pursePub, taskId: retryTag, transactionId } = this;
    const transitionInfo = await wex.db.runReadWriteTx(
      { storeNames: ["peerPullCredit"] },
      async (tx) => {
        const pullCreditRec = await tx.peerPullCredit.get(pursePub);
        if (!pullCreditRec) {
          logger.warn(`peer pull credit ${pursePub} not found`);
          return;
        }
        let newStatus: PeerPullPaymentCreditStatus | undefined = undefined;
        switch (pullCreditRec.status) {
          case PeerPullPaymentCreditStatus.PendingCreatePurse:
          case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
            newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
            break;
          case PeerPullPaymentCreditStatus.PendingWithdrawing:
            throw Error("can't abort anymore");
          case PeerPullPaymentCreditStatus.PendingReady:
            newStatus = PeerPullPaymentCreditStatus.AbortingDeletePurse;
            break;
          case PeerPullPaymentCreditStatus.Done:
          case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
          case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
          case PeerPullPaymentCreditStatus.SuspendedReady:
          case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
          case PeerPullPaymentCreditStatus.Aborted:
          case PeerPullPaymentCreditStatus.AbortingDeletePurse:
          case PeerPullPaymentCreditStatus.Failed:
          case PeerPullPaymentCreditStatus.Expired:
          case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
            break;
          default:
            assertUnreachable(pullCreditRec.status);
        }
        if (newStatus != null) {
          const oldTxState =
            computePeerPullCreditTransactionState(pullCreditRec);
          pullCreditRec.status = newStatus;
          const newTxState =
            computePeerPullCreditTransactionState(pullCreditRec);
          await tx.peerPullCredit.put(pullCreditRec);
          return {
            oldTxState,
            newTxState,
          };
        }
        return undefined;
      },
    );
    wex.taskScheduler.stopShepherdTask(retryTag);
    notifyTransition(wex, transactionId, transitionInfo);
    wex.taskScheduler.startShepherdTask(retryTag);
  }
}

async function queryPurseForPeerPullCredit(
  wex: WalletExecutionContext,
  pullIni: PeerPullCreditRecord,
): Promise<TaskRunResult> {
  const purseDepositUrl = new URL(
    `purses/${pullIni.pursePub}/deposit`,
    pullIni.exchangeBaseUrl,
  );
  purseDepositUrl.searchParams.set("timeout_ms", "30000");
  logger.info(`querying purse status via ${purseDepositUrl.href}`);
  const resp = await wex.ws.runLongpollQueueing(
    wex,
    purseDepositUrl.hostname,
    async () => {
      return await wex.http.fetch(purseDepositUrl.href, {
        timeout: { d_ms: 60000 },
        cancellationToken: wex.cancellationToken,
      });
    },
  );
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub: pullIni.pursePub,
  });

  logger.info(`purse status code: HTTP ${resp.status}`);

  switch (resp.status) {
    case HttpStatusCode.Gone: {
      // Exchange says that purse doesn't exist anymore => expired!
      const transitionInfo = await wex.db.runReadWriteTx(
        { storeNames: ["peerPullCredit"] },
        async (tx) => {
          const finPi = await tx.peerPullCredit.get(pullIni.pursePub);
          if (!finPi) {
            logger.warn("peerPullCredit not found anymore");
            return;
          }
          const oldTxState = computePeerPullCreditTransactionState(finPi);
          if (finPi.status === PeerPullPaymentCreditStatus.PendingReady) {
            finPi.status = PeerPullPaymentCreditStatus.Expired;
          }
          await tx.peerPullCredit.put(finPi);
          const newTxState = computePeerPullCreditTransactionState(finPi);
          return { oldTxState, newTxState };
        },
      );
      notifyTransition(wex, transactionId, transitionInfo);
      return TaskRunResult.backoff();
    }
    case HttpStatusCode.NotFound:
      // FIXME: Maybe check error code? 404 could also mean something else.
      return TaskRunResult.longpollReturnedPending();
  }

  const result = await readSuccessResponseJsonOrThrow(
    resp,
    codecForExchangePurseStatus(),
  );

  logger.trace(`purse status: ${j2s(result)}`);

  const depositTimestamp = result.deposit_timestamp;

  if (!depositTimestamp || TalerProtocolTimestamp.isNever(depositTimestamp)) {
    logger.info("purse not ready yet (no deposit)");
    return TaskRunResult.backoff();
  }

  const reserve = await wex.db.runReadOnlyTx(
    { storeNames: ["reserves"] },
    async (tx) => {
      return await tx.reserves.get(pullIni.mergeReserveRowId);
    },
  );

  if (!reserve) {
    throw Error("reserve for peer pull credit not found in wallet DB");
  }

  await internalCreateWithdrawalGroup(wex, {
    amount: Amounts.parseOrThrow(pullIni.amount),
    wgInfo: {
      withdrawalType: WithdrawalRecordType.PeerPullCredit,
      contractPriv: pullIni.contractPriv,
    },
    forcedWithdrawalGroupId: pullIni.withdrawalGroupId,
    exchangeBaseUrl: pullIni.exchangeBaseUrl,
    reserveStatus: WithdrawalGroupStatus.PendingQueryingStatus,
    reserveKeyPair: {
      priv: reserve.reservePriv,
      pub: reserve.reservePub,
    },
  });
  const transitionInfo = await wex.db.runReadWriteTx(
    { storeNames: ["peerPullCredit"] },
    async (tx) => {
      const finPi = await tx.peerPullCredit.get(pullIni.pursePub);
      if (!finPi) {
        logger.warn("peerPullCredit not found anymore");
        return;
      }
      const oldTxState = computePeerPullCreditTransactionState(finPi);
      if (finPi.status === PeerPullPaymentCreditStatus.PendingReady) {
        finPi.status = PeerPullPaymentCreditStatus.PendingWithdrawing;
      }
      await tx.peerPullCredit.put(finPi);
      const newTxState = computePeerPullCreditTransactionState(finPi);
      return { oldTxState, newTxState };
    },
  );
  notifyTransition(wex, transactionId, transitionInfo);
  return TaskRunResult.backoff();
}

async function longpollKycStatus(
  wex: WalletExecutionContext,
  pursePub: string,
  exchangeUrl: string,
  kycInfo: KycPendingInfo,
  userType: KycUserType,
): Promise<TaskRunResult> {
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub,
  });
  const url = new URL(
    `kyc-check/${kycInfo.requirementRow}/${kycInfo.paytoHash}/${userType}`,
    exchangeUrl,
  );
  const kycStatusRes = await wex.ws.runLongpollQueueing(
    wex,
    url.hostname,
    async (timeoutMs) => {
      url.searchParams.set("timeout_ms", `${timeoutMs}`);
      logger.info(`kyc url ${url.href}`);
      return await wex.http.fetch(url.href, {
        method: "GET",
        cancellationToken: wex.cancellationToken,
      });
    },
  );
  if (
    kycStatusRes.status === HttpStatusCode.Ok ||
    // FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
    // remove after the exchange is fixed or clarified
    kycStatusRes.status === HttpStatusCode.NoContent
  ) {
    const transitionInfo = await wex.db.runReadWriteTx(
      { storeNames: ["peerPullCredit"] },
      async (tx) => {
        const peerIni = await tx.peerPullCredit.get(pursePub);
        if (!peerIni) {
          return;
        }
        if (
          peerIni.status !== PeerPullPaymentCreditStatus.PendingMergeKycRequired
        ) {
          return;
        }
        const oldTxState = computePeerPullCreditTransactionState(peerIni);
        peerIni.status = PeerPullPaymentCreditStatus.PendingCreatePurse;
        const newTxState = computePeerPullCreditTransactionState(peerIni);
        await tx.peerPullCredit.put(peerIni);
        return { oldTxState, newTxState };
      },
    );
    notifyTransition(wex, transactionId, transitionInfo);
    return TaskRunResult.progress();
  } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
    return TaskRunResult.longpollReturnedPending();
  } else {
    throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
  }
}

async function processPeerPullCreditAbortingDeletePurse(
  wex: WalletExecutionContext,
  peerPullIni: PeerPullCreditRecord,
): Promise<TaskRunResult> {
  const { pursePub, pursePriv } = peerPullIni;
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub,
  });

  const sigResp = await wex.cryptoApi.signDeletePurse({
    pursePriv,
  });
  const purseUrl = new URL(`purses/${pursePub}`, peerPullIni.exchangeBaseUrl);
  const resp = await wex.http.fetch(purseUrl.href, {
    method: "DELETE",
    headers: {
      "taler-purse-signature": sigResp.sig,
    },
    cancellationToken: wex.cancellationToken,
  });
  logger.info(`deleted purse with response status ${resp.status}`);

  const transitionInfo = await wex.db.runReadWriteTx(
    {
      storeNames: [
        "peerPullCredit",
        "refreshGroups",
        "denominations",
        "coinAvailability",
        "coins",
      ],
    },
    async (tx) => {
      const ppiRec = await tx.peerPullCredit.get(pursePub);
      if (!ppiRec) {
        return undefined;
      }
      if (ppiRec.status !== PeerPullPaymentCreditStatus.AbortingDeletePurse) {
        return undefined;
      }
      const oldTxState = computePeerPullCreditTransactionState(ppiRec);
      ppiRec.status = PeerPullPaymentCreditStatus.Aborted;
      await tx.peerPullCredit.put(ppiRec);
      const newTxState = computePeerPullCreditTransactionState(ppiRec);
      return {
        oldTxState,
        newTxState,
      };
    },
  );
  notifyTransition(wex, transactionId, transitionInfo);

  return TaskRunResult.backoff();
}

async function handlePeerPullCreditWithdrawing(
  wex: WalletExecutionContext,
  pullIni: PeerPullCreditRecord,
): Promise<TaskRunResult> {
  if (!pullIni.withdrawalGroupId) {
    throw Error("invalid db state (withdrawing, but no withdrawal group ID");
  }
  await waitWithdrawalFinal(wex, pullIni.withdrawalGroupId);
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub: pullIni.pursePub,
  });
  const wgId = pullIni.withdrawalGroupId;
  let finished: boolean = false;
  const transitionInfo = await wex.db.runReadWriteTx(
    { storeNames: ["peerPullCredit", "withdrawalGroups"] },
    async (tx) => {
      const ppi = await tx.peerPullCredit.get(pullIni.pursePub);
      if (!ppi) {
        finished = true;
        return;
      }
      if (ppi.status !== PeerPullPaymentCreditStatus.PendingWithdrawing) {
        finished = true;
        return;
      }
      const oldTxState = computePeerPullCreditTransactionState(ppi);
      const wg = await tx.withdrawalGroups.get(wgId);
      if (!wg) {
        // FIXME: Fail the operation instead?
        return undefined;
      }
      switch (wg.status) {
        case WithdrawalGroupStatus.Done:
          finished = true;
          ppi.status = PeerPullPaymentCreditStatus.Done;
          break;
        // FIXME: Also handle other final states!
      }
      await tx.peerPullCredit.put(ppi);
      const newTxState = computePeerPullCreditTransactionState(ppi);
      return {
        oldTxState,
        newTxState,
      };
    },
  );
  notifyTransition(wex, transactionId, transitionInfo);
  if (finished) {
    return TaskRunResult.finished();
  } else {
    // FIXME: Return indicator that we depend on the other operation!
    return TaskRunResult.backoff();
  }
}

async function handlePeerPullCreditCreatePurse(
  wex: WalletExecutionContext,
  pullIni: PeerPullCreditRecord,
): Promise<TaskRunResult> {
  const purseFee = Amounts.stringify(Amounts.zeroOfAmount(pullIni.amount));
  const pursePub = pullIni.pursePub;
  const mergeReserve = await wex.db.runReadOnlyTx(
    { storeNames: ["reserves"] },
    async (tx) => {
      return tx.reserves.get(pullIni.mergeReserveRowId);
    },
  );

  if (!mergeReserve) {
    throw Error("merge reserve for peer pull payment not found in database");
  }

  const contractTermsRecord = await wex.db.runReadOnlyTx(
    { storeNames: ["contractTerms"] },
    async (tx) => {
      return tx.contractTerms.get(pullIni.contractTermsHash);
    },
  );

  if (!contractTermsRecord) {
    throw Error("contract terms for peer pull payment not found in database");
  }

  const contractTerms: PeerContractTerms = contractTermsRecord.contractTermsRaw;

  const reservePayto = talerPaytoFromExchangeReserve(
    pullIni.exchangeBaseUrl,
    mergeReserve.reservePub,
  );

  const econtractResp = await wex.cryptoApi.encryptContractForDeposit({
    contractPriv: pullIni.contractPriv,
    contractPub: pullIni.contractPub,
    contractTerms: contractTermsRecord.contractTermsRaw,
    pursePriv: pullIni.pursePriv,
    pursePub: pullIni.pursePub,
    nonce: pullIni.contractEncNonce,
  });

  const mergeTimestamp = timestampPreciseFromDb(pullIni.mergeTimestamp);

  const purseExpiration = contractTerms.purse_expiration;
  const sigRes = await wex.cryptoApi.signReservePurseCreate({
    contractTermsHash: pullIni.contractTermsHash,
    flags: WalletAccountMergeFlags.CreateWithPurseFee,
    mergePriv: pullIni.mergePriv,
    mergeTimestamp: TalerPreciseTimestamp.round(mergeTimestamp),
    purseAmount: pullIni.amount,
    purseExpiration: purseExpiration,
    purseFee: purseFee,
    pursePriv: pullIni.pursePriv,
    pursePub: pullIni.pursePub,
    reservePayto,
    reservePriv: mergeReserve.reservePriv,
  });

  const reservePurseReqBody: ExchangeReservePurseRequest = {
    merge_sig: sigRes.mergeSig,
    merge_timestamp: TalerPreciseTimestamp.round(mergeTimestamp),
    h_contract_terms: pullIni.contractTermsHash,
    merge_pub: pullIni.mergePub,
    min_age: 0,
    purse_expiration: purseExpiration,
    purse_fee: purseFee,
    purse_pub: pullIni.pursePub,
    purse_sig: sigRes.purseSig,
    purse_value: pullIni.amount,
    reserve_sig: sigRes.accountSig,
    econtract: econtractResp.econtract,
  };

  logger.info(`reserve purse request: ${j2s(reservePurseReqBody)}`);

  const reservePurseMergeUrl = new URL(
    `reserves/${mergeReserve.reservePub}/purse`,
    pullIni.exchangeBaseUrl,
  );

  const httpResp = await wex.http.fetch(reservePurseMergeUrl.href, {
    method: "POST",
    body: reservePurseReqBody,
    cancellationToken: wex.cancellationToken,
  });

  if (httpResp.status === HttpStatusCode.UnavailableForLegalReasons) {
    const respJson = await httpResp.json();
    const kycPending = codecForWalletKycUuid().decode(respJson);
    logger.info(`kyc uuid response: ${j2s(kycPending)}`);
    return processPeerPullCreditKycRequired(wex, pullIni, kycPending);
  }

  const resp = await readSuccessResponseJsonOrThrow(httpResp, codecForAny());

  logger.info(`reserve merge response: ${j2s(resp)}`);

  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub: pullIni.pursePub,
  });

  const transitionInfo = await wex.db.runReadWriteTx(
    { storeNames: ["peerPullCredit"] },
    async (tx) => {
      const pi2 = await tx.peerPullCredit.get(pursePub);
      if (!pi2) {
        return;
      }
      const oldTxState = computePeerPullCreditTransactionState(pi2);
      pi2.status = PeerPullPaymentCreditStatus.PendingReady;
      await tx.peerPullCredit.put(pi2);
      const newTxState = computePeerPullCreditTransactionState(pi2);
      return { oldTxState, newTxState };
    },
  );
  notifyTransition(wex, transactionId, transitionInfo);
  return TaskRunResult.backoff();
}

export async function processPeerPullCredit(
  wex: WalletExecutionContext,
  pursePub: string,
): Promise<TaskRunResult> {
  if (!wex.ws.networkAvailable) {
    return TaskRunResult.networkRequired();
  }

  const pullIni = await wex.db.runReadOnlyTx(
    { storeNames: ["peerPullCredit"] },
    async (tx) => {
      return tx.peerPullCredit.get(pursePub);
    },
  );
  if (!pullIni) {
    throw Error("peer pull payment initiation not found in database");
  }

  const retryTag = constructTaskIdentifier({
    tag: PendingTaskType.PeerPullCredit,
    pursePub,
  });

  logger.trace(`processing ${retryTag}, status=${pullIni.status}`);

  switch (pullIni.status) {
    case PeerPullPaymentCreditStatus.Done: {
      return TaskRunResult.finished();
    }
    case PeerPullPaymentCreditStatus.PendingReady:
      return queryPurseForPeerPullCredit(wex, pullIni);
    case PeerPullPaymentCreditStatus.PendingMergeKycRequired: {
      if (!pullIni.kycInfo) {
        throw Error("invalid state, kycInfo required");
      }
      return await longpollKycStatus(
        wex,
        pursePub,
        pullIni.exchangeBaseUrl,
        pullIni.kycInfo,
        "individual",
      );
    }
    case PeerPullPaymentCreditStatus.PendingCreatePurse:
      return handlePeerPullCreditCreatePurse(wex, pullIni);
    case PeerPullPaymentCreditStatus.AbortingDeletePurse:
      return await processPeerPullCreditAbortingDeletePurse(wex, pullIni);
    case PeerPullPaymentCreditStatus.PendingWithdrawing:
      return handlePeerPullCreditWithdrawing(wex, pullIni);
    case PeerPullPaymentCreditStatus.Aborted:
    case PeerPullPaymentCreditStatus.Failed:
    case PeerPullPaymentCreditStatus.Expired:
    case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
    case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
    case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
    case PeerPullPaymentCreditStatus.SuspendedReady:
    case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
      break;
    default:
      assertUnreachable(pullIni.status);
  }

  return TaskRunResult.finished();
}

async function processPeerPullCreditKycRequired(
  wex: WalletExecutionContext,
  peerIni: PeerPullCreditRecord,
  kycPending: WalletKycUuid,
): Promise<TaskRunResult> {
  const transactionId = constructTransactionIdentifier({
    tag: TransactionType.PeerPullCredit,
    pursePub: peerIni.pursePub,
  });
  const { pursePub } = peerIni;

  const userType = "individual";
  const url = new URL(
    `kyc-check/${kycPending.requirement_row}/${kycPending.h_payto}/${userType}`,
    peerIni.exchangeBaseUrl,
  );

  logger.info(`kyc url ${url.href}`);
  const kycStatusRes = await wex.http.fetch(url.href, {
    method: "GET",
    cancellationToken: wex.cancellationToken,
  });

  if (
    kycStatusRes.status === HttpStatusCode.Ok ||
    //FIXME: NoContent is not expected https://docs.taler.net/core/api-exchange.html#post--purses-$PURSE_PUB-merge
    // remove after the exchange is fixed or clarified
    kycStatusRes.status === HttpStatusCode.NoContent
  ) {
    logger.warn("kyc requested, but already fulfilled");
    return TaskRunResult.backoff();
  } else if (kycStatusRes.status === HttpStatusCode.Accepted) {
    const kycStatus = await kycStatusRes.json();
    logger.info(`kyc status: ${j2s(kycStatus)}`);
    const { transitionInfo, result } = await wex.db.runReadWriteTx(
      { storeNames: ["peerPullCredit"] },
      async (tx) => {
        const peerInc = await tx.peerPullCredit.get(pursePub);
        if (!peerInc) {
          return {
            transitionInfo: undefined,
            result: TaskRunResult.finished(),
          };
        }
        const oldTxState = computePeerPullCreditTransactionState(peerInc);
        peerInc.kycInfo = {
          paytoHash: kycPending.h_payto,
          requirementRow: kycPending.requirement_row,
        };
        peerInc.kycUrl = kycStatus.kyc_url;
        peerInc.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
        const newTxState = computePeerPullCreditTransactionState(peerInc);
        await tx.peerPullCredit.put(peerInc);
        // We'll remove this eventually!  New clients should rely on the
        // kycUrl field of the transaction, not the error code.
        const res: TaskRunResult = {
          type: TaskRunResultType.Error,
          errorDetail: makeErrorDetail(
            TalerErrorCode.WALLET_WITHDRAWAL_KYC_REQUIRED,
            {
              kycUrl: kycStatus.kyc_url,
            },
          ),
        };
        return {
          transitionInfo: { oldTxState, newTxState },
          result: res,
        };
      },
    );
    notifyTransition(wex, transactionId, transitionInfo);
    return TaskRunResult.backoff();
  } else {
    throw Error(`unexpected response from kyc-check (${kycStatusRes.status})`);
  }
}

/**
 * Check fees and available exchanges for a peer push payment initiation.
 */
export async function checkPeerPullCredit(
  wex: WalletExecutionContext,
  req: CheckPeerPullCreditRequest,
): Promise<CheckPeerPullCreditResponse> {
  return runWithClientCancellation(
    wex,
    "checkPeerPullCredit",
    req.clientCancellationId,
    async () => internalCheckPeerPullCredit(wex, req),
  );
}

/**
 * Check fees and available exchanges for a peer push payment initiation.
 */
export async function internalCheckPeerPullCredit(
  wex: WalletExecutionContext,
  req: CheckPeerPullCreditRequest,
): Promise<CheckPeerPullCreditResponse> {
  // FIXME: We don't support exchanges with purse fees yet.
  // Select an exchange where we have money in the specified currency
  // FIXME: How do we handle regional currency scopes here? Is it an additional input?

  logger.trace("checking peer-pull-credit fees");

  const currency = Amounts.currencyOf(req.amount);
  let exchangeUrl;
  if (req.exchangeBaseUrl) {
    exchangeUrl = req.exchangeBaseUrl;
  } else {
    exchangeUrl = await getPreferredExchangeForCurrency(wex, currency);
  }

  if (!exchangeUrl) {
    throw Error("no exchange found for initiating a peer pull payment");
  }

  logger.trace(`found ${exchangeUrl} as preferred exchange`);

  const wi = await getExchangeWithdrawalInfo(
    wex,
    exchangeUrl,
    Amounts.parseOrThrow(req.amount),
    undefined,
  );
  if (wi.selectedDenoms.selectedDenoms.length === 0) {
    throw Error(
      `unable to check pull payment from ${exchangeUrl}, can't select denominations for instructed amount (${req.amount}`,
    );
  }

  logger.trace(`got withdrawal info`);

  let numCoins = 0;
  for (let i = 0; i < wi.selectedDenoms.selectedDenoms.length; i++) {
    numCoins += wi.selectedDenoms.selectedDenoms[i].count;
  }

  return {
    exchangeBaseUrl: exchangeUrl,
    amountEffective: wi.withdrawalAmountEffective,
    amountRaw: req.amount,
    numCoins,
  };
}

/**
 * Find a preferred exchange based on when we withdrew last from this exchange.
 */
async function getPreferredExchangeForCurrency(
  wex: WalletExecutionContext,
  currency: string,
): Promise<string | undefined> {
  // Find an exchange with the matching currency.
  // Prefer exchanges with the most recent withdrawal.
  const url = await wex.db.runReadOnlyTx(
    { storeNames: ["exchanges"] },
    async (tx) => {
      const exchanges = await tx.exchanges.iter().toArray();
      let candidate = undefined;
      for (const e of exchanges) {
        if (e.detailsPointer?.currency !== currency) {
          continue;
        }
        if (!candidate) {
          candidate = e;
          continue;
        }
        if (candidate.lastWithdrawal && !e.lastWithdrawal) {
          continue;
        }
        const exchangeLastWithdrawal = timestampOptionalPreciseFromDb(
          e.lastWithdrawal,
        );
        const candidateLastWithdrawal = timestampOptionalPreciseFromDb(
          candidate.lastWithdrawal,
        );
        if (exchangeLastWithdrawal && candidateLastWithdrawal) {
          if (
            AbsoluteTime.cmp(
              AbsoluteTime.fromPreciseTimestamp(exchangeLastWithdrawal),
              AbsoluteTime.fromPreciseTimestamp(candidateLastWithdrawal),
            ) > 0
          ) {
            candidate = e;
          }
        }
      }
      if (candidate) {
        return candidate.baseUrl;
      }
      return undefined;
    },
  );
  return url;
}

/**
 * Initiate a peer pull payment.
 */
export async function initiatePeerPullPayment(
  wex: WalletExecutionContext,
  req: InitiatePeerPullCreditRequest,
): Promise<InitiatePeerPullCreditResponse> {
  const currency = Amounts.currencyOf(req.partialContractTerms.amount);
  let maybeExchangeBaseUrl: string | undefined;
  if (req.exchangeBaseUrl) {
    maybeExchangeBaseUrl = req.exchangeBaseUrl;
  } else {
    maybeExchangeBaseUrl = await getPreferredExchangeForCurrency(wex, currency);
  }

  if (!maybeExchangeBaseUrl) {
    throw Error("no exchange found for initiating a peer pull payment");
  }

  const exchangeBaseUrl = maybeExchangeBaseUrl;

  const exchange = await fetchFreshExchange(wex, exchangeBaseUrl);
  requireExchangeTosAcceptedOrThrow(exchange);

  const mergeReserveInfo = await getMergeReserveInfo(wex, {
    exchangeBaseUrl: exchangeBaseUrl,
  });

  const pursePair = await wex.cryptoApi.createEddsaKeypair({});
  const mergePair = await wex.cryptoApi.createEddsaKeypair({});

  const contractTerms = req.partialContractTerms;

  const hContractTerms = ContractTermsUtil.hashContractTerms(contractTerms);

  const contractKeyPair = await wex.cryptoApi.createEddsaKeypair({});

  const withdrawalGroupId = encodeCrock(getRandomBytes(32));

  const mergeReserveRowId = mergeReserveInfo.rowId;
  checkDbInvariant(
    !!mergeReserveRowId,
    `merge reserve for ${exchangeBaseUrl} without rowid`,
  );

  const contractEncNonce = encodeCrock(getRandomBytes(24));

  const wi = await getExchangeWithdrawalInfo(
    wex,
    exchangeBaseUrl,
    Amounts.parseOrThrow(req.partialContractTerms.amount),
    undefined,
  );
  if (wi.selectedDenoms.selectedDenoms.length === 0) {
    throw Error(
      `unable to initiate pull payment from ${exchangeBaseUrl}, can't select denominations for instructed amount (${req.partialContractTerms.amount}`,
    );
  }

  const mergeTimestamp = TalerPreciseTimestamp.now();

  const transitionInfo = await wex.db.runReadWriteTx(
    { storeNames: ["peerPullCredit", "contractTerms"] },
    async (tx) => {
      const ppi: PeerPullCreditRecord = {
        amount: req.partialContractTerms.amount,
        contractTermsHash: hContractTerms,
        exchangeBaseUrl: exchangeBaseUrl,
        pursePriv: pursePair.priv,
        pursePub: pursePair.pub,
        mergePriv: mergePair.priv,
        mergePub: mergePair.pub,
        status: PeerPullPaymentCreditStatus.PendingCreatePurse,
        mergeTimestamp: timestampPreciseToDb(mergeTimestamp),
        contractEncNonce,
        mergeReserveRowId: mergeReserveRowId,
        contractPriv: contractKeyPair.priv,
        contractPub: contractKeyPair.pub,
        withdrawalGroupId,
        estimatedAmountEffective: wi.withdrawalAmountEffective,
      };
      await tx.peerPullCredit.put(ppi);
      const oldTxState: TransactionState = {
        major: TransactionMajorState.None,
      };
      const newTxState = computePeerPullCreditTransactionState(ppi);
      await tx.contractTerms.put({
        contractTermsRaw: contractTerms,
        h: hContractTerms,
      });
      return { oldTxState, newTxState };
    },
  );

  const ctx = new PeerPullCreditTransactionContext(wex, pursePair.pub);

  notifyTransition(wex, ctx.transactionId, transitionInfo);
  wex.taskScheduler.startShepherdTask(ctx.taskId);

  // The pending-incoming balance has changed.
  wex.ws.notify({
    type: NotificationType.BalanceChange,
    hintTransactionId: ctx.transactionId,
  });

  return {
    talerUri: stringifyTalerUri({
      type: TalerUriAction.PayPull,
      exchangeBaseUrl: exchangeBaseUrl,
      contractPriv: contractKeyPair.priv,
    }),
    transactionId: ctx.transactionId,
  };
}

export function computePeerPullCreditTransactionState(
  pullCreditRecord: PeerPullCreditRecord,
): TransactionState {
  switch (pullCreditRecord.status) {
    case PeerPullPaymentCreditStatus.PendingCreatePurse:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.CreatePurse,
      };
    case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.MergeKycRequired,
      };
    case PeerPullPaymentCreditStatus.PendingReady:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.Ready,
      };
    case PeerPullPaymentCreditStatus.Done:
      return {
        major: TransactionMajorState.Done,
      };
    case PeerPullPaymentCreditStatus.PendingWithdrawing:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.Withdraw,
      };
    case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.CreatePurse,
      };
    case PeerPullPaymentCreditStatus.SuspendedReady:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.Ready,
      };
    case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.Withdraw,
      };
    case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.MergeKycRequired,
      };
    case PeerPullPaymentCreditStatus.Aborted:
      return {
        major: TransactionMajorState.Aborted,
      };
    case PeerPullPaymentCreditStatus.AbortingDeletePurse:
      return {
        major: TransactionMajorState.Aborting,
        minor: TransactionMinorState.DeletePurse,
      };
    case PeerPullPaymentCreditStatus.Failed:
      return {
        major: TransactionMajorState.Failed,
      };
    case PeerPullPaymentCreditStatus.Expired:
      return {
        major: TransactionMajorState.Expired,
      };
    case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
      return {
        major: TransactionMajorState.Aborting,
        minor: TransactionMinorState.DeletePurse,
      };
  }
}

export function computePeerPullCreditTransactionActions(
  pullCreditRecord: PeerPullCreditRecord,
): TransactionAction[] {
  switch (pullCreditRecord.status) {
    case PeerPullPaymentCreditStatus.PendingCreatePurse:
      return [
        TransactionAction.Retry,
        TransactionAction.Abort,
        TransactionAction.Suspend,
      ];
    case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
      return [
        TransactionAction.Retry,
        TransactionAction.Abort,
        TransactionAction.Suspend,
      ];
    case PeerPullPaymentCreditStatus.PendingReady:
      return [
        TransactionAction.Retry,
        TransactionAction.Abort,
        TransactionAction.Suspend,
      ];
    case PeerPullPaymentCreditStatus.Done:
      return [TransactionAction.Delete];
    case PeerPullPaymentCreditStatus.PendingWithdrawing:
      return [
        TransactionAction.Retry,
        TransactionAction.Abort,
        TransactionAction.Suspend,
      ];
    case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
      return [TransactionAction.Resume, TransactionAction.Abort];
    case PeerPullPaymentCreditStatus.SuspendedReady:
      return [TransactionAction.Abort, TransactionAction.Resume];
    case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
      return [TransactionAction.Resume, TransactionAction.Fail];
    case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
      return [TransactionAction.Resume, TransactionAction.Fail];
    case PeerPullPaymentCreditStatus.Aborted:
      return [TransactionAction.Delete];
    case PeerPullPaymentCreditStatus.AbortingDeletePurse:
      return [
        TransactionAction.Retry,
        TransactionAction.Suspend,
        TransactionAction.Fail,
      ];
    case PeerPullPaymentCreditStatus.Failed:
      return [TransactionAction.Delete];
    case PeerPullPaymentCreditStatus.Expired:
      return [TransactionAction.Delete];
    case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
      return [TransactionAction.Resume, TransactionAction.Fail];
  }
}
