In a regular store, implementing a discount is straightforward: the customer pays less, end of story. In Medusa.js, standard promotions are a native module, but
in a marketplace things get complicated quickly and custom solutions are needed — because a seller enters the picture, and there is one question you need to ask before you write the first line of code: who funds this discount?
That question forced me into a non-standard architecture when building new features at Artovnia.com.
In this post I describe how I implemented two platform-funded discounts — loyalty points and newsletter signup — without passing the cost of the platform's promotion on to sellers.
Finances and a problem easy to overlook
Imagine a simple scenario:
- the product costs 100 PLN,
- the platform commission is 20%,
- the platform offers new newsletter subscribers a 5% discount.
Naive implementation: I add a line item adjustment with code NEWSLETTER_SIGNUP, item.total drops to 95 PLN. Done.
But now let's calculate the seller payout:
prowizja = 95 × 20% = 19 PLN
payout sprzedawcy = 95 - 19 = 76 PLNWithout the newsletter discount the seller would have received 100 - 20 = 80 PLN.
The platform offers a discount promoting its own newsletter, and the seller loses 4 PLN — with no notification, no consent, no justification. This is not a library bug. It is a consequence of the fact that the standard discount mechanism in every cart system — including Medusa — operates on item.total, not on commission.
Two-step architecture
To prevent the seller from funding the platform's discount, every platform-funded discount must go through two steps:
Step 1 — when calculating the commission: add the discount back into the commission base.
Step 2 — after the commission has been calculated: subtract the discount from the platform's commission line.
Net result with a 5 PLN newsletter discount:
commission base = 95 + 5 = 100 PLN
prowizja = 100 × 20% = 20 PLN
prowizja po korekcie = 20 - 5 = 15 PLN
payout sprzedawcy = 95 - 15 = 80 PLN ✓The seller receives the same amount as without the discount. The customer pays less. The platform funds the difference from its own commission. Everyone wins.
One definition, two mechanisms
I implemented two types of platform-funded discounts: LOYALTY_POINTS and NEWSLETTER_SIGNUP. To avoid scattering the logic across the entire codebase — checkout, commission, payout — I defined them once in a single place:
export const PLATFORM_FUNDED_ADJUSTMENT_CODES = [
'LOYALTY_POINTS',
'NEWSLETTER_SIGNUP',
] as const
export const sumPlatformFundedAdjustments = (
adjustments,
options?: { includeCodes?: PlatformFundedAdjustmentCode[] }
) => {
return roundPlatformCurrency(
adjustments.reduce((sum, adjustment) => {
if (!isPlatformFundedAdjustmentCode(adjustment?.code)) return sum
if (
options?.includeCodes &&
!options.includeCodes.includes(adjustment.code)
) return sum
return sum + Math.max(0, toPlatformAmountNumber(adjustment.amount))
}, 0)
)
}Checkout can still reduce item.total as normal. However, every place that calculates commission or payout knows which codes are platform-funded — and treats them differently.
Add-back in calculate-commission-lines
In calculate-commission-lines.ts before calculating the commission for each item I add the platform-funded adjustments back in:
const platformFundedDiscountForItem = MathBN.convert(
sumPlatformFundedAdjustments(item.adjustments)
)
const itemForCommission = {
...item,
total: MathBN.add(
MathBN.convert(item.total),
platformFundedDiscountForItem
),
}
const commissionValue = await calculateCommissionValue(
commissionRule.rate,
itemForCommission,
order.currency_code,
container
)Regular seller discounts still reduce the commission base — because they affect the product price. Only platform-funded adjustments are neutralised at this stage.
VAT-aware commission adjustment
This is the piece of logic that is easy to get wrong, yet its importance is critical.
The customer's discount is a gross amount — the customer paid 5 PLN less. However, commission_line.value may be a net amount (when include_tax = false). If I subtracted 5 PLN gross from a net commission, the platform would give back more than it should.
Example at 23% VAT:
odjęcie 30 PLN od prowizji netto
= realna utrata 36,90 PLN prowizji bruttoThat is why the finalizer must distinguish between both cases:
export const calculatePlatformFundedCommissionAdjustment = ({
commissionBefore,
fundedDiscountAmount,
includeTax,
taxRate,
}) => {
const taxMultiplier = 1 + taxRate
if (includeTax) {
// commission_line.value jest brutto — odejmujemy 1:1
const appliedDiscount = Math.min(fundedDiscountAmount, commissionBefore)
return {
appliedDiscount,
commissionAfter: commissionBefore - appliedDiscount,
commissionReductionAmount: appliedDiscount,
}
}
// commission_line.value jest netto — przeliczamy rabat przez VAT
const commissionBeforeGross = commissionBefore * taxMultiplier
const appliedDiscount = Math.min(fundedDiscountAmount, commissionBeforeGross)
const commissionReductionAmount = appliedDiscount / taxMultiplier
return {
appliedDiscount,
commissionAfter: commissionBefore - commissionReductionAmount,
commissionReductionAmount,
}
}A real numeric example from a manual test:
commission net before: 40.00
commission gross before: 49.20 (VAT 23%)
platform-funded discount: 30.00 brutto
---
commission gross after: 19.20
commission net after: 15.61
VAT after: 3.59Audit before mutation
The finalizer first writes an audit record to platform_commission_adjustment, and only then updates the commission line. The order is intentional — if the update fails, you have a trace. If the audit fails, you have no mutation without a trace.
await commissionService.createPlatformCommissionAdjustments({
source_type: sourceType, // 'newsletter_signup' | 'loyalty_points'
order_id: order.id,
item_line_id: itemLineId,
target_commission_line_id: commissionLine.id,
discount_amount: appliedDiscount,
commission_before: commissionBefore,
commission_after: commissionAfter,
status,
idempotency_key: idempotencyKey,
metadata: {
commission_discount_mode: 'gross_discount_to_commission_value',
commission_reduction_amount: commissionReductionAmount,
commission_before_gross: commissionBeforeGross,
commission_after_gross: commissionAfterGross,
include_tax: includeTax,
tax_rate: taxRate,
},
})
await commissionService.updateCommissionLines({
id: commissionLine.id,
value: commissionAfter,
})Every commission adjustment has a clear audit trail: where the discount came from, what the commission was before and after, and which VAT rate was applied.
Idempotency and retry
The order lifecycle in Medusa is an environment where hooks can be re-executed. Every adjustment has a deterministic key:
const idempotencyKey =
`platform_commission_adjustment:${sourceType}:${order.id}:${itemLineId}:${idempotencyIdentity}`When the finalizer runs again I do not subtract the commission a second time. Instead I repair the commission line to the value recorded in the audit:
if (existingAdjustment) {
await commissionService.updateCommissionLines({
id: existingAdjustment.target_commission_line_id,
value: toPlatformAmountNumber(existingAdjustment.commission_after),
})
continue
}Retries and replays become safe. This is especially important because create-commission-lines can recreate commission lines on the order side — the finalizer must survive that without applying a double adjustment.
Loyalty — points stay in their own domain
Both mechanisms share the same commission finalizer, but loyalty has its own points logic completely isolated from commission. Each layer does its own job — and only its own job.
const commissionAdjustmentResult =
await applyPlatformFundedCommissionAdjustments({
orderId: order.id,
container,
sourceType: 'loyalty_points',
adjustmentCodes: ['LOYALTY_POINTS'],
metadataKey: 'loyalty',
})
// Punkty na podstawie faktycznie zastosowanego rabatu
const normalizedRedeemedPoints = resolveRedeemedPointsForAppliedDiscount({
appliedDiscount: commissionAdjustmentResult.appliedDiscountTotal,
configuredDiscountAmount,
configuredAppliedPoints,
pointsPerCurrencyUnit,
redeemablePoints,
})
await loyaltyService.createLoyaltyTransactionRecord({
customer_id: order.customer_id,
order_id: order.id,
type: 'redeem',
points: -normalizedRedeemedPoints,
amount: commissionAdjustmentResult.appliedDiscountTotal,
reason: 'order_placed_redeem',
idempotency_key: `loyalty_redeem_order_${order.id}`,
})Separation of concerns: the commission finalizer knows how to adjust the commission line. The loyalty module knows how to manage points and account balance. They are connected by the order ID and the audit record in platform_commission_adjustment.
Safety cap
Not every points amount should be redeemable. A platform-funded discount cannot exceed the platform's commission — otherwise the platform would be subsidising beyond its own margin, or the seller payout would become unstable.
When the discount is applied to the cart, the maximum commission for that order is estimated. This is the upper limit for the total platform-funded adjustments. If the customer wants to redeem more points than the safety cap allows — the system trims the discount, it does not error. Silently, deterministically, without an exception.
Newsletter: one-time guarantee via database constraint
The newsletter welcome code is a one-time entitlement per subscriber/email. Beyond application-level logic, the one-time guarantee is enforced at the database level — because application logic fails under race conditions:
CREATE UNIQUE INDEX IF NOT EXISTS "UQ_newsletter_welcome_discount_owner"
ON "newsletter_welcome_discount" ("subscriber_id", lower("email"))
WHERE "deleted_at" IS NULL;With concurrent requests (double-click, race condition) the database rejects the second insert. The service handles the conflict idempotently — it reads the existing record and treats the request as a repeat.
Full numeric example
Produkty: 400.00 PLN
Dostawa: 25.00 PLN
Loyalty discount: 30.00 PLN
Klient płaci: 395.00 PLN
Commission base: 400.00 PLN (add-back loyalty)
Commission net: 40.00 PLN
Commission gross: 49.20 PLN (VAT 23%)
Po korekcie loyalty:
Commission gross: 19.20 PLN
Commission net: 15.61 PLN
VAT: 3.59 PLN
Payout sprzedawcy:
370.00 - 19.20 + 25.00 = 375.80 PLNWithout the loyalty discount the seller would have received:
400.00 - 49.20 + 25.00 = 375.80 PLNIdentical. That is what a good abstraction should look like.
Summary
The standard discount in Medusa (and in virtually every other e-commerce framework) reduces item.total. In a marketplace that is not enough — someone has to pay for the difference, and by default that someone is the seller.
The solution is two-step: add-back to the commission base, then adjust the commission line after the commission has been calculated. On top of that:
- a single definition of platform-funded codes as the source of truth for checkout, commission, and payout,
- converting the gross discount through VAT when adjusting a net commission line,
- audit before mutation, idempotency key protecting against double adjustment on retry,
- safety cap ensuring the discount never exceeds the platform's commission,
- a unique database constraint for one-time entitlements.
This is the kind of implementation you do not get by installing a plugin.
I build custom solutions on Medusa.js — from module architecture through integrations to full marketplaces.


