Designing a Digital Wallet

Designing a Digital Wallet

Introduction

In this article, we will see how to design the backend of a real-world application, a digital wallet. This design will lay the cornerstone of a series of articles where we will explore different concepts, techniques, patterns, and technologies related to the development of complex applications. This includes (but is not limited to) Domain-Driven Design, Event-Driven Architectures, Hexagonal Architectures, and more.

First things first. Every great project needs a great name. “Naming provides a hook, of which we start to hang an entire web of meaning. And without meaning, we can’t work.” said someone on the internet. So our digital wallet name will be (drumroll)... Walletera!

Cool, our project has a name. Now let’s see what you will find in this article.

We will start defining from a business perspective what a Digital Wallet is. Then we will focus on two specific requirements, deposits and withdrawals using a fictitious payments service provider called DinoPay. These requirements will be the basis for our design and development.

After having the requirements in place the next step will be modeling the domain. This is the first stop in our Domain-Driven Design journey. It is the part where the developers join with the stakeholders to build a model, or set of models, of the domain. For this stage, we are going to use Event Storming (we will see later what this is about). Following this practice, we will identify the domain events along with the commands that triggered those events and the aggregates that these commands modify. As part of the domain modeling, we will also identify the Bounded Contexts and the relationships that exist between them.

Once we have the domain model written down we will use it to decide what architectural style suits better our use case. Should we go with microservices or should we choose a modular monolith? Pair with me along this article and you will find out.

Finally, we will dig a bit into one of the microservices and propose a high-level design for it. In this design, we will outline the main components of the microservice and we will propose that the communication between them be event-based.

Fasten your seatbelt, our journey begins!

What is a Digital Wallet?

From investopedia.com

A digital wallet (or electronic wallet) is a financial transaction application that runs on any connected device. It securely stores your payment information and passwords in the cloud. Digital wallets may be accessible from a computer; mobile wallets, which are a subset, are primarily used on mobile devices.

Digital wallets allow you to pay when you're shopping using your device so that you don't need to carry your cards around. You enter and store your credit card, debit card, or bank account information and can then use your device to pay for purchases.

That's the basic idea, we use digital wallets to pay (or receive payments) for services and goods that we buy (or sell).

To be able to pay using your wallet you can either store your card details in the wallet (in which case the wallet will create a charge on your card whenever you perform a purchase) or you can fund the wallet by transferring money from an external source and then use those funds.

If you use your wallet to receive payments, you will be interested in the possibility of extracting the money you have in the wallet to external sources, like other wallets or bank accounts.

The external sources used to receive or send money are usually known as Payment Service Providers (PSPs from now on). Examples of payment providers are Wise, Paypal, or MecardoPago. The digital wallets interact with these PSPs through the APIs they provide.

Even though modern digital wallets may provide several features, for the purpose of this series of articles we are going to focus on the ones mentioned above, the ability to receive or send money to and from external sources.

Requirements

As we mentioned before we will focus on two features, the ability to receive money from an external payment provider and the ability to send money to an external payment provider. We will call these features deposits and withdrawals respectively.

For the moment (and for the scope of these requirements) we are only interested in deposits and withdrawals that can be made using the API of a specific payment service provider called DinoPay.

A fictitious Payment Service Provider: DinoPay

For the purpose of this series of blog posts, we will create a fictitious PSP called DinoPay. The DinoPay’s API will offer one endpoint for creating payments and another endpoint that will allow clients to subscribe to notifications of new payments created or for updates of previously created payments. The notifications will be sent via webhooks.
The details of the API endpoints can be found in the Appendix A, at the bottom of this article.

DinoPay Deposits

Context

Walletera users want to be able to receive Payments from DinoPay accounts.

To be able to receive DinoPay Deposits, Walletera users must create a DinoPay Deposit Account in Walletera. This account will be linked to a DinoPay account that will be owned and managed by Walletera.

In order to receive a Deposit a Walletera user must share its DinoPay Deposit Account details.

When someone sends money to a user’s DinoPay Deposit Account, and Walletera receives the corresponding PaymentCreated notification from DinoPay, a Deposit must be created for the corresponding user in Walletera.

Acceptance criteria

Given a user with a DinoPay Deposit Account created in Walletera
When someone sends money to that DinoPay account
And Walletera receives the corresponding DinoPay PaymentCreated notification
Then A Deposit must be created for the user on Walletera

DinoPay Withdrawals

Context

Walletera users with funds on their accounts want to be able to Withdraw those funds (totally or partially) to DinoPay accounts.

In order for users to be able to withdraw money to a DinoPay account they must first create a DinoPay Beneficiary with the details of the destination account. Once the Beneficiary is created the user can create a Withdrawal to that Beneficiary. When the Withdrawal is created Walletera must create a corresponding Payment on DinoPay API.

Acceptance criteria

Given a user with funds in his Walletera account
And the user has created a DinoPay Beneficiary
When the user creates a Withdrawal for that beneficiary
Then a Payment must be created on DinoPay with the Beneficiary Account as the Destination Account

Domain Modeling

We have our requirements defined. Our digital wallet will offer its users the ability to receive deposits and perform withdrawals using the DinoPay payment provider.

The next step will be modeling our domain. The Domain Model should represent the vocabulary and key concepts of the problem domain and it should identify the relationships among all of the entities within the scope of the domain. This is the phase where we understand the problem.

To model our domain we will follow a particular technique called Event Storming. EventStorming is a workshop format for quickly exploring complex business domains. It proposes to explore the domain starting from Domain Events. A Domain Event is something meaningful that happened in the domain. It can be easily translated into software, but the real value here is that it could be quickly grasped by non-technical people. Domain events say little about the design and nothing about the implementation, which is exactly what you want from a good domain model.

Along with the domain events, Event Storming proposes to also define the commands that produce the events, the aggregates those commands modify, and the actors that execute the commands.

Once we have the Domain Events identified we will group them in Bounded Contexts. Bounded context is one of the most important concepts of Domain Driven Design, we can say that is a conceptual limit to where a domain model is applicable. As you try to model a large domain, you will have great difficulties, because different groups of people will use subtly different terms and sentences. That means that any use of that vocabulary outside of that limit will probably mean something different. We can say then that a Bounded context is mainly a linguistic delimitation.

It is important to understand that each Bounded context will have its own Ubiquitous Language. This language will be compounded by the terms (the business concepts) used when naming the events, commands, aggregates, and actors. It is a set of unambiguous vocabulary shared by the developers and stakeholders involved in the modeling process.

The last stage of our domain modeling process will be the creation of a Contexts Map. The Contexts Map, another concept from DDD, is a diagram that shows the way each bounded context interacts with others.

We know what we have to do, now let’s get to work.

Event Storming

As a result of our Event Storming session, we have 5 domain events. Let's describe each of these events along with its associated commands and entities (aggregates).

WithdrawalCreated: A user creates a withdrawal using the web or mobile interface. This user action triggers a CreateWithdrawal command. The command will modify the Withdrawal entity. If the command succeeds a WithdrawalCreated event will be produced.

OutboundDinoPayPaymentCreated: The process that handles the WithdrawalCreated event (for the DinoPay PSP) will generate a CreateOutboundDinoPayPayment command. Handling this command involves creating a payment using the DinoPay API. If the payment is successfully created an OutboundDinoPayPaymentCreated event is produced. The associated entity is the OutboundDinoPayPayment.

OutboundDinoPayPaymentUpdated: This event will be triggered after processing a PaymentUpdated notification sent by DinoPay. The notification handler will generate an UpdateOutboundDinoPayPayment command. The command updates the OutboundDinoPayPayment entity. If it succeeds then the event OutboundDinoPayPaymentUpdated is published.

WithdrawalUpdated: Processing either an OutboundDinoPayPaymentCreated or an OutboundDinoPayPaymentUpdated event implies updating a Withdrawal. In the first case, the update must change the Withdrawal status to delivered, and in the second case, the new status will depend on the status contained in the OutboundDinoPayPaymentUpdated event. To update a Withdrawal an UpdateWithdrawal command must be generated. If this command succeeds we published a WithdrawalUpdated event.

InboundDinoPayPaymentCreated: To notify about a new payment DinoPay send a PaymentCreated notification. Upon receiving this notification we must create an InboundDinoPayPayment entity. To do this we generate a CreateInboundDinoPayPayment command. If the command succeeds an InboundDinoPayPaymentCreated event will be produced.

DepositCreated: The processing of an InboundDinoPayPaymentCreated event will produce a CreateDeposit command to create a Deposit. If the command succeeds a DepositCreated event will be produced.

Bounded Contexts

Now we will draw boundaries around our domain events.

We identified a couple of bounded contexts. We have the Payments contexts which contain the core domain events Deposit Created, Withdrawal Created, and Withdrawal Updated. We also identified another context that we called DinoPay Gateway. This context contains the events (aggregates and commands) that model the interaction with the DinoPay PSP. The events of the DinoPay context are Outbound DinoPay Payment Created, Outbound DinoPay Payment Updated, and Inbound DinoPay Payment Created.

Note that for the operation of transferring money from one account to another, in the DinoPay Context, we use the term Payment (prefixed by Inbound or Outbound to clarify the direction of the operation) while in the Payments Context, we use the terms Deposit and Withdrawal. This is because in the first case, we are modeling the part of our domain that interacts with a particular payments service provider and that particular PSP uses the term Payment to denote this operation. In the other case, we modeled the core business logic related to this operation, and in our particular business the operation of transferring money out of the system is called withdrawal and the operation of receiving money from an external source is called deposit. That's the idea behind Bounded Contexts, instead of trying to fit the entire domain in a unique language we use different languages (the so-called ubiquitous languages) to model different parts of the domain.

Finally, you can see that there is an arrow defining a relation between one bounded context to another. The resulting diagram is another DDD concept called Context Map. Context Maps are diagrams that define how different bounded contexts interact with each other. In our case, the gateway contexts interact with the Payment context using a particular type of relation called Conformist. In this kind of relationship one of the bounded contexts, the Gateway context, will conform to the interface defined by the other bounded context, the Payments context.

The architecture of our digital wallet

We modeled our domain. We defined the domain events, along with their associated command and aggregates, and we put them into different bounded contexts. We also defined the type of relationship that will allow those contexts to interact. At this point, we can say that we have a good understanding of the problem that we want to solve. Now we can move forward and start working on the solution.

The first thing that we are going to do is decide what type of architecture will suit better our use case. Should we use a monolithic architecture or opt for a microservice architecture? Let’s see if the work we did previously can help us to make this decision.

A digital wallet has many subdomains (bounded contexts). We already discussed two of these subdomains, Payments and DinoPay Gateway, but there will be more. For example, for each different external payment provider that we integrate into our platform, we will have a new bounded context. Moreover, a digital wallet is not only about deposits and withdrawals, it will also need to implement some kind of KYC (Know Your Client) to comply with government regulations, it may also offer to its users investment alternatives (generating some revenue from the money that the user has in the wallet), etc. Each of these subdomains should be modeled also as a different bounded context.

The fact of having different independent subdomains inside our digital wallet domain tells us that the architecture must be modular. Is this enough to decide on microservices over a monolithic architecture? Well, no. In both cases, we can achieve modularity. Microservices are modular by definition but we can also build a monolithic architecture in a modular way by implementing what is known as a modular monolith. So, we need to dig deeper into our domain and look for other characteristics that can help us decide on what architectural style should we choose.

Let’s start with maintainability. Will all the subdomains require the same level of maintenance? Do we want to achieve the same level of reliability on all of them? The Payments service for example is part of the core of our business, we want it to be as reliable as possible. We can also expect it to be more stable in terms of the pace at which we will be adding new features. On the other hand, we may have a subdomain that will deal with marketing campaigns (for example, a referral program that rewards users that bring new clients to the platform). In this subdomain, we can expect to have a higher rate of releases. Furthermore, with this kind of subdomains, we will probably be more concerned about time-to-market than reliability. So it looks like in our digital wallet domain we have different subdomains with different maintainability requirements. In cases like this, it is better to implement the subdomains as different deployable units, a.k.a. microservices.

We can also think in terms of scalability. We may have different subdomains with different scalability requirements. Let's think about the gateways. On one hand, we might have a gateway that handles domestic instant payments. In Argentina, for instance, users from digital wallets can send and receive instant payments directly to or from a bank account via the usage of a 22-digit number code called CVU. On the other hand, we may have another gateway that enables users to send international payments using the SWIFT network. This can be achieved by integrating the platform with payment providers like Wise. These two gateways will clearly have different scalability requirements. We can expect thousands of domestic payments per day (if our product is successful enough) but only a few international payments per week. In this case, implementing each gateway as an independent microservice allows us to run as many instances as we need for the domestic payments gateway and just the minimum required (to guarantee high availability) for the international payments gateway.

We found some strong arguments in favor of a microservices architecture, so that will be our choice. Let’s see what this architecture may look like.

In the diagram, we drew a microservice for each of the bounded contexts defined in the previous section. We have the Payments service and the DinoPay Gateway service.

The Payments service will expose an API that allows end users to create and consult withdrawals. The API will also allow querying for the received deposits. Whenever a withdrawal or a deposit is created the Payments service will publish a domain event to the message queue.

The Payments service will also expose an internal API. This API will be used by the different gateways to create deposits and also update the status of the withdrawals. In Appendix B, you can find a detailed explanation of the Payments Internal APIs (the HTTP API and the Events API).

The DinoPay Gateway microservice will handle the communication with the DinoPay PSP. On one hand, it will translate WithdrawalCreated events published by the Payments service into requests for the DinoPay API. On the other hand, the Gateway will receive webhook notifications coming from DinoPay, like PaymentCreated or PaymentUpdated, and will translate them into requests for the Payments service.

DinoPay Withdrawal flow

Let’s now describe the flow of a DinoPay withdrawal. The flow starts when a user creates a withdrawal via the Payments Users API. After applying the correspondent business rules (limit checks, funds verification, funds locking) the Payments service creates the withdrawal in a pending state. Then it publishes a WithdrawalCreated event to the Message Queue.

The Message Queue will send the message to the subscribed gateways. Each gateway will receive the message but only the DinoPay gateway will process it (let's assume that the event contains an attribute that will allow each gateway to know what event has to process). The gateway will use the event information to build the request that the DinoPay API expects to create a payment. Based on the result of the request the gateway will return to the Payments service and will update the withdrawal state sending an HTTP PATCH request. If the Payment was successfully created the Withdrawal must be updated with the new status which in this case is delivered and with the external_id which is the id assigned by DinoPay to the Payment. If the request failed the withdrawal must be updated to the failed status.

DinoPay will send a webhook notification of type PaymentUpdated to let us know about the final state of the payment. If the payment was successfully delivered to the destination account the PaymentUpdated notification will contain a status attribute with the value confirmed. If any error occurred and DinoPay was not able to deliver the payment the status attribute will be failed. When the Gateway receives this notification it must translate it into the PATCH request that the Payments service expects to update the corresponding withdrawal.

DinoPay Deposit flow

In the case of the deposits the flow will be simpler because the deposits don’t have a status attribute. The flow begins with a PaymentCreated webhook notification sent by DinoPay. The request will be handled by the DinoPay Gateway. For each of these notifications, the Gateway must build a POST request to create a Deposit via the /deposits endpoint of the internal Payments API. The Payments service will process the request to create the Deposit. It will apply the corresponding business rules and if there are no errors the deposit will be created and a DepositCreated event will be published.

DinoPay Gateway Design

We are going to end this article with a high-level design of one of our microservices, the DinoPay Gateway. This design will set the basis for the following articles where we will start writing some code and explore some software design principles, practices, and patterns.

Continuing with our event-first approach we see that the DinoPay Gateway will have to deal with three different groups of events

  • Payments event: WithdrawalCreated

  • DinoPay PSP webhook notifications: PaymentCreated, PaymentUpdated

  • Gateway internal events: OutboundDinoPayPaymentCreated, InboundDinoPayPaymentCreated, InboundDinoPayPaymentUpdated

With this in mind, we can start drawing a design diagram.

We split the DinoPay Gateway into three independent processing pipelines. The tasks involved in these pipelines are abstracted in each Message Processor. As we are going to see later, all the communication between two different pipelines will be indirect, through events. This way we can keep the pipelines completely decoupled from each other.

From now on we will use the term message instead of event. A message can be thought of as a wrapper of an event that adds metadata, like the event type, useful for storing or routing an event.

The Message Processors are high-level components that will coordinate the tasks needed to process the messages. Some of these tasks involve connecting to the messages source, deserializing the messages, and routing the deserialized message to specific handlers. The handlers are components that know how to process a specific event.

Now let’s think about the processing of each particular message group. Starting with the Payments events we have the WithdrawalCreated message. For each of these messages, we have to create a Payment on the DinoPay API. This is done by sending a POST request to the DinoPay API /payments endpoint. The logic needed to interact with the DinoPay API will be implemented in a specific component called DinoPayAPIClient. The WithdrawalCreatedHandler will use this component to create a Payment, if the payment is created successfully the handler will generate and store an OutboundDinoPayPaymentCreated message. Let's see how this fits into our diagram.

Another group of events that we need to process are the DinoPay webhook notifications. These notifications are PaymentCreated and PaymentUpdated. If we revisit the section where we introduced the DinoPay API we will see that the PaymentCreated notification will be sent whenever an inbound payment (which belongs to one of our user accounts) is received by DinoPay. This means that when we receive this notification we have to generate and store in the Internal Messages Repository an InboundDinoPayPaymentCreated message. In the case of the PaymentUpdated notification, from the DinoPay API description, we know that it will be sent by DinoPay whenever the status of an outbound payment changes, so in this case we have to generate and store an OutboundDinoPayPaymentUpdated event. The diagram now looks like this.

Last but not least we have the internal messages InboundDinoPayPaymentCreated and OutboundDinoPayPaymentUpdated that were generated and stored by the other two pipelines. From the InboundDinoPayPaymentCreated we must create a Deposit in the Payments service. To do so we have to send an HTTP POST request to the Payment’s endpoint /deposits. In the case of the OutboundDinoPayPaymentUpdated what we have to do is update the corresponding Withdrawal. For this, we have to send an HTTP PATCH request. The interaction with the Payment API will be implemented in a component named PaymentAPIClient. Let’s see the final version of our diagram.

We finished the high-level design of the DinoPay Gateway. The design defines the internal architecture of the application which will be event-based. We split the application into three independent message-processing pipelines, one for each of the event groups that the application must handle, and also identified the main components that will compound these pipelines.

Summary

This blog post inaugurates the Walletera series of articles where we will explore different software engineering topics by building a digital wallet. In this article, we introduced, from a business perspective what a digital wallet is, we wrote the requirements for two essential features, deposits and withdrawals and we modeled the domain using Event Storming and Domain Driven Design.

With the requirements and the domain model in place, we discussed some alternatives for the architecture. Based on the analysis of the characteristics of each of the subdomains that exist in a digital wallet we choose a microservice architecture over a modular monolith. In this section, besides identifying the different services, we also established how these services will communicate with each other and we also defined the basic flow for the deposits and withdrawals.

Finally, we dug a bit deeper into the DinoPay Gateway microservice. We defined its main components, their responsibilities, and how they will interact.

In the following articles, we will start writing some code. Along the way, we will see some design principles and patterns that can be applied to produce readable, testable, and maintainable applications. See you there!

Appendix A: DinoPay API Endpoints

Creating payments

In order to create a payment on the DinoPay API we have to send a POST request to the /payments endpoint. Below there is an example the request body.

{
   "amount": 100,
   "currency": "USD",
   "sourceAccount": {
      "accountHolder": "john doe",
      "accountNumber": "IE12BOFI90000112345678"
   },
   "destinationAccount": {
      "accountHolder": "jane doe",
      "accountNumber": "IE12BOFI90000112349876"
   },
   "customerTransactionId": "9713ec22-cf8d-4a21-affb-719db00d7388"
}

If the request succeeds the response status code will be 201 and the body will contain the details of the payment created. Here is an example of the json returned by this endpoint.

{
   "id": "bb17667e-daac-41f6-ada3-2c22f24caf22",
   "amount": 100,
   "currency": "USD",
   "sourceAccount": {
      "accountHolder": "john doe",
      "accountNumber": "IE12BOFI90000112345678"
   },
   "destinationAccount": {
      "accountHolder": "jane doe",
      "accountNumber": "IE12BOFI90000112349876"
   },
   "status": "pending",
   "customerTransactionId": "9713ec22-cf8d-4a21-affb-719db00d7388",
   "createdAt": "2023-07-07T19:31:11Z",
   "updatedAt": "2023-07-07T19:31:11Z"
}

The possible statuses for a payment are: pending, confirmed, failed.

Receiving notifications from DinoPay

In order to be notified about new payments created in DinoPay for accounts linked to our user’s internal accounts we have to use the /webhooks/subscription endpoint. To create a new subscription we will send a POST request with the following body.

Then whenever a new inbound payment is created in DinoPay for one of our accounts we will receive a POST request on the callbackUrl that we configured. The body of the request will be like this.

{
  "id": "647f9176-466a-4d8c-b027-d53b4da77d4d",
  "type": "PaymentCreated",
  "time": "2023-07-07T19:31:11.123Z",
  "data": {
    "id": "bb17667e-daac-41f6-ada3-2c22f24caf22",
    "amount": 100,
    "currency": "USD",
    "sourceAccount": {
      "accountHolder": "john doe",
      "accountNumber": "IE12BOFI90000112345678"
    },
    "destinationAccount": {
      "accountHolder": "jane doe",
      "accountNumber": "IE12BOFI90000112349876"
    },
    "status": "confirmed",
    "customerTransactionId": "9713ec22-cf8d-4a21-affb-719db00d7388",
    "createdAt": "2023-07-07T19:31:11Z",
    "updatedAt": "2023-07-07T19:31:11Z"
  }
}

The request body contains the event id, the event type, and the data which in this case is a payment. For inbound payments, the status will always be confirmed.

Besides the PaymentCreated event, we can also subscribe to the PaymentUpdated event. We will receive this event when the status of an outbound payment changes, either from pending to confirmed if the operation was successful or from pending to failed if any error occurred. In this case, the request will also be an HTTP POST with the following body.

{
  "id": "647f9176-466a-4d8c-b027-d53b4da77d4d",
  "type": "PaymentUpdatedd",
  "time": "2023-07-07T19:31:11.123Z",
  "data": {
    "id": "bb17667e-daac-41f6-ada3-2c22f24caf22",
    "amount": 100,
    "currency": "USD",
    "sourceAccount": {
      "accountHolder": "john doe",
      "accountNumber": "IE12BOFI90000112345678"
    },
    "destinationAccount": {
      "accountHolder": "jane doe",
      "accountNumber": "IE12BOFI90000112349876"
    },
    "status": "confirmed",
    "customerTransactionId": "9713ec22-cf8d-4a21-affb-719db00d7388",
    "createdAt": "2023-07-07T19:31:11Z",
    "updatedAt": "2023-07-07T19:31:11Z"
  }
}

Appendix B: Payments Internal API

As part of the architecture definition, we will define the internal APIs of the Payments service. These APIs are the HTTP API that will be used by the gateways to create deposits and update the status of withdrawals, and the Events API which will be compound by the domain events published by the Payments service.

HTTP API

To create a deposit a gateway must send an HTTP POST request to the endpoint /deposits with the following body.

{
  "id": "9713ec22-cf8d-4a21-affb-719db00d7388",
  "user_id": "9713ec26-cf8d-5r63-affb-819db00d9876",
  "psp_id": "dinopay",
  "external_id": "bb17667e-daac-41f6-ada3-2c22f24caf22",
  "amount": 100,
  "currency": "USD",
  "source_account": {
    "holder": "john doe",
    "number": "067011987",
    "routing_key": "1200079345"
  },
  "destination_account": {
    "holder": "jane doe",
    "number": "097011456",
    "routing_key": "5600079946"
  }
}

To update a withdrawal a gateway must send an HTTP PATCH request to the endpoint /withdrawals/{withdrawalId} with the following body.

{
  "external_id": "bb17667e-daac-41f6-ada3-2c22f24caf22",
  "status": "confirmed"
}

Events API

Whenever a user creates a withdrawal the Payments service will publish a WithdrawalCreated event with the following structure.

{
  "type": "WithdrawalCreated",
  "data": {
     "id": "0ae1733e-7538-4908-b90a-5721670cb093",
     "user_id": "2432318c-4ff3-4ac0-b734-9b61779e2e46",
     "psp_id": "dinopay",
     "external_id": "7722d65d-9be1-4fa4-bdd8-fbbcdb3526f3",
     "amount": 0,
     "currency": "USD",
     "status": "pending",
     "beneficiary": {
       "id": "2f98dbe7-72ab-4167-9be5-ecd3608b55e4",
       "description": "Richard Roe DinoPay account",
       "account": {
        "holder": "Richard Roe",
        "number": 0,
        "routing_key": "1200079635"
       }
     }
  }
}

The possible values for the status field are: pending after created but before it is sent to the payments provider, delivered when it was successfully sent to the payments provider, failed or confirmed.