/*
This file is part of GNU Taler
(C) 2019 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
*/
/**
* Type and schema definitions for the wallet's transaction list.
*
* @author Florian Dold
* @author Torsten Grote
*/
/**
* Imports.
*/
import { TalerPreciseTimestamp, TalerProtocolTimestamp } from "./time.js";
import {
AmountString,
Product,
InternationalizedString,
MerchantInfo,
codecForInternationalizedString,
codecForMerchantInfo,
codecForProduct,
Location,
} from "./taler-types.js";
import {
Codec,
buildCodecForObject,
codecOptional,
codecForString,
codecForList,
codecForAny,
codecForBoolean,
codecForEither,
codecForConstString,
} from "./codec.js";
import {
RefreshReason,
TalerErrorDetail,
TransactionIdStr,
TransactionStateFilter,
WithdrawalExchangeAccountDetails,
} from "./wallet-types.js";
export interface TransactionsRequest {
/**
* return only transactions in the given currency
*/
currency?: string;
/**
* if present, results will be limited to transactions related to the given search string
*/
search?: string;
/**
* Sort order of the transaction items.
* By default, items are sorted ascending by their
* main timestamp.
*/
sort?: "ascending" | "descending";
/**
* If true, include all refreshes in the transactions list.
*/
includeRefreshes?: boolean;
filterByState?: TransactionStateFilter;
}
export interface TransactionState {
major: TransactionMajorState;
minor?: TransactionMinorState;
}
export enum TransactionMajorState {
// No state, only used when reporting transitions into the initial state
None = "none",
Pending = "pending",
Done = "done",
Aborting = "aborting",
Aborted = "aborted",
Suspended = "suspended",
Dialog = "dialog",
SuspendedAborting = "suspended-aborting",
Failed = "failed",
Expired = "expired",
// Only used for the notification, never in the transaction history
Deleted = "deleted",
}
export enum TransactionMinorState {
// Placeholder until D37 is fully implemented
Unknown = "unknown",
Deposit = "deposit",
KycRequired = "kyc",
AmlRequired = "aml",
MergeKycRequired = "merge-kyc",
Track = "track",
SubmitPayment = "submit-payment",
RebindSession = "rebind-session",
Refresh = "refresh",
Pickup = "pickup",
AutoRefund = "auto-refund",
User = "user",
Bank = "bank",
Exchange = "exchange",
ClaimProposal = "claim-proposal",
CheckRefund = "check-refund",
CreatePurse = "create-purse",
DeletePurse = "delete-purse",
RefreshExpired = "refresh-expired",
Ready = "ready",
Merge = "merge",
Repurchase = "repurchase",
BankRegisterReserve = "bank-register-reserve",
BankConfirmTransfer = "bank-confirm-transfer",
WithdrawCoins = "withdraw-coins",
ExchangeWaitReserve = "exchange-wait-reserve",
AbortingBank = "aborting-bank",
Aborting = "aborting",
Refused = "refused",
Withdraw = "withdraw",
MerchantOrderProposed = "merchant-order-proposed",
Proposed = "proposed",
RefundAvailable = "refund-available",
AcceptRefund = "accept-refund",
}
export enum TransactionAction {
Delete = "delete",
Suspend = "suspend",
Resume = "resume",
Abort = "abort",
Fail = "fail",
Retry = "retry",
}
export interface TransactionsResponse {
// a list of past and pending transactions sorted by pending, timestamp and transactionId.
// In case two events are both pending and have the same timestamp,
// they are sorted by the transactionId
// (lexically ascending and locale-independent comparison).
transactions: Transaction[];
}
export interface TransactionCommon {
// opaque unique ID for the transaction, used as a starting point for paginating queries
// and for invoking actions on the transaction (e.g. deleting/hiding it from the history)
transactionId: TransactionIdStr;
// the type of the transaction; different types might provide additional information
type: TransactionType;
// main timestamp of the transaction
timestamp: TalerPreciseTimestamp;
/**
* Transaction state, as per DD37.
*/
txState: TransactionState;
/**
* Possible transitions based on the current state.
*/
txActions: TransactionAction[];
/**
* Raw amount of the transaction (exclusive of fees or other extra costs).
*/
amountRaw: AmountString;
/**
* Amount added or removed from the wallet's balance (including all fees and other costs).
*/
amountEffective: AmountString;
error?: TalerErrorDetail;
/**
* If the transaction minor state is in KycRequired this field is going to
* have the location where the user need to go to complete KYC information.
*/
kycUrl?: string;
}
export type Transaction =
| TransactionWithdrawal
| TransactionPayment
| TransactionRefund
| TransactionReward
| TransactionRefresh
| TransactionDeposit
| TransactionPeerPullCredit
| TransactionPeerPullDebit
| TransactionPeerPushCredit
| TransactionPeerPushDebit
| TransactionInternalWithdrawal;
export enum TransactionType {
Withdrawal = "withdrawal",
InternalWithdrawal = "internal-withdrawal",
Payment = "payment",
Refund = "refund",
Refresh = "refresh",
Reward = "reward",
Deposit = "deposit",
PeerPushDebit = "peer-push-debit",
PeerPushCredit = "peer-push-credit",
PeerPullDebit = "peer-pull-debit",
PeerPullCredit = "peer-pull-credit",
}
export enum WithdrawalType {
TalerBankIntegrationApi = "taler-bank-integration-api",
ManualTransfer = "manual-transfer",
}
export type WithdrawalDetails =
| WithdrawalDetailsForManualTransfer
| WithdrawalDetailsForTalerBankIntegrationApi;
interface WithdrawalDetailsForManualTransfer {
type: WithdrawalType.ManualTransfer;
/**
* Payto URIs that the exchange supports.
*
* Already contains the amount and message.
*
* @deprecated in favor of exchangeCreditAccounts
*/
exchangePaytoUris: string[];
exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[];
// Public key of the reserve
reservePub: string;
/**
* Is the reserve ready for withdrawal?
*/
reserveIsReady: boolean;
}
interface WithdrawalDetailsForTalerBankIntegrationApi {
type: WithdrawalType.TalerBankIntegrationApi;
/**
* Set to true if the bank has confirmed the withdrawal, false if not.
* An unconfirmed withdrawal usually requires user-input and should be highlighted in the UI.
* See also bankConfirmationUrl below.
*/
confirmed: boolean;
/**
* If the withdrawal is unconfirmed, this can include a URL for user
* initiated confirmation.
*/
bankConfirmationUrl?: string;
// Public key of the reserve
reservePub: string;
/**
* Is the reserve ready for withdrawal?
*/
reserveIsReady: boolean;
exchangeCreditAccountDetails?: WithdrawalExchangeAccountDetails[];
}
/**
* A withdrawal transaction (either bank-integrated or manual).
*/
export interface TransactionWithdrawal extends TransactionCommon {
type: TransactionType.Withdrawal;
/**
* Exchange of the withdrawal.
*/
exchangeBaseUrl: string;
/**
* Amount that got subtracted from the reserve balance.
*/
amountRaw: AmountString;
/**
* Amount that actually was (or will be) added to the wallet's balance.
*/
amountEffective: AmountString;
withdrawalDetails: WithdrawalDetails;
}
/**
* Internal withdrawal operation, only reported on request.
*
* Some transactions (peer-*-credit) internally do a withdrawal,
* but only the peer-*-credit transaction is reported.
*
* The internal withdrawal transaction allows to access the details of
* the underlying withdrawal for testing/debugging.
*
* It is usually not reported, so that amounts of transactions properly
* add up, since the amountEffecive of the withdrawal is already reported
* in the peer-*-credit transaction.
*/
export interface TransactionInternalWithdrawal extends TransactionCommon {
type: TransactionType.InternalWithdrawal;
/**
* Exchange of the withdrawal.
*/
exchangeBaseUrl: string;
/**
* Amount that got subtracted from the reserve balance.
*/
amountRaw: AmountString;
/**
* Amount that actually was (or will be) added to the wallet's balance.
*/
amountEffective: AmountString;
withdrawalDetails: WithdrawalDetails;
}
export interface PeerInfoShort {
expiration: TalerProtocolTimestamp | undefined;
summary: string | undefined;
}
/**
* Credit because we were paid for a P2P invoice we created.
*/
export interface TransactionPeerPullCredit extends TransactionCommon {
type: TransactionType.PeerPullCredit;
info: PeerInfoShort;
/**
* Exchange used.
*/
exchangeBaseUrl: string;
/**
* Amount that got subtracted from the reserve balance.
*/
amountRaw: AmountString;
/**
* Amount that actually was (or will be) added to the wallet's balance.
*/
amountEffective: AmountString;
/**
* URI to send to the other party.
*
* Only available in the right state.
*/
talerUri: string | undefined;
}
/**
* Debit because we paid someone's invoice.
*/
export interface TransactionPeerPullDebit extends TransactionCommon {
type: TransactionType.PeerPullDebit;
info: PeerInfoShort;
/**
* Exchange used.
*/
exchangeBaseUrl: string;
amountRaw: AmountString;
amountEffective: AmountString;
}
/**
* We sent money via a P2P payment.
*/
export interface TransactionPeerPushDebit extends TransactionCommon {
type: TransactionType.PeerPushDebit;
info: PeerInfoShort;
/**
* Exchange used.
*/
exchangeBaseUrl: string;
/**
* Amount that got subtracted from the reserve balance.
*/
amountRaw: AmountString;
/**
* Amount that actually was (or will be) added to the wallet's balance.
*/
amountEffective: AmountString;
/**
* URI to accept the payment.
*
* Only present if the transaction is in a state where the other party can
* accept the payment.
*/
talerUri?: string;
}
/**
* We received money via a P2P payment.
*/
export interface TransactionPeerPushCredit extends TransactionCommon {
type: TransactionType.PeerPushCredit;
info: PeerInfoShort;
/**
* Exchange used.
*/
exchangeBaseUrl: string;
/**
* Amount that got subtracted from the reserve balance.
*/
amountRaw: AmountString;
/**
* Amount that actually was (or will be) added to the wallet's balance.
*/
amountEffective: AmountString;
}
export enum PaymentStatus {
/**
* Explicitly aborted after timeout / failure
*/
Aborted = "aborted",
/**
* Payment failed, wallet will auto-retry.
* User should be given the option to retry now / abort.
*/
Failed = "failed",
/**
* Paid successfully
*/
Paid = "paid",
/**
* User accepted, payment is processing.
*/
Accepted = "accepted",
}
export interface TransactionPayment extends TransactionCommon {
type: TransactionType.Payment;
/**
* Additional information about the payment.
*/
info: OrderShortInfo;
/**
* Wallet-internal end-to-end identifier for the payment.
*/
proposalId: string;
/**
* Amount that must be paid for the contract
*/
amountRaw: AmountString;
/**
* Amount that was paid, including deposit, wire and refresh fees.
*/
amountEffective: AmountString;
/**
* Amount that has been refunded by the merchant
*/
totalRefundRaw: AmountString;
/**
* Amount will be added to the wallet's balance after fees and refreshing
*/
totalRefundEffective: AmountString;
/**
* Amount pending to be picked up
*/
refundPending: AmountString | undefined;
/**
* Reference to applied refunds
*/
refunds: RefundInfoShort[];
/**
* Is the wallet currently checking for a refund?
*/
refundQueryActive: boolean;
/**
* Does this purchase has an pos validation
*/
posConfirmation: string | undefined;
}
export interface OrderShortInfo {
/**
* Order ID, uniquely identifies the order within a merchant instance
*/
orderId: string;
/**
* Hash of the contract terms.
*/
contractTermsHash: string;
/**
* More information about the merchant
*/
merchant: MerchantInfo;
/**
* Summary of the order, given by the merchant
*/
summary: string;
/**
* Map from IETF BCP 47 language tags to localized summaries
*/
summary_i18n?: InternationalizedString;
/**
* URL of the fulfillment, given by the merchant
*/
fulfillmentUrl?: string;
/**
* Plain text message that should be shown to the user
* when the payment is complete.
*/
fulfillmentMessage?: string;
/**
* Translations of fulfillmentMessage.
*/
fulfillmentMessage_i18n?: InternationalizedString;
}
export interface RefundInfoShort {
transactionId: string;
timestamp: TalerProtocolTimestamp;
amountEffective: AmountString;
amountRaw: AmountString;
}
/**
* Summary information about the payment that we got a refund for.
*/
export interface RefundPaymentInfo {
summary: string;
summary_i18n?: InternationalizedString;
/**
* More information about the merchant
*/
merchant: MerchantInfo;
}
export interface TransactionRefund extends TransactionCommon {
type: TransactionType.Refund;
// Amount that has been refunded by the merchant
amountRaw: AmountString;
// Amount will be added to the wallet's balance after fees and refreshing
amountEffective: AmountString;
// ID for the transaction that is refunded
refundedTransactionId: string;
paymentInfo: RefundPaymentInfo | undefined;
}
export interface TransactionReward extends TransactionCommon {
type: TransactionType.Reward;
// Raw amount of the tip, without extra fees that apply
amountRaw: AmountString;
/**
* More information about the merchant
*/
// merchant: MerchantInfo;
// Amount will be (or was) added to the wallet's balance after fees and refreshing
amountEffective: AmountString;
merchantBaseUrl: string;
}
/**
* A transaction shown for refreshes.
* Only shown for (1) refreshes not associated with other transactions
* and (2) refreshes in an error state.
*/
export interface TransactionRefresh extends TransactionCommon {
type: TransactionType.Refresh;
refreshReason: RefreshReason;
/**
* Transaction ID that caused this refresh.
*/
originatingTransactionId?: string;
/**
* Always zero for refreshes
*/
amountRaw: AmountString;
/**
* Fees, i.e. the effective, negative effect of the refresh
* on the balance.
*
* Only applicable for stand-alone refreshes, and zero for
* other refreshes where the transaction itself accounts for the
* refresh fee.
*/
amountEffective: AmountString;
refreshInputAmount: AmountString;
refreshOutputAmount: AmountString;
}
export interface DepositTransactionTrackingState {
// Raw wire transfer identifier of the deposit.
wireTransferId: string;
// When was the wire transfer given to the bank.
timestampExecuted: TalerProtocolTimestamp;
// Total amount transfer for this wtid (including fees)
amountRaw: AmountString;
// Wire fee amount for this exchange
wireFee: AmountString;
}
/**
* Deposit transaction, which effectively sends
* money from this wallet somewhere else.
*/
export interface TransactionDeposit extends TransactionCommon {
type: TransactionType.Deposit;
depositGroupId: string;
/**
* Target for the deposit.
*/
targetPaytoUri: string;
/**
* Raw amount that is being deposited
*/
amountRaw: AmountString;
/**
* Effective amount that is being deposited
*/
amountEffective: AmountString;
wireTransferDeadline: TalerProtocolTimestamp;
wireTransferProgress: number;
/**
* Did all the deposit requests succeed?
*/
deposited: boolean;
trackingState: Array;
}
export interface TransactionByIdRequest {
transactionId: string;
}
export const codecForTransactionByIdRequest =
(): Codec =>
buildCodecForObject()
.property("transactionId", codecForString())
.build("TransactionByIdRequest");
export const codecForTransactionsRequest = (): Codec =>
buildCodecForObject()
.property("currency", codecOptional(codecForString()))
.property("search", codecOptional(codecForString()))
.property(
"sort",
codecOptional(
codecForEither(
codecForConstString("ascending"),
codecForConstString("descending"),
),
),
)
.property("includeRefreshes", codecOptional(codecForBoolean()))
.build("TransactionsRequest");
// FIXME: do full validation here!
export const codecForTransactionsResponse = (): Codec =>
buildCodecForObject()
.property("transactions", codecForList(codecForAny()))
.build("TransactionsResponse");
export const codecForOrderShortInfo = (): Codec =>
buildCodecForObject()
.property("contractTermsHash", codecForString())
.property("fulfillmentMessage", codecOptional(codecForString()))
.property(
"fulfillmentMessage_i18n",
codecOptional(codecForInternationalizedString()),
)
.property("fulfillmentUrl", codecOptional(codecForString()))
.property("merchant", codecForMerchantInfo())
.property("orderId", codecForString())
.property("summary", codecForString())
.property("summary_i18n", codecOptional(codecForInternationalizedString()))
.build("OrderShortInfo");