Marketplace · Backend plugin

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.

Why a user id, not a row id. The stamp is meaningful even with the marketplace disabled (it\'s just a user id, no dangling foreign key), and a user who becomes a vendor after a stamp exists is still credited on the next paid invoice.
', '

Refunds & reversal

On invoice.refunded the matching accrued earnings flip to reversed (a safe no-op when none match). The ledger status lifecycle is accruedreversed (refunded) or withdrawn (paid out).

The callback commits itself.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

SeamPurpose
The stamp (extra_data["vendor_id"])The money path — vertical → marketplace.
Listing-type catalogAdvertises which verticals are sellable right now (plugin enabled ∧ entity type registered ∧ vendor-mode on).
Vendor CRUD routesEach vertical serves /<vertical>/vendor/* scoped to the caller\'s vendor id.
Owner-resolver registryAdmin “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

EndpointWho
POST /api/v1/marketplace/become-vendorAny authenticated user.
GET /api/v1/marketplace/my/salesThe vendor (own lines only).
GET /api/v1/marketplace/my/earningsThe vendor (ledger + balance).
GET /api/v1/marketplace/listing-typesThe 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.

')