Skip to main content

The marketplace surface

Most of this package is for an app that sells its own product. This section is for the other shape: a platform that collects a fan's payment and routes it to a connected merchant, keeping a commission.

If you are a single seller, you never need to read past this paragraph. With the marketplace switch off — the shipped default — none of what follows exists: no route, no table, no event, no config key you have to set. Your install is byte-for-byte what it would be if this section had never been written. That promise is stated once, here, and every marketplace feature is built to keep it.

The switch, and what makes it real

billing.marketplace.enabled turns the surface on. But a config flag alone does not make money routable, and that is deliberate. The marketplace path hangs off an optional contract, RoutesMoney, that a driver must implement:

interface RoutesMoney
{
public function marketplaceRails(): MarketplaceRails;
}

A driver that does not implement RoutesMoney has no marketplaceRails() method, so no amount of configuration can produce a rails object to route through. The switch and the capability are two locks, not one: turning the flag on without a driver that routes money refuses to boot rather than silently doing nothing. The Stripe driver ships this capability; a custom driver opts in by implementing the interface.

MarketplaceRails itself is the seam onto the connected-account operations:

interface MarketplaceRails
{
public function onboarding(): MerchantOnboarding;
public function accounts(): MerchantAccountDirectory;
}

Two gates, two questions

The single-seller path knows one eligibility question: may this owner move money OUT? The marketplace adds its mirror, CanReceiveMoney, a separate fail-closed gate answering a different question about a different person — may money be routed TO this merchant?

interface CanReceiveMoney
{
public function check(Model $merchant): bool;
}

It is fail-closed because the capabilities behind it — a merchant's identity checks, their payout capability — arrive asynchronously from the provider. "We have not heard yet" has to mean no, or a merchant the provider has not cleared would be paid.

Routing a payment

A routed payment carries a ChargeRouting value: the destination account, the platform fee, and a ChargeType that decides who the provider treats as the merchant of record. That choice is a liability decision, not a technical one — see who carries the liability below — so it is made per payment and never inferred.

A refund of a routed payment reverses the merchant's share in the same call. A refund that returned the buyer's money without clawing back the merchant's would leave the platform paying out of its own pocket, and a lost chargeback is not a call the package can decline.

Who carries the liability

The charge type moves the merchant of record between the platform and the connected account, and everything downstream follows: who the buyer's receipt names, who bears a dispute, who pays the provider's processing fee. The decision matrix — which posture your platform should declare — is a jurisdiction question and is not repeated here; what matters at this level is that the money flow and the declared seller are checked against each other, and a pair that disagrees is refused before any money moves.

What stays a single-seller concern

The jurisdiction-specific tax rules — the regimes, the statutes, the creator tax-status matrix — belong to a jurisdiction profile, not here. This overview is the mechanics of routing money; a consumer on another profile reads it without meeting a single paragraph of one country's tax law.