I'd love to see how Jeffrey would do this, particularly with subscriptions. But for the meantime, I'm most of the way through doing it myself.
I would suggest avoiding Worldpay if possible. Just use Stripe. I'm only using Worldpay for legacy subscriptions and will no longer offer it as a payment option. Paypal, however, is worthwhile. Many people prefer paying with Paypal and some even insist on it (e.g. people who don't have a credit card).
I think the first step is to think about your "business objects". I ended up with the following tables in my database:
- users
- subscriptions
- payments
- cards
- vendors
- vendor_customers
- vendor_subscriptions
- vendor_messages
(...and of course there are other tables too, such as password_reminders)
"Vendors" are gateways like Stripe, Worldpay, Paypal. I chose to "namespace" some of the tables with "vendor_", but that's somewhat subjective. Each of these tables corresponds to a model, and you have a bunch of relationships to hook up.
Subscriptions, payments, and users contain no vendor-specific information. So for example, a subscription contains details about the price, the status (active / cancelled / whatever), and the schedule. Subscriptions also need a vendor-specific identifier, but that lives in the vendor_subscriptions table.
...and thinking about it, maybe I should have done the same thing for cards. It's up to you how much you want to normalise your database.
Anyway, the point of all this is that I maintain important business information independently of the vendor. For example, my own application knows when to expect the next payment of a subscription. Also, there are differences and inconsistencies between the vendors; I want to "strip out" these inconsistencies as early as possible, before they get into my business layer. We'll see that now:
When a Stripe webhook (or Paypal IPN, or Worldpay payment response...) comes in, I want to extract all the relevant business information, and also log the entire message as a JSON string in the vendor_messages table.
In my application this happens in several stages:
Each vendor is given a different route to send their messages -- e.g. /webhooks/stripe, /webhooks/paypal. These map onto methods in my PaymentsController. So the Stripe webhook is handled by PaymentsController@stripe. These methods are lightweight, and merely set a vendor propery (i.e. $this->setVendor('stripe')) and in Paypal's case does the Paypal IPN verification (via dedicated classes). Then they pass control to a handle() method.
I have a PaymentMessageInterface, which has implementations for Stripe, Paypal, and Worldpay. Back in my controller, the setVendor() method binds the specific implementation -- so when I ask for a PaymentMessageInterface, I will get (say) a StripeMessageHandler.
The StripeMessageHandler does not do any database updates. All it does is inspect the Stripe message, extract the relevant information, and convert it into a "standard" format (a big array). It then passes this to the next stage, which is my WebhookHandler. This means that all the Stripe-specific logic is contained in my StripeMessageHandler, and I "translate" the message into something standard that my system can easily handle. This includes some categorisation of the message -- e.g. this message is a 'subscription.start' or a 'subscription.payment' message.
The WebhookHandler receives this, and then works out what needs to be done. It prepares an array of events that need to be fired, along with the data they need. So for example, it might decide that the subscription.start event needs to be fired, as well as the subscription.payment event. The WebhookHandler does not do any database updates.
In all cases, a vendor.message.log event is prepared, because I always want to log the vendor message.
The Webhook handler outputs an array of event names, which themselves are arrays containing the data that the event needs. So back in the PaymentsController, the handle() method does the following:
- Pass the message onto the StripeMessageHandler, and get the output.
- Pass that to the WebhookHandler, and get its output.
- Take the output from the WebhookHandler and pass it to a fireEvents() method.
The fireEvents() method simply loops through the events and fires them all. These events are what finally update the database (and do other things, like sending an email).
Because of this structure, the handlers are very easy to test. I just give them an input array, and expect a certain output array. I still have functional/integration tests for the events, and even a few "end to end" tests for the PaymentsController. But by separating it into stages, I can use unit tests for the vast majority of combinations (and there are a lot of combinations).
The general idea is to separate your business logic from the payment gateway's logic, and to do this as early as possible.
I'm sure there are many improvements that could be made to this, especially with the command bus approach that Jeffrey has shown (I was too late for that). This is my first time doing it, and I'm not even a professional developer (this is just for my own site). Still, hopefully that might spark some ideas for you too. :)