以下情境为教学假设,人物与数字不代表经核实的真实案例。
返点最容易被当成一次性的折扣:卖得多,年底退一点。真正改变财务结果的是返点的计算方式:按全年进货额追溯,还是只对增量部分计算;区域保护有没有最低销量;最低售价条款是否把直销价格锁住。三个条款合起来,决定渠道的实际单价和冲突成本。
这一节继续用 8,500 美元的项目作为教学单位。返点档位 10%、15%、20%,触发线 100,000 美元和 250,000 美元,以及代理所需的 15% 毛利,都是教学假设,不是市场报价。它们的作用是让“返点怎么改变净得”变成可以复算的表,而不是一句“多卖多返”。
阶梯返点改变的是全年单价
先设三档返点:年进货额低于 100,000 美元返 10%,100,000 到 249,999 美元返 15%,250,000 美元及以上返 20%。返点按全年进货额追溯计算,年底一次性支付。
| 年进货额 | 返点 | 对应 8,500 美元项目的年销量 |
|---|---|---|
| 低于 $100,000 | 10% | 1–11 笔 |
| $100,000–$249,999 | 15% | 12–29 笔 |
| $250,000 及以上 | 20% | 30 笔及以上 |
把不同年销量代入,单笔净得会随着返点档位变化:
| 年销量 | 年进货额 | 返点 | 返点金额 | 净得 | 单笔净得 |
|---|---|---|---|---|---|
| 8 笔 | $68,000 | 10% | $6,800 | $61,200 | $7,650 |
| 11 笔 | $93,500 | 10% | $9,350 | $84,150 | $7,650 |
| 12 笔 | $102,000 | 15% | $15,300 | $86,700 | $7,225 |
| 20 笔 | $170,000 | 15% | $25,500 | $144,500 | $7,225 |
| 29 笔 | $246,500 | 15% | $36,975 | $209,525 | $7,225 |
| 30 笔 | $255,000 | 20% | $51,000 | $204,000 | $6,800 |
| 40 笔 | $340,000 | 20% | $68,000 | $272,000 | $6,800 |
单笔净得从 7,650 降到 7,225,再降到 6,800,看起来是“卖得越多、单价越低”。但总净得从 61,200 升到 144,500,再升到 272,000,销量增长带来的总收入远大于单价下降。返点的设计目的不是让单价好看,而是用更高的总返点换更大的进货量。
这里有一个关键口径:上表假设返点是追溯的,达到 15% 档后,全年每一笔都按 15% 计算。如果合同写的是“增量返点”,只有超过 100,000 美元的部分按 15%,单笔净得会不同,总返点也会低很多。两种写法在合同里可能只差一句话,财务结果差一整档。
| 返点写法 | 12 笔的返点 | 12 笔净得 | 与追溯返点的差额 |
|---|---|---|---|
| 追溯:全年按 15% | $15,300 | $86,700 | — |
| 增量:前 $100,000 按 10%,超出部分按 15% | $10,300 | $91,700 | +$5,000 |
增量返点对厂商更有利,因为它只为超出的 2,000 美元多付 5 个百分点;追溯返点对渠道更有利,因为达到档位后全年都涨价。谈判时争的不是“有没有返点”,而是“返点从哪一笔开始算”。
返点悬崖:第 30 笔为什么让总收入下降
追溯返点有一个反直觉的后果:跨档的那一笔,可能让全年总净得下降。
先看 11 笔到 12 笔。第 12 笔带来 8,500 美元新增进货额,返点从 10% 升到 15%,返点金额从 9,350 美元升到 15,300 美元,增加 5,950 美元。新增净得只有 2,550 美元,而不是 7,650 美元。第 12 笔本身是正贡献,但它的边际净得被前 11 笔的返点上调吃掉了大半。
再看 29 笔到 30 笔。第 30 笔带来 8,500 美元新增进货额,返点从 15% 升到 20%,返点金额从 36,975 美元升到 51,000 美元,增加 14,025 美元。新增净得是:
8,500 − 14,025 = −5,525 美元
第 30 笔让全年总净得减少 5,525 美元。这不是算术错误,而是追溯返点的结构性结果:第 30 笔不仅自己按 20% 返,还让前 29 笔一起从 15% 升到 20%。
| 跨越点 | 新增毛收入 | 新增返点 | 新增净得 | 判断 |
|---|---|---|---|---|
| 11 → 12 笔 | $8,500 | $5,950 | +$2,550 | 正贡献,但边际很低 |
| 29 → 30 笔 | $8,500 | $14,025 | −$5,525 | 负贡献,越卖越少 |
这张表说明,返点档位的触发线不能随便设在预期销量附近。如果渠道预计年销 30 笔,触发线正好设在 250,000 美元,那么第 30 笔会让渠道的总净得下降;渠道要么停在 29 笔,要么要求厂商补偿跨档损失。两种反应都不利于厂商。
更合理的做法有三种。第一,把返点改成增量制,只对超过触发线的部分给高返点,避免跨档回吐。第二,把触发线设在预期销量的下方,让渠道在达到预期销量前就已经进入下一档,跨档不是意外。第三,用平滑的返点曲线,比如每增加 50,000 美元加 1 个百分点,而不是 10%、15%、20% 三档跳变。
返点悬崖也会改变渠道的行为。如果渠道知道第 30 笔会让总净得下降,它可能把订单拆到下一年,或者把第 30 笔转到另一个品牌。厂商看到的是“销量没达标”,实际原因是返点结构在惩罚最后一笔。
区域保护:用独家换销量,底线在哪里
区域保护是另一条会改变单价的条款。厂商承诺在某个区域只通过一家经销商销售,经销商则承诺投入市场、备货和售后。保护的对价不是一笔固定费用,而是厂商放弃的直销收入。
用同一组数字算。假设某个区域一年能做 20 笔 8,500 美元的项目。直销净得 8,075 美元一笔,年净得 161,500 美元。经销商在 15% 返点下净得 7,225 美元一笔,20 笔的年净得是 144,500 美元。区域保护让厂商每年少赚 17,000 美元。
经销商要证明保护值得,必须把区域销量做上去。盈亏平衡点是:
161,500 ÷ 7,225 = 22.35 笔
取整是 23 笔。23 笔时经销净得 166,175 美元,超过直销 4,675 美元;22 笔时只有 158,950 美元,仍低于直销 2,550 美元。
| 区域年销量 | 经销净得 | 直销净得 | 差额 |
|---|---|---|---|
| 20 笔 | $144,500 | $161,500 | −$17,000 |
| 22 笔 | $158,950 | $161,500 | −$2,550 |
| 23 笔 | $166,175 | $161,500 | +$4,675 |
| 24 笔 | $173,400 | $161,500 | +$11,900 |
这张表把区域保护变成了一条可写进合同的绩效线:经销商年销量低于 23 笔,保护不成立;达到 23 笔,保护继续;连续两个周期低于 23 笔,保护转为非独家。没有绩效线的区域保护,等于厂商用 17,000 美元一年的代价,买了一个不确定的销量承诺。
区域保护还有两个容易漏掉的边界。第一,区域怎么定义。按地理画圈,可能把经销商服务不了的客户也锁进去;按客户名单或行业清单定义,更接近实际销售能力。第二,谁来履约。经销商拿到保护后,如果只在核心城市投入,周边客户仍然找不到人服务,厂商又不能直接卖,最后是市场空白和渠道抱怨同时存在。
区域保护不是不能给,而是要给得有条件。保护的范围、期限、最低销量、违约后的降级路径,四项都写清楚,保护才是一笔可计算的交易,而不是一句“这个区域归你”。
价格冲突与 MFN:500 美元差价如何赶走代理
渠道冲突最常见的触发点,是厂商自己的直销价格低于渠道的进货成本加合理毛利。代理不会因为“厂商卖得便宜”而退出,它会因为“按厂商的价格卖就亏钱”而退出。
继续用 20 笔、15% 返点的经销商场景。经销商的净进货成本是 7,225 美元一笔。如果它需要 15% 的毛利,最低售价是:
7,225 ÷ (1 − 15%) = 8,500 美元
8,500 美元正好是标价。也就是说,标价本身就是经销商 15% 毛利的底线。厂商任何低于 8,500 美元的直销价,都会压缩经销商的毛利。
| 直销价 | 经销商毛利 | 是否低于 15% 底线 | 可能的结果 |
|---|---|---|---|
| $8,500 | 15.0% | 否 | 相安无事 |
| $8,000 | 9.7% | 是 | 要求补差,或减少投入 |
| $7,200 | −0.3% | 是,且为负 | 停止进货,退出渠道 |
8,000 美元的直销价只给经销商留 9.7% 的毛利,低于它需要的 15%。经销商有三种反应:要求厂商补差、把资源转到别的品牌、或者直接退出。7,200 美元的直销价已经低于进货成本,经销商按这个价格卖一笔亏一笔,退出只是时间问题。
MFN(最低售价)条款的作用,是把价格冲突从“事后争吵”变成“事前算账”。常见的写法是:厂商给任何客户的价格,不得低于给渠道的净价;如果更低,厂商要向渠道补差。补差金额可以按成交笔数计算。
假设厂商直销卖了 10 笔,价格 8,000 美元,标价 8,500 美元,每笔差价 500 美元:
MFN 补差 = 10 × 500 = 5,000 美元
厂商的折扣损失是 10 × 500 = 5,000 美元,但支付费也随价格下降少付 250 美元,净收入实际减少 4,750 美元;再加上 5,000 美元 MFN 补差,合计减少 9,750 美元。如果没有新增销量,降价是净损失。如果低价带来 3 笔新增成交,每笔直销净得 7,600 美元,新增净得 22,800 美元;扣掉 4,750 美元折扣损失和 5,000 美元补差,净增益是 13,050 美元。
| 情景 | 直销净得 | MFN 补差 | 合计 | 相对 10 笔原价 |
|---|---|---|---|---|
| 10 笔原价 | $80,750 | $0 | $80,750 | — |
| 10 笔降价,无新增 | $76,000 | $5,000 | $71,000 | −$9,750 |
| 10 笔降价,新增 3 笔 | $98,800 | $5,000 | $93,800 | +$13,050 |
真正昂贵的不是补差,而是代理退出后的收入缺口。假设经销商一年做 20 笔,厂商净得 7,225 美元一笔,年净得 144,500 美元。经销商退出后,直销只能接回 60% 的客户,即 12 笔,每笔净得 7,600 美元,接回部分的净得是 91,200 美元。第一年的净损失是:
144,500 − 91,200 = 53,300 美元
一笔 500 美元的降价,可能换来 53,300 美元的渠道损失。这个数字还没有算过渡期的销售空档、重新招聘和合同纠纷成本。
| 直销价 | 经销商毛利 | 10 笔 MFN 补差 | 代理退出后第一年净损失 |
|---|---|---|---|
| $8,500 | 15.0% | $0 | $0 |
| $8,000 | 9.7% | $5,000 | $53,300 |
| $7,200 | −0.3% | $13,000 | $62,420 |
7,200 美元一行的净损失是 62,420 美元:直销净得 6,840 美元一笔,接回 12 笔是 82,080 美元,比原来的 144,500 美元少 62,420 美元。价格越低,接回的每笔净得越少,损失越大。
这张表说明,直销低价不是“多卖一点”的问题,而是“渠道还愿不愿意继续卖”的问题。降价前要先算三笔账:折扣总额、MFN 补差、渠道退出后的收入缺口。三笔账里最大的通常不是前两笔,而是第三笔。
把返点与冲突写成代码
下面的 rebate.ts 把三档返点、价格底线、MFN 补差和代理退出损失固化成可复算的函数。rebateRate 选档,rebateOn 算返点金额,volumeNet 算不同年销量下的单笔净得,minimumResalePrice 算渠道的最低售价,marginAtPrice 算渠道在某个直销价下的毛利,mfnMakeWhole 算补差,agentExitLoss 算代理退出后的第一年净损失。
// file: src/contract/rebate.ts
// tier: L1
// verify: node --test test/contract/rebate.test.ts
import { type Cents } from "../core/money.ts";
import { type Bps, applyRate, divide } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";
export type RebateTier = {
minAnnualCents: Cents;
rateBps: Bps;
};
/** Teaching assumption: the rebate is retroactive on the full annual purchase. */
export const REBATE_TIERS: RebateTier[] = [
{ minAnnualCents: 0, rateBps: 1000 },
{ minAnnualCents: 10_000_000, rateBps: 1500 },
{ minAnnualCents: 25_000_000, rateBps: 2000 },
];
export type RebateResult = {
annualPurchaseCents: Cents;
rateBps: Bps;
rebateCents: Cents;
netCents: Cents;
};
export type VolumeNet = RebateResult & {
deals: number;
unitNetCents: Cents;
};
export type ExitLossInput = {
annualDeals: number;
agentNetPerDealCents: Cents;
directNetPerDealCents: Cents;
recaptureRateBps: Bps;
};
export type ExitLoss = {
lostAgentNetCents: Cents;
recapturedDeals: number;
recapturedNetCents: Cents;
netLossCents: Cents;
};
function checkAmount(value: Cents, label: string): string | null {
if (!Number.isInteger(value) || value < 0) {
return `${label} must be a non-negative integer amount in cents`;
}
return null;
}
function checkBps(value: Bps, label: string): string | null {
if (!Number.isInteger(value) || value < 0 || value > 10000) {
return `${label} must be between 0 and 10000 bps`;
}
return null;
}
export function rebateRate(annualPurchaseCents: Cents): Result<Bps> {
const error = checkAmount(annualPurchaseCents, "annual purchase");
if (error) {
return fail(error);
}
let selected = REBATE_TIERS[0].rateBps;
for (const tier of REBATE_TIERS) {
if (annualPurchaseCents >= tier.minAnnualCents) {
selected = tier.rateBps;
}
}
return ok(selected);
}
export function rebateOn(annualPurchaseCents: Cents): Result<RebateResult> {
const rate = rebateRate(annualPurchaseCents);
if (!rate.ok) {
return rate;
}
const rebateCents = applyRate(annualPurchaseCents, rate.value);
return ok({
annualPurchaseCents,
rateBps: rate.value,
rebateCents,
netCents: annualPurchaseCents - rebateCents,
});
}
export function volumeNet(
grossPerDealCents: Cents,
deals: number,
): Result<VolumeNet> {
if (!Number.isInteger(grossPerDealCents) || grossPerDealCents <= 0) {
return fail("gross per deal must be a positive integer amount in cents");
}
if (!Number.isInteger(deals) || deals <= 0) {
return fail("deals must be a positive integer");
}
const annualPurchaseCents = grossPerDealCents * deals;
const rebate = rebateOn(annualPurchaseCents);
if (!rebate.ok) {
return rebate;
}
return ok({
...rebate.value,
deals,
unitNetCents: Math.round(rebate.value.netCents / deals),
});
}
/**
* The resale price at which the channel keeps exactly the required margin
* after the rebate. A direct price below this line undercuts the channel.
*/
export function minimumResalePrice(
netCostCents: Cents,
requiredMarginBps: Bps,
): Result<Cents> {
const costError = checkAmount(netCostCents, "net cost");
if (costError) {
return fail(costError);
}
if (netCostCents === 0) {
return fail("net cost must be positive");
}
if (
!Number.isInteger(requiredMarginBps) ||
requiredMarginBps < 0 ||
requiredMarginBps >= 10000
) {
return fail("required margin must be between 0 and 9999 bps");
}
return ok(
Math.ceil((netCostCents * 10000) / (10000 - requiredMarginBps)),
);
}
export function marginAtPrice(
netCostCents: Cents,
resalePriceCents: Cents,
): Result<Bps> {
const costError = checkAmount(netCostCents, "net cost");
if (costError) {
return fail(costError);
}
if (!Number.isInteger(resalePriceCents) || resalePriceCents <= 0) {
return fail("resale price must be a positive integer amount in cents");
}
return ok(divide(resalePriceCents - netCostCents, resalePriceCents));
}
/** A most-favored-nation clause makes the vendor pay back the price gap. */
export function mfnMakeWhole(
listPriceCents: Cents,
directPriceCents: Cents,
deals: number,
): Result<Cents> {
for (const [value, label] of [
[listPriceCents, "list price"],
[directPriceCents, "direct price"],
] as Array<[Cents, string]>) {
const error = checkAmount(value, label);
if (error) {
return fail(error);
}
}
if (!Number.isInteger(deals) || deals <= 0) {
return fail("deals must be a positive integer");
}
const gap = listPriceCents - directPriceCents;
return ok(gap > 0 ? gap * deals : 0);
}
export function agentExitLoss(input: ExitLossInput): Result<ExitLoss> {
if (!Number.isInteger(input.annualDeals) || input.annualDeals <= 0) {
return fail("annual deals must be a positive integer");
}
for (const [value, label] of [
[input.agentNetPerDealCents, "agent net per deal"],
[input.directNetPerDealCents, "direct net per deal"],
] as Array<[Cents, string]>) {
const error = checkAmount(value, label);
if (error) {
return fail(error);
}
}
const rateError = checkBps(input.recaptureRateBps, "recapture rate");
if (rateError) {
return fail(rateError);
}
const lostAgentNetCents =
input.annualDeals * input.agentNetPerDealCents;
const recapturedDeals = Math.round(
(input.annualDeals * input.recaptureRateBps) / 10000,
);
const recapturedNetCents =
recapturedDeals * input.directNetPerDealCents;
return ok({
lostAgentNetCents,
recapturedDeals,
recapturedNetCents,
netLossCents: lostAgentNetCents - recapturedNetCents,
});
}
把 8,500 美元代入,volumeNet(850_000, 8) 返回单笔净得 765,000 美分,即 7,650 美元;20 笔返回 722,500 美分,即 7,225 美元;30 笔返回 680,000 美分,即 6,800 美元。29 笔到 30 笔的总净得变化是 −552,500 美分,即 −5,525 美元。minimumResalePrice(722_500, 1500) 返回 850,000 美分,即 8,500 美元;marginAtPrice(722_500, 800_000) 返回 969 基点,即 9.69%;mfnMakeWhole(850_000, 800_000, 10) 返回 500,000 美分,即 5,000 美元;agentExitLoss 返回 5,330,000 美分,即 53,300 美元。
代码里有三个刻意的限制。第一,返点按全年进货额追溯计算,不处理增量返点;要改口径,只需把 rebateOn 的分段逻辑换掉。第二,价格底线假设渠道要求固定毛利,不处理渠道为了清库存自愿降价的情况。第三,代理退出损失只算第一年的收入缺口,不含过渡期、法律费用和客户流失的长期影响;这些在 9.3 节展开。
返点和区域保护是渠道合同的“价格层”,价格冲突和退出是“执行层”。价格层写得再细,如果执行层没有退出条款,厂商仍然会在渠道退出时付出未计算的成本。下一节把退出条款拆成库存回购、未结佣金、客户归属和竞业限制四笔账。
小结
- 追溯返点改变的是全年单价,不是单笔折扣;同样 20 笔,15% 返点下每笔净得 7,225 美元。
- 8 笔净得 7,650 美元一笔,40 笔净得 6,800 美元一笔;销量增长带来的总净得远大于单价下降。
- 11 笔到 12 笔,新增一笔只增加 2,550 美元净得;29 笔到 30 笔,新增一笔让总净得减少 5,525 美元。
- 返点悬崖来自追溯制:跨档那笔让全年返点一起上调;增量返点或平滑曲线可以避免。
- 区域保护的盈亏平衡点是 23 笔;低于 23 笔,直销的 161,500 美元仍高于经销的 144,500 美元。
- 经销商 15% 返点后的进货成本是 7,225 美元;要留 15% 毛利,最低售价正好是 8,500 美元。
- 8,000 美元直销价只给经销商 9.7% 毛利,7,200 美元已经是负毛利;代理退出不是情绪,是算术。
- 10 笔 8,000 美元直销的 MFN 补差是 5,000 美元;代理退出后的第一年净损失是 53,300 美元。
阅读导航:上一节:9.1 直销、代理、经销的选择 · 下一节:9.3 渠道合同与退出 。
常见错误与边界
- 把返点当成一次性折扣。 返点按全年进货额追溯,会改变每一笔的实际单价;只按单笔算,会低估返点对总净得的影响。
- 忽略追溯与增量的区别。 追溯返点在跨档时让全年返点一起上调,增量返点只对超出部分上调;同一档位,两种写法的返点金额可能差一半。
- 把触发线设在预期销量附近。 第 30 笔跨入 20% 档时,总净得反而减少 5,525 美元;触发线要设在预期销量下方,或用平滑曲线。
- 用单笔净得判断渠道价值。 单笔净得低,但销量可能高;要比较总净得,而不是只比较单价。
- 区域保护不写绩效线。 没有最低销量的独家保护,等于厂商用 17,000 美元一年的代价买一个不确定的承诺。
- 区域定义只按地理画圈。 经销商服务不了的客户也被锁进保护范围,厂商不能直接卖,市场出现空白。
- 把 15% 毛利当成渠道的“合理利润”。 15% 是本节教学假设;真实渠道的毛利要求受品类、周转、售后和资金成本影响,不能直接套用。
- 只算折扣,不算 MFN 补差。 10 笔 8,000 美元直销的补差是 5,000 美元;如果只算折扣,会漏掉这笔现金支出。
- 只算补差,不算代理退出损失。 代理退出后的第一年净损失是 53,300 美元,远大于 5,000 美元补差;最大的成本是收入缺口,不是补差。
- 把价格冲突当成销售部门的问题。 价格冲突的根源是渠道毛利结构;不改返点、区域保护或 MFN 条款,只靠沟通解决不了。
本节的边界是“单一渠道、固定毛利要求、追溯返点”的静态模型。它假设渠道年销量可以按档位选择,不含渠道的库存周转、资金成本、灰色市场和跨区窜货,也不含不同品类之间的返点差异。它足够回答“返点怎么改变净得、区域保护的最低销量是多少、降价会不会赶走代理”,但不足以回答“渠道退出时库存和客户怎么处理”。后者需要把退出条款逐项拆开,见 9.3 渠道合同与退出 。折扣对收入确认的影响,见 6.2 年付折扣与月付 。本节不构成法律意见,具体返点与区域条款请由律师审阅。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。