What we need:
- The payment gateway plugin type
- The payment gateway config entity + UI
- The payment type YAML plugin
- The credit_card payment type
- The payment method content entity (for storing tokenized payment information), which uses the payment types as bundles
- The payment content entity, which uses payment types as bundles
The checkout flow plugin type + config entity combo introduced in
We could also spin off the payment content entity to its own issue, focus only on the initial infrastructure.
The main logic will go into commerce_payment_gateway plugins which will store their configuration in commerce_payment_gateway configuration entities.
The plugins will extend a base class (we can provide multiple) and implement a set of interfaces to indicate its own capabilities (supporting preauth, refunds, saving payment information). The plugin has complete freedom in how it’s structured internally, it can use an SDK, inject a Guzzle object, delegate to its own helpers, etc.
We should be flexible enough to support non-CC payments. Contrib should be able to implement Direct Debit (used for recurring in europe), or a two-step flow for checks (check received/deposited).
We need to name the entity that stores payment information for a user, as well, as its bundle.
This terminology is very uneven between payment gateways and other systems, mostly around how they define a payment method.
Here’s a definition given by Microsoft:
A payment method is the way in which customers pay for the items that they purchase on a Web site. Payment methods can include credit cards, gift certificates, purchase orders, cash cards, and custom methods.
But does that make a “credit card as a concept” a payment method, or is an actual credit card the payment method? Braintree thinks it’s an actual user’s card:
“A payment method represents transactable payment information such as credit card details or a customer's authorization to charge a PayPal or Venmo account. Payment methods belong to a customer”
Others use it to mostly to mean “the type of payment we support”, and name the saved payment information in some other way (Stripe alternates beteen calling it the payment source and the token, authorize.net calls it the payment profile, paymill calls it just “payment” which is weird).
Most major sites say “Payment method” to mean “your saved information”.
So, we have two options here:
1) “Payment method” is the saved payment information. The bundle comes from the “payment type” plugin (ala hooks in D7).
2) “Payment information” is again the saved payment information, the bundle comes from the “payment method” plugin.
Note: We decided to go with #1.
The second problem is whether to have a Payment entity or a Payment transaction entity.
Gateways tend to think in terms of transactions, users tend to think in terms of payments, I am unsure which version is easier for us developers.
Payment transactions are more low-level and look like this:
A transaction is immutable, performing an action on it (e.g. capturing an authorization) simply creates another one. It stores the remote transaction id, the remote status, and a message providing additional information (PayPal has 20 reasons why a payment might be pending, for example). Authorize, Capture, Refund are transaction types (bundles). They are grouped by payment type (credit card, direct debit). The status of a transaction is FAILURE, SUCCESS, PENDING.
This is exactly what Commerce 1.x did. Note that it is possible to capture less than what was authorized (the rest is returned), and it is possible to refund a single capture several times, until all of the money is gone. There’s no concept of “this transaction is all used up” (authorization captured, capture refunded), there might need to be.
With a payment entity you’d create a $100 payment and authorize it.
If you decided to capture $80, then that becomes the new $payment->amount.
If you refunded $30, the new $payment->amount is $50.
(We could also make refunds separate payments with a negative amount but I’m unsure I see a point).
A payment would have a workflow:
New -> Pending -> Complete -> Cancelled | (Partially) Refunded.
This workflow is generic enough for all payment types.
For credit cards, new means “I am about to redirect / authorize”. Pending means “we have an authorization that needs to be approved OR something is stuck on the gateway side”.
There would also be a boolean status to indicate whether the actual request failed, requiring a retry.
In this case the payment only has the amount, remote status and message for the last action, anything before that (authorization, a failed capture) is lost, which creates the need to have a log entity (e.g. the order level activity-log with a payment group that can be filtered on)
Note: We decided to go with a Payment entity.