SellubDevelopersPricing architecture

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

ModulePurpose
plansSellerPlan, SellerPlanVariant, PLAN_CATALOG, ALL_PLANS
attributionAttribution sources (marketplace_*, seller_direct_*, …)
commissionresolveCommissionPercent({ plan, variant, attributionSource, override })
feesOrder-fee breakdown calculation (gross / commission / processing / payout)
addonsADDONS, ALL_ADDONS, calculatePerformanceAdFee()
currencyformatMinor(), formatPercent() (GHS pesewas-aware)
educationEDUCATION map of seller-facing FAQ/tooltip copy

Build & test:

cd packages/sellub-pricing
npm run build
npm test

The 3-tier plan model

PlanPricingDirect commissionMarketplace-attributed
MARKETPLACEFreen/a5%
CUSTOM_DOMAINStarter GHS 250/mo · Pro GHS 600/mo3% / 2%4.5%
COMMERCE_APIDeveloper GHS 500 · Growth GHS 15001.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 sources

An 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.5

Attribution 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, default MARKETPLACE.
  • sellerPlanVariant — string, nullable.
  • commissionOverridePercent — float, nullable, [0,100].
  • marketplaceDiscoveryEnabled — boolean, public, default true.
  • 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)null clears.
  • 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.
  • pricingAttributionSources
  • orderFeeBreakdowns(orderId)

Add-ons

Add-ons are independent of plans and live in @duabalabs/sellub-pricing/addons:

Add-onPricing
FEATURED_LISTING_SINGLEGHS 30 per listing
FEATURED_LISTING_BUNDLEGHS 120 / month, unlimited slots
PERFORMANCE_ADS11% of attributed sale
DUABACONNECTGHS 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