Payments and subscriptions
Taking money is split the same way as authentication: a core
Payment module owns the shape of a payment without knowing any provider, and
a provider module plugs in the actual charging. Stripe is the provider that ships, and
MemberStripe ties it back to individual members.
A provider-agnostic core
The Payment module owns the payment concepts a site works in, without
containing any provider's code. It models three things: a purchase, which is a one-off
charge; a subscription, which is a recurring charge on a schedule; and a merchant, which
is an account that receives money on platforms that pay several parties. Each of these
keeps a history of its status, so a purchase moves through created, pending, success or
error, and a subscription through pending, active, past-due and cancelled, with every
change recorded rather than overwritten. Amounts are stored as whole small units rather
than floating-point money so totals never drift.
Providers register with the core
The core does not charge cards itself; it defines the slots a provider fills. A
provider module registers itself against the Payment core as a purchase
provider, a subscription provider, or a merchant provider (or all three), and the core
then routes work of that kind to it. This is the same provider pattern the framework
uses for messaging and elsewhere: the core owns the durable records and the
provider-independent shape, and the provider owns the conversation with the outside
service. See Submodules, providers and vendors. It also
means a site could swap or add a payment provider without disturbing the records or the
rest of the application.
The Stripe provider
PaymentProviderStripe is that provider for Stripe. It handles the actual
charging: creating and capturing payments, saving a member's payment methods, onboarding
merchant accounts through Stripe Connect, and paying out. Because a real charge completes
asynchronously, the provider listens for Stripe's webhooks and updates the matching
record in the Payment core when the outcome arrives, so the core's status
history always reflects what actually happened at the provider. A site gives the provider
its Stripe keys and webhook secret through configuration, the same way every module is
configured:
Config::Provide('PaymentProviderStripe', [
'PAYMENT_PROVIDER_STRIPE_SECRET_KEY' => 'sk_live_...',
'PAYMENT_PROVIDER_STRIPE_PUBLISHABLE_KEY' => 'pk_live_...',
'PAYMENT_PROVIDER_STRIPE_WEBHOOK_SECRET' => 'whsec_...',
]);
The keys live with the provider that uses them, not in the core, so the core stays free of any Stripe-specific setting.
Members and their Stripe identity
MemberStripe links a member to their Stripe identity. It keeps, per
member, the Stripe customer and any connected merchant account, along with the onboarding
state Stripe reports back (whether the account is verified, can take charges, can be paid
out) and cached links for onboarding and the Stripe dashboard. It provides the return and
refresh pages Stripe redirects a member to during onboarding, so a member can set up as a
payee and come back into the site. This is where the generic
member account meets the payment provider: the account stays
provider-neutral, and MemberStripe holds the Stripe-specific link beside it.
Subscriptions and billing
A subscription in the core carries who it belongs to, how much and how often, and a
reference to the provider's own subscription, plus a status history that tracks its life
from pending through active to cancelled. There is no separate table of plans; a
subscription is described by its own fields (its owning module and identifier, its amount
and its regularity), and the provider holds the recurring billing object that actually
charges on schedule. So the core answers what a member is subscribed to and what state it
is in, while the provider does the charging and reports each cycle's outcome back through
webhooks. Operators see all of this through the PaymentAdmin and
PaymentProviderStripeAdmin submodules, built the way
Building an admin system describes.
Next: Messaging.