Pricing architecture
All Sellub pricing — plans, variants, commission rates, attribution sources, add-on costs and seller-facing copy — flows from a single TypeScript package:
@duabalabs/sellub-pricing (packages/sellub-pricing)
Server, dashboard and storefront all consume the same package. There is no hardcoded price, commission rate or plan name anywhere else in the codebase.
What lives in the package
| Module | Purpose |
|---|---|
plans | SellerPlan, SellerPlanVariant, PLAN_CATALOG, ALL_PLANS |
attribution | Attribution sources (marketplace_*, seller_direct_*, …) |
commission | resolveCommissionPercent({ plan, variant, attributionSource, override }) |
fees | Order-fee breakdown calculation (gross / commission / processing / payout) |
addons | ADDONS, ALL_ADDONS, calculatePerformanceAdFee() |
currency | formatMinor(), formatPercent() (GHS pesewas-aware) |
education | EDUCATION map of seller-facing FAQ/tooltip copy |
Build & test:
cd packages/sellub-pricing
npm run build
npm testThe 3-tier plan model
| Plan | Pricing | Direct commission | Marketplace-attributed |
|---|---|---|---|
MARKETPLACE | Free | n/a | 5% |
CUSTOM_DOMAIN | Starter GHS 250/mo · Pro GHS 600/mo | 3% / 2% | 4.5% |
COMMERCE_API | Developer GHS 500 · Growth GHS 1500 | 1.5% / 1% | 3.5% |
MARKETPLACE always has marketplace discovery on. The other two plans expose a
toggle (marketplaceDiscoveryEnabled).
Commission resolution
Commission depends on (plan, attributionSource) — never on plan alone.
direct_rate(plan, variant) — for seller_direct_*, external_*, *_campaign
marketplace_rate(plan, variant) — for marketplace_* attribution sourcesAn admin override (commissionOverridePercent, clamped [0,100]) always wins
when set.
import { resolveCommissionPercent } from "@duabalabs/sellub-pricing";
const rate = resolveCommissionPercent({
plan: "CUSTOM_DOMAIN",
variant: "PRO",
attributionSource: "marketplace_search",
overridePercent: null,
}); // 4.5Attribution sources
11 attribution sources, grouped:
marketplace_search,marketplace_category,marketplace_homepage,marketplace_recommendation→ marketplace rate.seller_direct_storefront,seller_direct_app,seller_direct_link,external_api,external_referral,email_campaign,social_campaign→ direct rate.
The default for an order is seller_direct_storefront. Commerce API orders
default to external_api. Resolution lives in
PricingService.resolveAttribution() and consults order.metadata first.
Server plugin: sellub-pricing-plugin
Registered first in the Vendure plugins array (before MultivendorPlugin)
so PaystackService can resolve PricingService via ModuleRef at runtime.
Adds these Seller customFields:
sellerPlan— string enum, defaultMARKETPLACE.sellerPlanVariant— string, nullable.commissionOverridePercent— float, nullable,[0,100].marketplaceDiscoveryEnabled— boolean, public, defaulttrue.sellubSandboxChannelId— string, nullable; managed by the commerce-api plugin.
Adds an Order customField:
attributionSource— string, nullable.
Persists an OrderFeeBreakdown entity (one row per (order, seller)) on
OrderPlacedEvent. The row is an immutable snapshot of plan, variant,
attribution, gross, rate, commission, processing fee, payout and platform
revenue at the moment of sale.
PaystackService integration
PaystackService.getPlatformFeePercent(ctx, sellerId) lazy-resolves
PricingService and delegates per seller. Multivendor orders compute each
seller’s share independently; the aggregate platformFeePercent reported back
to Paystack is a weighted average for telemetry only — the real per-seller
splits are honoured.
Admin GraphQL surface
Mutations (SuperAdmin only):
setSellerPlan(input: { sellerId, plan, variant? })setSellerCommissionOverride(sellerId, overridePercent)—nullclears.setMarketplaceDiscoveryEnabled(sellerId, enabled)— refuses if plan doesn’t support it.
Queries:
sellerPricingProfile(sellerId)— effective rates after override + discovery state.pricingPlans— also exposed on Shop API for the public pricing page.pricingAttributionSourcesorderFeeBreakdowns(orderId)
Add-ons
Add-ons are independent of plans and live in
@duabalabs/sellub-pricing/addons:
| Add-on | Pricing |
|---|---|
FEATURED_LISTING_SINGLE | GHS 30 per listing |
FEATURED_LISTING_BUNDLE | GHS 120 / month, unlimited slots |
PERFORMANCE_ADS | 11% of attributed sale |
DUABACONNECT | GHS 350 / month |
Subscription state is persisted by the existing dps-e-billing plugin’s
FeatureSubscription entity. The dashboard surfaces them through the admin
query sellerSubscriptions(sellerId).
Migration notes
The legacy 5-tier plans (Starter, Growth, Pro, Modules, Enterprise)
have been removed. A one-shot migration maps every existing seller onto
MARKETPLACE, CUSTOM_DOMAIN or COMMERCE_API based on their previous tier.
The legacy Seller.customFields.platformFeePercent is kept as a fallback only
when PricingService is unavailable.
See also
- Commerce API (Tier-3) — programmatic access on top of this pricing model.
- Architecture — where the pricing plugin fits in.
- Plans (sellers) — the user-facing explanation of these same numbers.