Marketplace — backend plugin
Vendor identity, an idempotent earnings ledger credited from invoice.paid, commission, and payout through the withdraw rail — the agnostic vendor core.
What the backend plugin owns
The marketplace backend plugin is the central, agnostic vendor layer. It owns two tables — vbwd_vendor (a user\'s selling identity) and vbwd_vendor_earning (one accrual per attributed invoice line) — plus the services that turn a paid invoice into a vendor credit and a withdraw into real money. It follows the standard layered shape (routes → services → repositories → models) and declares a single dependency: withdraw.
The decoupled money path
The plugin never imports a vertical. Coupling is one documented string on the invoice line:
line_item.extra_data["vendor_id"] = str(vendor_user_id)At checkout a vertical stamps the buyer\'s line with the selling vendor\'s user id. The marketplace subscribes to invoice.paid, reads that key, resolves the vendor and writes one VendorEarning — the commission snapshotted at the rate in force at sale time. A UNIQUE(invoice_id, line_item_id) guard makes a replayed event a no-op, so it never double-credits.
Refunds & reversal
On invoice.refunded the matching accrued earnings flip to reversed (a safe no-op when none match). The ledger status lifecycle is accrued → reversed (refunded) or withdrawn (paid out).
invoice.paid/invoice.refunded are fire-and-forget events published after the payment transaction commits, so the handler persists its own ledger write and is wrapped so a failure can never break the payment flow.Payout through the withdraw rail
On enable, the plugin registers a vendor_earnings balance source into the withdraw plugin (implementing its IWithdrawableBalance port). That is what lets a vendor cash out through the existing withdraw flow with no new payout code: the balance is the sum of accrued net_credit, a debit consumes accrued rows oldest-first, and a failed payout restores them.
The seams a vertical plugs into
| Seam | Purpose |
|---|---|
The stamp (extra_data["vendor_id"]) | The money path — vertical → marketplace. |
| Listing-type catalog | Advertises which verticals are sellable right now (plugin enabled ∧ entity type registered ∧ vendor-mode on). |
| Vendor CRUD routes | Each vertical serves /<vertical>/vendor/* scoped to the caller\'s vendor id. |
| Owner-resolver registry | Admin “who owns this now?” — deliberately not the money path. |
A new vertical becomes sellable by adding a nullable vendor_id, stamping the checkout line, registering its entity type, and adding one row to the listing-type catalog — the only change inside the marketplace plugin.
API surface
| Endpoint | Who |
|---|---|
POST /api/v1/marketplace/become-vendor | Any authenticated user. |
GET /api/v1/marketplace/my/sales | The vendor (own lines only). |
GET /api/v1/marketplace/my/earnings | The vendor (ledger + balance). |
GET /api/v1/marketplace/listing-types | The vendor (create dropdown). |
GET/PUT /api/v1/admin/marketplace/vendors[/<id>] | Admin (marketplace.manage). |
Permissions: marketplace.vendor (granted on approval), marketplace.view and marketplace.manage. Config: default_commission_rate, auto_approve_vendors, payout_currency.
The two front-ends
The backend serves data to two plugins: the fe-user vendor area and the fe-admin management plugin. Start from the marketplace overview for the product picture.