/*
 This file is part of GNU Taler
 (C) 2022-2025 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 {
  Amounts,
  CheckPeerPullCreditRequest,
  CheckPeerPullCreditResponse,
  ContractTermsUtil,
  ExchangeReservePurseRequest,
  ExchangeWalletKycStatus,
  HostPortPath,
  HttpStatusCode,
  InitiatePeerPullCreditRequest,
  InitiatePeerPullCreditResponse,
  Logger,
  NotificationType,
  PeerContractTerms,
  ScopeInfo,
  ScopeType,
  TalerErrorCode,
  TalerErrorDetail,
  TalerPreciseTimestamp,
  TalerUriAction,
  Transaction,
  TransactionAction,
  TransactionIdStr,
  TransactionMajorState,
  TransactionMinorState,
  TransactionState,
  TransactionType,
  WalletAccountMergeFlags,
  WalletNotification,
  assertUnreachable,
  checkDbInvariant,
  checkProtocolInvariant,
  encodeCrock,
  getRandomBytes,
  j2s,
  stringifyPayPullUri,
  stringifyTalerUri,
  talerPaytoFromExchangeReserve,
} from "@gnu-taler/taler-util";
import {
  PendingTaskType,
  TaskIdStr,
  TaskIdentifiers,
  TaskRunResult,
  TransactionContext,
  TransitionResultType,
  constructTaskIdentifier,
  genericWaitForStateVal,
  requireExchangeTosAcceptedOrThrow,
  runWithClientCancellation,
} from "./common.js";
import {
  OperationRetryRecord,
  PeerPullCreditRecord,
  PeerPullPaymentCreditStatus,
  WalletDbAllStoresReadOnlyTransaction,
  WalletDbReadWriteTransaction,
  WithdrawalGroupRecord,
  WithdrawalGroupStatus,
  WithdrawalRecordType,
  timestampPreciseFromDb,
  timestampPreciseToDb,
} from "./db.js";
import {
  BalanceThresholdCheckResult,
  checkIncomingAmountLegalUnderKycBalanceThreshold,
  fetchFreshExchange,
  getPreferredExchangeForCurrency,
  getScopeForAllExchanges,
  handleStartExchangeWalletKyc,
} from "./exchanges.js";
import {
  GenericKycStatusReq,
  checkPeerCreditHardLimitExceeded,
  isKycOperationDue,
  runKycCheckAlgo,
} from "./kyc.js";
import {
  getMergeReserveInfo,
  isPurseDeposited,
  recordCreate,
  recordDelete,
  recordTransition,
  recordTransitionStatus,
  recordUpdateMeta,
} from "./pay-peer-common.js";
import {
  BalanceEffect,
  applyNotifyBalanceEffect,
  constructTransactionIdentifier,
  isUnsuccessfulTransaction,
} from "./transactions.js";
import { WalletExecutionContext, walletExchangeClient } from "./wallet.js";
import {
  WithdrawTransactionContext,
  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,
    });
  }

  readonly store = "peerPullCredit";
  readonly recordId = this.pursePub;
  readonly recordState = (rec: PeerPullCreditRecord) => ({
    txState: computePeerPullCreditTransactionState(rec),
    stId: rec.status,
  });
  readonly recordMeta = (rec: PeerPullCreditRecord) => ({
    transactionId: this.transactionId,
    status: rec.status,
    timestamp: rec.mergeTimestamp,
    currency: Amounts.currencyOf(rec.amount),
    exchanges: [rec.exchangeBaseUrl],
  });
  updateTransactionMeta = (
    tx: WalletDbReadWriteTransaction<["peerPullCredit", "transactionsMeta"]>,
  ) => recordUpdateMeta(this, tx);

  async deleteTransactionInTx(
    tx: WalletDbReadWriteTransaction<
      [
        "withdrawalGroups",
        "peerPullCredit",
        "planchets",
        "tombstones",
        "transactionsMeta",
      ]
    >,
  ): Promise<{ notifs: WalletNotification[] }> {
    return recordDelete(this, tx, async (rec, notifs) => {
      if (rec.withdrawalGroupId) {
        const withdrawalGroupId = rec.withdrawalGroupId;
        const withdrawalCtx = new WithdrawTransactionContext(
          this.wex,
          withdrawalGroupId,
        );
        await withdrawalCtx.deleteTransactionInTx(tx);
      }
    });
  }

  /**
   * 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);

    let kycUrl: string | undefined = undefined;
    if (pullCredit.kycAccessToken) {
      kycUrl = new URL(
        `kyc-spa/${pullCredit.kycAccessToken}`,
        pullCredit.exchangeBaseUrl,
      ).href;
    }

    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,
        stId: pullCredit.status,
        scopes: await getScopeForAllExchanges(tx, [pullCredit.exchangeBaseUrl]),
        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,
          iconId: peerContractTerms.icon_id,
        },
        talerUri: stringifyPayPullUri({
          exchangeBaseUrl: wsr.exchangeBaseUrl as HostPortPath, // FIXME: change record type
          contractPriv: wsr.wgInfo.contractPriv,
        }),
        transactionId: this.transactionId,
        abortReason: pullCredit.abortReason,
        failReason: pullCredit.failReason,
        // FIXME: Is this the KYC URL of the withdrawal group?!
        kycUrl: kycUrl,
        ...(wsrOrt?.lastError
          ? {
              error: silentWithdrawalErrorForInvoice
                ? undefined
                : wsrOrt.lastError,
            }
          : {}),
      };
    }

    const txState = computePeerPullCreditTransactionState(pullCredit);
    return {
      type: TransactionType.PeerPullCredit,
      txState,
      stId: pullCredit.status,
      scopes: await getScopeForAllExchanges(tx, [pullCredit.exchangeBaseUrl]),
      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,
        iconId: peerContractTerms.icon_id,
      },
      talerUri: stringifyPayPullUri({
        exchangeBaseUrl: pullCredit.exchangeBaseUrl as HostPortPath, // FIXME: change record type
        contractPriv: pullCredit.contractPriv,
      }),
      transactionId: this.transactionId,
      kycUrl,
      kycAccessToken: pullCredit.kycAccessToken,
      kycPaytoHash: pullCredit.kycPaytoHash,
      abortReason: pullCredit.abortReason,
      failReason: pullCredit.failReason,
      ...(pullCreditOrt?.lastError ? { error: pullCreditOrt.lastError } : {}),
    };
  }

  async deleteTransaction(): Promise<void> {
    const res = await this.wex.db.runReadWriteTx(
      {
        storeNames: [
          "withdrawalGroups",
          "peerPullCredit",
          "planchets",
          "tombstones",
          "transactionsMeta",
        ],
      },
      this.deleteTransactionInTx.bind(this),
    );
    for (const notif of res.notifs) {
      this.wex.ws.notify(notif);
    }
  }

  async suspendTransaction(): Promise<void> {
    await recordTransition(this, {}, async (rec) => {
      switch (rec.status) {
        case PeerPullPaymentCreditStatus.PendingCreatePurse:
          rec.status = PeerPullPaymentCreditStatus.SuspendedCreatePurse;
          return TransitionResultType.Transition;
        case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
          rec.status = PeerPullPaymentCreditStatus.SuspendedMergeKycRequired;
          return TransitionResultType.Transition;
        case PeerPullPaymentCreditStatus.PendingWithdrawing:
          rec.status = PeerPullPaymentCreditStatus.SuspendedWithdrawing;
          return TransitionResultType.Transition;
        case PeerPullPaymentCreditStatus.PendingReady:
          rec.status = PeerPullPaymentCreditStatus.SuspendedReady;
          return TransitionResultType.Transition;
        case PeerPullPaymentCreditStatus.AbortingDeletePurse:
          rec.status = PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse;
          return TransitionResultType.Transition;
        case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
          rec.status = PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired;
          return TransitionResultType.Transition;
        case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
          rec.status = PeerPullPaymentCreditStatus.SuspendedBalanceKycInit;
          return TransitionResultType.Transition;
        case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
        case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
        case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
        case PeerPullPaymentCreditStatus.SuspendedReady:
        case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
        case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
        case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
        case PeerPullPaymentCreditStatus.Done:
        case PeerPullPaymentCreditStatus.Aborted:
        case PeerPullPaymentCreditStatus.Failed:
        case PeerPullPaymentCreditStatus.Expired:
          return TransitionResultType.Stay;
        default:
          assertUnreachable(rec.status);
      }
    });
    this.wex.taskScheduler.stopShepherdTask(this.taskId);
  }

  async failTransaction(reason?: TalerErrorDetail): Promise<void> {
    await recordTransition(this, {}, async (rec) => {
      switch (rec.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:
        case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
        case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
        case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
        case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
          return TransitionResultType.Stay;
        case PeerPullPaymentCreditStatus.AbortingDeletePurse:
        case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
          rec.status = PeerPullPaymentCreditStatus.Failed;
          rec.failReason = reason;
          return TransitionResultType.Transition;
        default:
          assertUnreachable(rec.status);
      }
    });
    this.wex.taskScheduler.stopShepherdTask(this.taskId);
  }

  async resumeTransaction(): Promise<void> {
    await recordTransition(this, {}, async (rec) => {
      switch (rec.status) {
        case PeerPullPaymentCreditStatus.PendingCreatePurse:
        case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
        case PeerPullPaymentCreditStatus.PendingWithdrawing:
        case PeerPullPaymentCreditStatus.PendingReady:
        case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
        case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
        case PeerPullPaymentCreditStatus.AbortingDeletePurse:
        case PeerPullPaymentCreditStatus.Done:
        case PeerPullPaymentCreditStatus.Failed:
        case PeerPullPaymentCreditStatus.Expired:
        case PeerPullPaymentCreditStatus.Aborted:
          return TransitionResultType.Stay;
        case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
          rec.status = PeerPullPaymentCreditStatus.PendingBalanceKycInit;
          return TransitionResultType.Transition;
        case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
          rec.status = PeerPullPaymentCreditStatus.PendingCreatePurse;
          return TransitionResultType.Transition;
        case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
          rec.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
          return TransitionResultType.Transition;
        case PeerPullPaymentCreditStatus.SuspendedReady:
          rec.status = PeerPullPaymentCreditStatus.PendingReady;
          return TransitionResultType.Transition;
        case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
          rec.status = PeerPullPaymentCreditStatus.PendingWithdrawing;
          return TransitionResultType.Transition;
        case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
          rec.status = PeerPullPaymentCreditStatus.AbortingDeletePurse;
          return TransitionResultType.Transition;
        case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
          rec.status = PeerPullPaymentCreditStatus.PendingBalanceKycRequired;
          return TransitionResultType.Transition;
        default:
          assertUnreachable(rec.status);
      }
    });
    this.wex.taskScheduler.startShepherdTask(this.taskId);
  }

  async abortTransaction(reason?: TalerErrorDetail): Promise<void> {
    await recordTransition(this, {}, async (rec) => {
      switch (rec.status) {
        case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
        case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
        case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
        case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
        case PeerPullPaymentCreditStatus.PendingCreatePurse:
        case PeerPullPaymentCreditStatus.PendingMergeKycRequired:
          rec.status = PeerPullPaymentCreditStatus.AbortingDeletePurse;
          rec.abortReason = reason;
          return TransitionResultType.Transition;
        case PeerPullPaymentCreditStatus.PendingWithdrawing:
          throw Error("can't abort anymore");
        case PeerPullPaymentCreditStatus.PendingReady:
          rec.abortReason = reason;
          rec.status = PeerPullPaymentCreditStatus.AbortingDeletePurse;
          return TransitionResultType.Transition;
        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:
          return TransitionResultType.Stay;
        default:
          assertUnreachable(rec.status);
      }
    });
    this.wex.taskScheduler.stopShepherdTask(this.taskId);
    this.wex.taskScheduler.startShepherdTask(this.taskId);
  }
}

async function queryPurseForPeerPullCredit(
  wex: WalletExecutionContext,
  pullIni: PeerPullCreditRecord,
): Promise<TaskRunResult> {
  const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub);
  const exchangeClient = walletExchangeClient(pullIni.exchangeBaseUrl, wex);
  const resp = await exchangeClient.getPurseStatusAtDeposit(
    pullIni.pursePub,
    true,
  );

  switch (resp.case) {
    case "ok":
      break;
    case HttpStatusCode.Gone:
      // Exchange says that purse doesn't exist anymore => expired!
      await recordTransitionStatus(
        ctx,
        PeerPullPaymentCreditStatus.PendingReady,
        PeerPullPaymentCreditStatus.Expired,
      );
      return TaskRunResult.finished();
    case HttpStatusCode.NotFound:
      await ctx.failTransaction(resp.detail);
      return TaskRunResult.finished();
    default:
      assertUnreachable(resp);
  }

  if (!isPurseDeposited(resp.body)) {
    logger.info("purse not ready yet (no deposit)");
    return TaskRunResult.longpollReturnedPending();
  }

  const reserve = await wex.db.runReadOnlyTx(
    { storeNames: ["reserves"] },
    (tx) => 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,
    },
  });
  await recordTransitionStatus(
    ctx,
    PeerPullPaymentCreditStatus.PendingReady,
    PeerPullPaymentCreditStatus.PendingWithdrawing,
  );
  return TaskRunResult.progress();
}

async function processPendingMergeKycRequired(
  wex: WalletExecutionContext,
  pullIni: PeerPullCreditRecord,
): Promise<TaskRunResult> {
  const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub);
  const { exchangeBaseUrl, kycPaytoHash } = pullIni;

  // FIXME: What if this changes? Should be part of the p2p record
  const mergeReserveInfo = await getMergeReserveInfo(wex, {
    exchangeBaseUrl,
  });

  const accountPub = mergeReserveInfo.reservePub;
  const accountPriv = mergeReserveInfo.reservePriv;

  let myKycState: GenericKycStatusReq | undefined;

  if (kycPaytoHash) {
    myKycState = {
      accountPriv,
      accountPub,
      // FIXME: Is this the correct amount?
      amount: pullIni.estimatedAmountEffective,
      exchangeBaseUrl,
      operation: "MERGE",
      paytoHash: kycPaytoHash,
      lastAmlReview: pullIni.kycLastAmlReview,
      lastCheckCode: pullIni.kycLastCheckCode,
      lastCheckStatus: pullIni.kycLastCheckStatus,
      lastDeny: pullIni.kycLastDeny,
      lastRuleGen: pullIni.kycLastRuleGen,
      haveAccessToken: pullIni.kycAccessToken != null,
    };
  }

  if (myKycState == null || isKycOperationDue(myKycState)) {
    return processPeerPullCreditCreatePurse(wex, pullIni);
  }

  const algoRes = await runKycCheckAlgo(wex, myKycState);

  if (!algoRes.updatedStatus) {
    return algoRes.taskResult;
  }

  const updatedStatus = algoRes.updatedStatus;

  checkProtocolInvariant(algoRes.requiresAuth != true);

  recordTransition(ctx, {}, async (rec) => {
    rec.kycLastAmlReview = updatedStatus.lastAmlReview;
    rec.kycLastCheckStatus = updatedStatus.lastCheckStatus;
    rec.kycLastCheckCode = updatedStatus.lastCheckCode;
    rec.kycLastDeny = updatedStatus.lastDeny;
    rec.kycLastRuleGen = updatedStatus.lastRuleGen;
    rec.kycAccessToken = updatedStatus.accessToken;
    return TransitionResultType.Transition;
  });
  return algoRes.taskResult;
}

async function processPeerPullCreditAbortingDeletePurse(
  wex: WalletExecutionContext,
  peerPullIni: PeerPullCreditRecord,
): Promise<TaskRunResult> {
  const { pursePub, pursePriv } = peerPullIni;
  const ctx = new PeerPullCreditTransactionContext(wex, peerPullIni.pursePub);
  const exchangeClient = walletExchangeClient(peerPullIni.exchangeBaseUrl, wex);
  const sigResp = await wex.cryptoApi.signDeletePurse({
    pursePriv,
  });
  const resp = await exchangeClient.deletePurse(pursePub, sigResp.sig);
  switch (resp.case) {
    case "ok":
    case HttpStatusCode.NotFound:
      await recordTransitionStatus(
        ctx,
        PeerPullPaymentCreditStatus.AbortingDeletePurse,
        PeerPullPaymentCreditStatus.Aborted,
      );
      return TaskRunResult.finished();
    case HttpStatusCode.Forbidden:
      await ctx.failTransaction(resp.detail);
      return TaskRunResult.finished();
    case HttpStatusCode.Conflict:
      // FIXME check if done ?
      throw Error(`cannot be deleted`);
    default:
      assertUnreachable(resp);
  }
}

async function processPeerPullCreditWithdrawing(
  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 ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub);
  const wgId = pullIni.withdrawalGroupId;
  let newTxState: TransactionState | undefined;
  await recordTransition(
    ctx,
    {
      extraStores: ["withdrawalGroups"],
    },
    async (rec, tx) => {
      if (rec.status !== PeerPullPaymentCreditStatus.PendingWithdrawing) {
        return TransitionResultType.Stay;
      }
      const wg = await tx.withdrawalGroups.get(wgId);
      if (!wg) {
        // FIXME: Fail the operation instead?
        return TransitionResultType.Stay;
      }
      switch (wg.status) {
        case WithdrawalGroupStatus.Done:
          rec.status = PeerPullPaymentCreditStatus.Done;
          break;
        // FIXME: Also handle other final states!
      }
      newTxState = computePeerPullCreditTransactionState(rec);
      return TransitionResultType.Transition;
    },
  );
  if (newTxState && newTxState.major != TransactionMajorState.Pending) {
    return TaskRunResult.finished();
  } else {
    // FIXME: Return indicator that we depend on the other operation!
    return TaskRunResult.backoff();
  }
}

async function processPeerPullCreditCreatePurse(
  wex: WalletExecutionContext,
  pullIni: PeerPullCreditRecord,
): Promise<TaskRunResult> {
  const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub);

  const kycCheckRes = await checkIncomingAmountLegalUnderKycBalanceThreshold(
    wex,
    pullIni.exchangeBaseUrl,
    pullIni.estimatedAmountEffective,
  );

  if (kycCheckRes.result === "violation") {
    // Do this before we transition so that the exchange is already in the right state.
    await handleStartExchangeWalletKyc(wex, {
      amount: kycCheckRes.nextThreshold,
      exchangeBaseUrl: pullIni.exchangeBaseUrl,
    });
    await recordTransitionStatus(
      ctx,
      PeerPullPaymentCreditStatus.PendingCreatePurse,
      PeerPullPaymentCreditStatus.PendingBalanceKycInit,
    );
    return TaskRunResult.progress();
  }

  const purseFee = Amounts.stringify(Amounts.zeroOfAmount(pullIni.amount));

  const mergeReserve = await wex.db.runReadOnlyTx(
    { storeNames: ["reserves"] },
    async (tx) => 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) => 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,
  };

  const exchangeClient = walletExchangeClient(pullIni.exchangeBaseUrl, wex);

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

  const resp = await exchangeClient.createPurseFromReserve(
    mergeReserve.reservePub,
    reservePurseReqBody,
  );

  switch (resp.case) {
    case "ok":
      break;
    case HttpStatusCode.UnavailableForLegalReasons: {
      logger.info(`kyc uuid response: ${j2s(resp.body)}`);
      return handlePeerPullCreditKycRequired(wex, pullIni, resp.body.h_payto);
    }
    case HttpStatusCode.Forbidden:
    case HttpStatusCode.NotFound:
      await ctx.failTransaction(resp.detail);
      return TaskRunResult.finished();
    case HttpStatusCode.Conflict:
      await ctx.failTransaction({ code: resp.body.code });
      return TaskRunResult.finished();
    case HttpStatusCode.PaymentRequired:
      throw Error(`unexpected reserve merge response ${resp.case}`);
    default:
      assertUnreachable(resp);
  }

  await recordTransition(ctx, {}, async (rec, _) => {
    rec.status = PeerPullPaymentCreditStatus.PendingReady;
    return TransitionResultType.Transition;
  });
  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) => tx.peerPullCredit.get(pursePub),
  );
  if (!pullIni) {
    throw Error("peer pull payment initiation not found in database");
  }

  const ctx = new PeerPullCreditTransactionContext(wex, pullIni.pursePub);
  logger.trace(`processing ${ctx.taskId}, status=${pullIni.status}`);

  switch (pullIni.status) {
    case PeerPullPaymentCreditStatus.Done:
      return TaskRunResult.finished();
    case PeerPullPaymentCreditStatus.PendingReady:
      return await queryPurseForPeerPullCredit(wex, pullIni);
    case PeerPullPaymentCreditStatus.PendingMergeKycRequired: {
      if (!pullIni.kycPaytoHash) {
        throw Error("invalid state, kycPaytoHash required");
      }
      return await processPendingMergeKycRequired(wex, pullIni);
    }
    case PeerPullPaymentCreditStatus.PendingCreatePurse:
      return await processPeerPullCreditCreatePurse(wex, pullIni);
    case PeerPullPaymentCreditStatus.AbortingDeletePurse:
      return await processPeerPullCreditAbortingDeletePurse(wex, pullIni);
    case PeerPullPaymentCreditStatus.PendingWithdrawing:
      return await processPeerPullCreditWithdrawing(wex, pullIni);
    case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
    case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
      return await processPeerPullCreditBalanceKyc(ctx, pullIni);
    case PeerPullPaymentCreditStatus.Aborted:
    case PeerPullPaymentCreditStatus.Failed:
    case PeerPullPaymentCreditStatus.Expired:
    case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
    case PeerPullPaymentCreditStatus.SuspendedAbortingDeletePurse:
    case PeerPullPaymentCreditStatus.SuspendedCreatePurse:
    case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
    case PeerPullPaymentCreditStatus.SuspendedReady:
    case PeerPullPaymentCreditStatus.SuspendedWithdrawing:
    case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
      return TaskRunResult.finished();
    default:
      assertUnreachable(pullIni.status);
  }
}

async function processPeerPullCreditBalanceKyc(
  ctx: PeerPullCreditTransactionContext,
  peerInc: PeerPullCreditRecord,
): Promise<TaskRunResult> {
  const exchangeBaseUrl = peerInc.exchangeBaseUrl;
  const amount = peerInc.estimatedAmountEffective;

  const ret = await genericWaitForStateVal(ctx.wex, {
    async checkState(): Promise<BalanceThresholdCheckResult | undefined> {
      const checkRes = await checkIncomingAmountLegalUnderKycBalanceThreshold(
        ctx.wex,
        exchangeBaseUrl,
        amount,
      );
      logger.info(
        `balance check result for ${exchangeBaseUrl} +${amount}: ${j2s(
          checkRes,
        )}`,
      );
      if (checkRes.result === "ok") {
        return checkRes;
      } else if (
        peerInc.status === PeerPullPaymentCreditStatus.PendingBalanceKycInit &&
        checkRes.walletKycStatus === ExchangeWalletKycStatus.Legi
      ) {
        return checkRes;
      }
      await handleStartExchangeWalletKyc(ctx.wex, {
        amount: checkRes.nextThreshold,
        exchangeBaseUrl,
      });
      return undefined;
    },
    filterNotification: (notif) =>
      (notif.type === NotificationType.ExchangeStateTransition &&
        notif.exchangeBaseUrl === exchangeBaseUrl) ||
      notif.type === NotificationType.BalanceChange,
  });

  if (ret.result === "ok") {
    await recordTransitionStatus(
      ctx,
      PeerPullPaymentCreditStatus.PendingBalanceKycRequired,
      PeerPullPaymentCreditStatus.PendingCreatePurse,
    );
    return TaskRunResult.progress();
  } else if (
    peerInc.status === PeerPullPaymentCreditStatus.PendingBalanceKycInit &&
    ret.walletKycStatus === ExchangeWalletKycStatus.Legi
  ) {
    await recordTransition(ctx, {}, async (rec) => {
      if (rec.status !== PeerPullPaymentCreditStatus.PendingBalanceKycInit) {
        return TransitionResultType.Stay;
      }
      rec.status = PeerPullPaymentCreditStatus.PendingBalanceKycRequired;
      rec.kycAccessToken = ret.walletKycAccessToken;
      return TransitionResultType.Transition;
    });
    return TaskRunResult.progress();
  } else {
    throw Error("not reached");
  }
}

async function handlePeerPullCreditKycRequired(
  wex: WalletExecutionContext,
  peerIni: PeerPullCreditRecord,
  kycPaytoHash: string,
): Promise<TaskRunResult> {
  const ctx = new PeerPullCreditTransactionContext(wex, peerIni.pursePub);

  await recordTransition(ctx, {}, async (rec, tx) => {
    logger.info(`setting peer-pull-credit kyc payto hash to ${kycPaytoHash}`);
    rec.kycLastDeny = timestampPreciseToDb(TalerPreciseTimestamp.now());
    rec.kycPaytoHash = kycPaytoHash;
    rec.status = PeerPullPaymentCreditStatus.PendingMergeKycRequired;
    applyNotifyBalanceEffect(tx.notify, ctx.transactionId, BalanceEffect.Flags);
    return TransitionResultType.Transition;
  });
  return TaskRunResult.progress();
}

/**
 * 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

  const instructedAmount = Amounts.parseOrThrow(req.amount);
  const currency = instructedAmount.currency;

  logger.trace(
    `checking peer push debit for ${Amounts.stringify(instructedAmount)}`,
  );

  // FIXME: Create helper to avoid code duplication with pull credit initiation

  let restrictScope: ScopeInfo;
  if (req.restrictScope) {
    restrictScope = req.restrictScope;
  } else if (req.exchangeBaseUrl) {
    restrictScope = {
      type: ScopeType.Exchange,
      currency,
      url: req.exchangeBaseUrl,
    };
  } else {
    throw Error("client must either specify exchangeBaseUrl or restrictScope");
  }

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

  const exchangeUrl = await getPreferredExchangeForCurrency(
    wex,
    restrictScope.currency,
    restrictScope,
  );

  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),
    WithdrawalRecordType.PeerPullCredit,
    undefined,
  );

  if (wi.selectedDenoms.selectedDenoms.length === 0) {
    logger.warn(`Amount for peer-pull-credit payment too low`);
  }

  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,
  };
}

/**
 * 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(wex, exchange);

  if (
    checkPeerCreditHardLimitExceeded(exchange, req.partialContractTerms.amount)
  ) {
    throw Error("peer credit would exceed hard KYC limit");
  }

  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),
    WithdrawalRecordType.PeerPullCredit,
    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 ctx = new PeerPullCreditTransactionContext(wex, pursePair.pub);
  await recordCreate(
    ctx,
    {
      extraStores: ["contractTerms"],
      label: "create-transaction-peer-pull-credit",
    },
    async (tx) => {
      await tx.contractTerms.put({
        contractTermsRaw: contractTerms,
        h: hContractTerms,
      });
      return {
        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,
      };
    },
  );
  wex.taskScheduler.startShepherdTask(ctx.taskId);

  return {
    talerUri: stringifyTalerUri({
      type: TalerUriAction.PayPull,
      exchangeBaseUrl: exchangeBaseUrl as HostPortPath, // FIXME: change record type,
      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:
      if (pullCreditRecord.kycAccessToken != null) {
        return {
          major: TransactionMajorState.Pending,
          minor: TransactionMinorState.MergeKycRequired,
        };
      } else {
        return {
          major: TransactionMajorState.Pending,
          minor: TransactionMinorState.KycInit,
        };
      }
    case PeerPullPaymentCreditStatus.SuspendedMergeKycRequired:
      if (pullCreditRecord.kycAccessToken != null) {
        return {
          major: TransactionMajorState.Suspended,
          minor: TransactionMinorState.MergeKycRequired,
        };
      } else {
        return {
          major: TransactionMajorState.Suspended,
          minor: TransactionMinorState.KycInit,
        };
      }
    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.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,
      };
    case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.BalanceKycRequired,
      };
    case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.BalanceKycRequired,
      };
    case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
      return {
        major: TransactionMajorState.Pending,
        minor: TransactionMinorState.BalanceKycInit,
      };
    case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
      return {
        major: TransactionMajorState.Suspended,
        minor: TransactionMinorState.BalanceKycInit,
      };
  }
}

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];
    case PeerPullPaymentCreditStatus.PendingBalanceKycRequired:
      return [TransactionAction.Suspend, TransactionAction.Abort];
    case PeerPullPaymentCreditStatus.SuspendedBalanceKycRequired:
      return [TransactionAction.Resume, TransactionAction.Abort];
    case PeerPullPaymentCreditStatus.PendingBalanceKycInit:
      return [TransactionAction.Suspend, TransactionAction.Abort];
    case PeerPullPaymentCreditStatus.SuspendedBalanceKycInit:
      return [TransactionAction.Resume, TransactionAction.Abort];
  }
}
