以下情境为教学假设,人物与数字不代表经核实的真实案例。
供应商合同最容易看错的地方,是把“月费”当成“退出成本”。月费只回答用的时候付多少,退出成本回答走的时候还要付多少。两者可能差一倍以上:一份 12 个月、月最低 2,000 美元的云合同,年合同额是 24,000 美元;第 6 个月退出,剩余承诺加罚金、导出和过渡,合计 19,100 美元,相当于年合同额的 79.6%。
退出条款不是一条,而是一组。最低承诺决定你还要付几个月,罚金决定提前走要加多少,数据导出权决定你能不能把数据带走,过渡期支持决定切换时有没有人帮你,通知期决定你最后还要付多久。五项条款各自产生一笔成本,合起来才是退出价。这一节不写法律意见,只把这五项换算成美元。
决定退出的五类条款
先看五项条款在合同里长什么样,以及退出时各变成什么成本。下表的分母是年合同额,金额示例沿用后面的云合同。
| 条款 | 合同里常见写法 | 退出时变成什么成本 | 要问清楚的一个数 |
|---|---|---|---|
| 最低承诺消费 | 每月最低 2,000 美元,承诺 12 个月 | 剩余月份 × 月最低 | 还剩几个月 |
| 提前解约罚金 | 剩余承诺的 30% | 剩余承诺 × 罚金比例 | 罚金按剩余承诺还是年合同额算 |
| 数据导出权 | 可导出、指定格式、含附件 | 导出工时 + 校验 + 重建成本 | 导出是否含历史版本和附件 |
| 过渡期支持 | 终止后 2 个月并行运行 | 过渡月费 × 月数 | 过渡期是否另收费 |
| 通知期 | 提前 60 天书面通知 | 通知期超出剩余承诺的部分 | 通知期是否覆盖剩余月份 |
最低承诺是最容易被低估的一项。它不是“用多少付多少”的保底价,而是“不用也要付”的合同义务。LedgerLite 如果签了年最低 24,000 美元、实际年用量只有 18,000 美元,差额 6,000 美元仍然要付;这笔钱和提前解约罚金是两笔账,不能互相抵消。
罚金的基数要写清楚。按“剩余承诺”算是 6 个月 × 2,000 = 12,000 美元的 30%,即 3,600 美元;按“年合同额”算是 24,000 美元的 30%,即 7,200 美元。同样的 30%,基数不同,罚金差一倍。合同里只写“30%”不写基数,执行时就会各算各的。
数据导出权和过渡期支持是“反向条款”:它们不产生退出成本,而是降低退出成本。没有导出权,数据要么重新录入,要么花钱请供应商做一次性导出;没有过渡期支持,新旧系统要并行更久,旧账单和新账单同时付。评估合同时,不能只看罚金,要把这两项省下的钱一起算。
通知期是最后一段现金流出。第 6 个月退出时,剩余承诺 6 个月已经覆盖 60 天通知,通知期不额外加钱;第 12 个月退出时,剩余承诺是 0,但 60 天通知仍要付 2 个月,即 4,000 美元。通知期不是“提前打招呼”的形式,它是合同最后一笔月费。
Nord Studio 的咨询合同没有最低承诺,客户按 Net 30 付款;它的退出成本不是罚金,而是通知期内空出的工时和未结应收。这与云合同的最低承诺是两种不同的锁定:一个锁现金,一个锁产能。两者都要算,但算法不同。
把云合同拆成退出成本
设一份云合同:月最低 2,000 美元,承诺 12 个月,年合同额 24,000 美元;提前解约罚金是剩余承诺的 30%;通知期 60 天;数据导出一次性 1,500 美元;过渡期支持 2 个月,每月 1,000 美元。所有费率都是教学假设,不是供应商报价。
第 6 个月退出,剩余 6 个月:
剩余承诺 = 2,000 × 6 = 12,000 美元
罚金 = 12,000 × 30% = 3,600 美元
通知期补足 = max(0, 60 − 6 × 30) ÷ 30 × 2,000 = 0 美元
数据导出 = 1,500 美元
过渡支持 = 1,000 × 2 = 2,000 美元
总退出成本 = 12,000 + 3,600 + 0 + 1,500 + 2,000 = 19,100 美元
19,100 美元是年合同额 24,000 美元的 79.6%。也就是说,用了半年再走,退出价接近一年的合同额。这个数字比“罚金 3,600 美元”更接近真实:罚金只是五项里的一项,剩余承诺才是最大的一块。
| 退出时点 | 剩余承诺 | 罚金 30% | 通知期补足 | 数据导出 | 过渡支持 | 总退出成本 | 占年合同额 |
|---|---|---|---|---|---|---|---|
| 第 6 个月 | $12,000 | $3,600 | $0 | $1,500 | $2,000 | $19,100 | 79.6% |
| 第 9 个月 | $6,000 | $1,800 | $0 | $1,500 | $2,000 | $11,300 | 47.1% |
| 第 12 个月 | $0 | $0 | $4,000 | $1,500 | $2,000 | $7,500 | 31.3% |
这张表说明两件事。第一,退出成本随时点下降,但不会降到零:第 12 个月仍有通知期 4,000 美元、导出 1,500 美元、过渡 2,000 美元,合计 7,500 美元。第二,第 6 个月到第 9 个月,每多用一个季度,退出成本减少 7,800 美元;这 7,800 美元就是“提前走”的价格。如果这项云服务未来一个季度带来的价值低于 7,800 美元,现在走反而划算;如果高于,撑到季末更便宜。
最低承诺还有一笔“沉默成本”:年最低 24,000 美元,实际年用量 18,000 美元,差额 6,000 美元要付。它不体现在退出成本里,但会体现在年度账单里。评估合同时要把两笔账分开:一笔是“用不满的差额”,一笔是“提前走的罚金”。前者每年都可能发生,后者只在退出时发生。
如果合同写的是“年最低 24,000 美元,按季度考核”,差额还会按季度结算。Q1 用了 3,000 美元,季度最低 6,000 美元,差额 3,000 美元当季就要付;不能等到年底用 Q4 的超额用量去补。这类条款把“全年最低”拆成“每季最低”,现金压力更早出现,退出时的剩余承诺也要按季度余额重算。签约前要问清楚:最低承诺是按年考核、按季考核,还是按月考核。
数据导出与过渡期:最容易被低估的两项
数据导出权听起来像“能不能拿走数据”,实际是“拿走要花多少钱”。有导出权,供应商提供指定格式的导出,成本主要是内部校验和迁移工时;没有导出权,只能重新录入、从报表反推,或者请供应商做一次性导出,后者的报价通常按工时算。
设一次完整导出的内部工时是 80 小时,内部费率 150 美元/小时,重建和校验成本 12,000 美元;有导出权时,供应商收 1,500 美元导出费;没有导出权时,只能按 80 小时重建。差额是 10,500 美元。导出后的内部校验另计,不计入供应商账单。这个差额不是罚金,但它是退出成本的一部分。
| 条款 | 有条款时你付什么 | 没有条款时你付什么 | 差额 |
|---|---|---|---|
| 数据导出 | $1,500 导出费 | 80 小时重建 = $12,000 | 省 $10,500 |
| 过渡期支持 | 2 个月 × $1,000 = $2,000 | 3 个月并行 + 加班 = $6,000 | 省 $4,000 |
| 通知期 | 第 6 个月 $0;第 12 个月 $4,000 | 随时停,$0 | 贵 $0–4,000 |
把五项条款放在一起看,会发现“有合同”不一定比“没合同”贵。第 6 个月退出,有合同的总成本是 19,100 美元;没有最低承诺、没有罚金,但也没有导出权和过渡支持,成本是 12,000 美元重建 + 6,000 美元并行 = 18,000 美元。两者只差 1,100 美元。合同的剩余承诺和罚金多付了 15,600 美元,但导出权和过渡支持省了 14,500 美元,净差只有 1,100 美元。
这个比较不是替最低承诺辩护,而是说明退出条款是一个包。只盯着罚金,会得出“有合同更贵”的结论;只盯着导出权,会得出“有合同更便宜”的结论。两个都不完整。正确的做法是把五项条款逐项列成现金流,再和“无合同但自己重建”的基线比。
过渡期支持还有一个隐性成本:并行运行期间,团队要同时维护旧系统和新系统,工时不是免费的。2 个月并行按每月 1,000 美元的额外支持费算,是 2,000 美元;如果内部还要投入 40 小时/月,按 150 美元/小时算,又是 12,000 美元。这部分不在供应商账单里,但在退出预算里。评估过渡期条款时,要问“支持费含不含迁移顾问的工时”,而不是只看月费。
把退出条款写成代码
下面的 vendor-exit.ts 把五项条款拆成字段:remainingCommitmentCents 是最低承诺的剩余部分,noticeTopUpCents 是通知期超出剩余承诺的补足,penaltyCents 是提前解约罚金,dataExportCents 和 transitionSupportCents 是导出与过渡,最后汇总成 totalExitCents 和占年合同额的 exitCostBps。commitmentShortfall 单独算“用不满最低承诺”的差额。
// file: src/contract/vendor-exit.ts
// tier: L1
// verify: node --test test/contract/vendor-exit.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 VendorExitInput = {
monthlyMinimumCents: Cents;
commitMonths: number;
elapsedMonths: number;
noticeDays: number;
penaltyBps: Bps;
dataExportCents: Cents;
transitionMonths: number;
transitionMonthlyCents: Cents;
};
export type VendorExitCost = {
remainingMonths: number;
remainingCommitmentCents: Cents;
noticeTopUpCents: Cents;
penaltyCents: Cents;
dataExportCents: Cents;
transitionSupportCents: Cents;
totalExitCents: Cents;
annualContractValueCents: Cents;
exitCostBps: Bps;
};
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;
}
/**
* The minimum commitment is not only a pricing floor; it is the largest single
* exit cost. The remaining months are paid whether or not the service is used.
* A notice period only adds cost when it runs past the remaining commitment,
* because the same monthly minimum cannot be charged twice.
*/
export function vendorExitCost(
input: VendorExitInput,
): Result<VendorExitCost> {
const minimumError = checkAmount(
input.monthlyMinimumCents,
"monthly minimum",
);
if (minimumError) {
return fail(minimumError);
}
if (input.monthlyMinimumCents === 0) {
return fail("monthly minimum must be positive");
}
if (!Number.isInteger(input.commitMonths) || input.commitMonths <= 0) {
return fail("commit months must be a positive integer");
}
if (
!Number.isInteger(input.elapsedMonths) ||
input.elapsedMonths < 0 ||
input.elapsedMonths > input.commitMonths
) {
return fail("elapsed months must fit inside the commitment");
}
if (!Number.isInteger(input.noticeDays) || input.noticeDays < 0) {
return fail("notice days must be a non-negative integer");
}
const penaltyError = checkBps(input.penaltyBps, "early termination penalty");
if (penaltyError) {
return fail(penaltyError);
}
for (const [value, label] of [
[input.dataExportCents, "data export"],
[input.transitionMonthlyCents, "transition support"],
] as Array<[Cents, string]>) {
const error = checkAmount(value, label);
if (error) {
return fail(error);
}
}
if (
!Number.isInteger(input.transitionMonths) ||
input.transitionMonths < 0
) {
return fail("transition months must be a non-negative integer");
}
const remainingMonths = input.commitMonths - input.elapsedMonths;
const remainingCommitmentCents =
input.monthlyMinimumCents * remainingMonths;
const extraNoticeDays = Math.max(
0,
input.noticeDays - remainingMonths * 30,
);
const noticeTopUpCents = Math.round(
(input.monthlyMinimumCents * extraNoticeDays) / 30,
);
const penaltyCents = applyRate(remainingCommitmentCents, input.penaltyBps);
const transitionSupportCents =
input.transitionMonthlyCents * input.transitionMonths;
const totalExitCents =
remainingCommitmentCents +
noticeTopUpCents +
penaltyCents +
input.dataExportCents +
transitionSupportCents;
const annualContractValueCents =
input.monthlyMinimumCents * input.commitMonths;
return ok({
remainingMonths,
remainingCommitmentCents,
noticeTopUpCents,
penaltyCents,
dataExportCents: input.dataExportCents,
transitionSupportCents,
totalExitCents,
annualContractValueCents,
exitCostBps: divide(totalExitCents, annualContractValueCents),
});
}
/**
* A minimum commitment also creates a cost when the year is not over: if the
* actual spend is below the annual minimum, the shortfall is still payable.
* This is the quiet part of the clause, separate from the exit penalty.
*/
export function commitmentShortfall(
annualMinimumCents: Cents,
actualAnnualSpendCents: Cents,
): Result<Cents> {
const minimumError = checkAmount(annualMinimumCents, "annual minimum");
if (minimumError) {
return fail(minimumError);
}
const actualError = checkAmount(
actualAnnualSpendCents,
"actual annual spend",
);
if (actualError) {
return fail(actualError);
}
return ok(Math.max(0, annualMinimumCents - actualAnnualSpendCents));
}
把云合同代入:月最低 200,000 美分、承诺 12 个月、第 6 个月退出、通知 60 天、罚金 3,000 基点、导出 150,000 美分、过渡 2 个月 × 100,000 美分,返回剩余承诺 1,200,000 美分、罚金 360,000 美分、通知补足 0、总退出 1,910,000 美分、占年合同额 7,958 基点。第 9 个月退出,总退出 1,130,000 美分、4,708 基点;第 12 个月退出,总退出 750,000 美分、3,125 基点。commitmentShortfall(2,400,000, 1,800,000) 返回 600,000 美分,即用不满最低承诺的差额 6,000 美元。
代码里有三个刻意的选择。第一,通知期只在超出剩余承诺时补足,避免把同一段月费算两次;如果合同写的是“通知期与剩余承诺同时支付”,把 Math.max 改成加法即可。第二,罚金按剩余承诺算,不按年合同额算;基数不同,结果会差一倍,签约时要看清。第三,导出和过渡是加法,不随退出时点变化;如果合同规定“提前退出不提供过渡支持”,把 transitionSupportCents 置零,再看总成本。
退出成本算完,才能回答“要不要现在走”。如果留下一个季度的服务价值高于 7,800 美元的退出成本下降,撑到季末更划算;如果低于,早走的罚金反而更便宜。最低承诺的差额、导出权和过渡期支持,都是这个比较里的变量,不是合同里的背景文字。
小结
- 退出成本是五项条款的加法:最低承诺剩余、提前解约罚金、数据导出、过渡期支持、通知期补足;只看罚金会低估一半以上。
- 12 个月、月最低 2,000 美元的云合同,第 6 个月退出总成本 19,100 美元,占年合同额 79.6%;第 9 个月 11,300 美元,第 12 个月 7,500 美元。
- 罚金基数是关键:按剩余承诺 30% 是 3,600 美元,按年合同额 30% 是 7,200 美元;合同只写“30%”不写基数,执行时会有争议。
- 最低承诺差额是另一笔账:年最低 24,000 美元、实际用量 18,000 美元,差额 6,000 美元即使不退出也要付。
- 数据导出权价值约 10,500 美元:有标准导出是 1,500 美元,没有导出、重建 80 小时是 12,000 美元。
- 过渡期支持价值约 4,000 美元:有支持是 2,000 美元,没有支持、并行 3 个月是 6,000 美元。
- 通知期在第 12 个月仍要付 4,000 美元;它不是形式,是合同最后一笔月费。
- 有合同第 6 个月退出 19,100 美元,无合同但自己重建 18,000 美元,净差只有 1,100 美元;退出条款要作为一个包来比较。
阅读导航:上一节:10.1 云、支付、平台的关键依赖 · 下一节:10.3 备选方案与迁移成本 。
常见错误与边界
- 把月费当退出成本。 月费只回答用的时候付多少;第 6 个月退出的总成本是 19,100 美元,不是 2,000 美元。
- 只看罚金,不看剩余承诺。 罚金 3,600 美元只是一项,剩余承诺 12,000 美元才是最大的一块;两项合计 15,600 美元。
- 罚金基数没写清。 按剩余承诺 30% 是 3,600 美元,按年合同额 30% 是 7,200 美元;签约时不写基数,退出时就会各算各的。
- 把最低承诺差额和罚金混在一起。 用不满最低承诺的 6,000 美元是年度账单差额,不是提前解约罚金;两笔账要分开。
- 忽略通知期。 第 12 个月退出,剩余承诺是 0,但 60 天通知仍要付 4,000 美元;通知期是最后一段现金流出。
- 把数据导出权当成“有就行”。 有导出权还要看格式、附件、历史版本和校验工时;标准导出 1,500 美元,重建 12,000 美元,差 10,500 美元。
- 忽略过渡期的内部工时。 供应商支持费 2,000 美元之外,内部并行 40 小时/月 × 2 个月 × 150 美元 = 12,000 美元,这部分不在账单里。
- 用“有合同 vs 没合同”只比罚金。 有合同第 6 个月退出 19,100 美元,无合同重建 18,000 美元;只比罚金会漏掉导出和过渡省下的 14,500 美元。
- 假设最低承诺按年考核。 按季考核时,Q1 用不满的差额当季就要付,现金压力更早;退出时的剩余承诺也要按季度余额重算。
- 把退出成本当成一次性支出。 退出后还有替代供应商的迁移、并行和客户沟通成本;本节只算旧合同侧,替代侧要放进 10.3 的迁移成本。
本节的边界是“单一供应商、固定月最低、线性通知期”的静态模型。它假设月最低不变、罚金按剩余承诺、导出和过渡按合同执行,不含最低承诺的用量抵扣、不含罚金上限、不含争议期间的付款暂停、不含汇率和税务预提。它足够回答“现在走要付多少、撑到什么时候更便宜”,但不足以回答“供应商违约时能不能少付”。后者要把违约、责任上限和争议解决放在一起看,见 2.2 期限、自动续约与退出窗口 ;渠道合同的退出逻辑见 9.3 渠道合同与退出 。本节不构成法律意见,具体条款请由律师审阅。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。