《独立开发者的商业指南·合同、应收与风险》10.2 供应商合同与退出条款

退出成本不是一句“提前解约要付罚金”,而是最低承诺、罚金、数据导出、过渡期和通知期五项条款的加法。本节用一份 12 个月、月最低 2,000 美元的云合同做示例,算清第 6、9、12 个月退出分别要付 19,100、11,300、7,500 美元,并拆出最低承诺差额与数据导出权各自值多少钱。

以下情境为教学假设,人物与数字不代表经核实的真实案例。

供应商合同最容易看错的地方,是把“月费”当成“退出成本”。月费只回答用的时候付多少,退出成本回答走的时候还要付多少。两者可能差一倍以上:一份 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,10079.6%
第 9 个月$6,000$1,800$0$1,500$2,000$11,30047.1%
第 12 个月$0$0$4,000$1,500$2,000$7,50031.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,0003 个月并行 + 加班 = $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 备选方案与迁移成本 。

常见错误与边界

  1. 把月费当退出成本。 月费只回答用的时候付多少;第 6 个月退出的总成本是 19,100 美元,不是 2,000 美元。
  2. 只看罚金,不看剩余承诺。 罚金 3,600 美元只是一项,剩余承诺 12,000 美元才是最大的一块;两项合计 15,600 美元。
  3. 罚金基数没写清。 按剩余承诺 30% 是 3,600 美元,按年合同额 30% 是 7,200 美元;签约时不写基数,退出时就会各算各的。
  4. 把最低承诺差额和罚金混在一起。 用不满最低承诺的 6,000 美元是年度账单差额,不是提前解约罚金;两笔账要分开。
  5. 忽略通知期。 第 12 个月退出,剩余承诺是 0,但 60 天通知仍要付 4,000 美元;通知期是最后一段现金流出。
  6. 把数据导出权当成“有就行”。 有导出权还要看格式、附件、历史版本和校验工时;标准导出 1,500 美元,重建 12,000 美元,差 10,500 美元。
  7. 忽略过渡期的内部工时。 供应商支持费 2,000 美元之外,内部并行 40 小时/月 × 2 个月 × 150 美元 = 12,000 美元,这部分不在账单里。
  8. 用“有合同 vs 没合同”只比罚金。 有合同第 6 个月退出 19,100 美元,无合同重建 18,000 美元;只比罚金会漏掉导出和过渡省下的 14,500 美元。
  9. 假设最低承诺按年考核。 按季考核时,Q1 用不满的差额当季就要付,现金压力更早;退出时的剩余承诺也要按季度余额重算。
  10. 把退出成本当成一次性支出。 退出后还有替代供应商的迁移、并行和客户沟通成本;本节只算旧合同侧,替代侧要放进 10.3 的迁移成本。

本节的边界是“单一供应商、固定月最低、线性通知期”的静态模型。它假设月最低不变、罚金按剩余承诺、导出和过渡按合同执行,不含最低承诺的用量抵扣、不含罚金上限、不含争议期间的付款暂停、不含汇率和税务预提。它足够回答“现在走要付多少、撑到什么时候更便宜”,但不足以回答“供应商违约时能不能少付”。后者要把违约、责任上限和争议解决放在一起看,见 2.2 期限、自动续约与退出窗口 ;渠道合同的退出逻辑见 9.3 渠道合同与退出 。本节不构成法律意见,具体条款请由律师审阅。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「books」更多文章

  1. 《移动游戏产品的架构·商业化与发行工程》附录 C 商业化与发行评审清单
  2. 《移动游戏产品的架构·商业化与发行工程》附录 B 双案例商业化事实表
  3. 《移动游戏产品的架构·商业化与发行工程》附录 A 商业化与发行术语速查