《独立开发者的商业指南·合同、应收与风险》3.3 开票、对账与催收节奏

现金到账不是到期日那一下,而是一条由开票、对账、提醒和升级组成的分布。本节用 LedgerLite 的 Net 15 与 Nord Studio 的 Net 30,把催收节点映射成到账概率,算出有节奏与无节奏之间的 6.28 天和 6.58 天差,以及对应的现金释放与资金成本。

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

3.2 节的账龄表告诉你应收有多老,但没有告诉你这些钱什么时候能回来。一笔 31–60 天的应收,可能在第 45 天到账,也可能拖到第 120 天;两者在账龄表上属于同一档,在现金流上差了两个多月。要回答“什么时候到账”,需要把开票、对账、提醒和升级拆成一条时间线,再给每个节点一个概率。

这一节的核心不是催收话术,而是把催收节奏折算成期望到账时间。LedgerLite 的账期是 Net 15,逾期 30 天以上占 6%;Nord Studio 是 Net 30,逾期占 8%;Tidecast 面对平台月结 30 天,应收为 0。三个案例的催收对象不同,但计算方法是同一个:先把回款写成分布,再把每个节点的动作折算成分布的变化。

到期日只是一个点,到账是一条分布

一张发票的时间线至少有四个日期:开票日、到期日、实际付款日、现金可用日。合同只约定到期日,现金却由实际付款日决定。把实际付款日画成分布,通常会看到大多数客户在到期日附近付款,少数客户拖到 +7、+30、+60,极少数进入 90 天以上。

用概率描述这条分布,期望到账天数可以写成:

期望到账天数 = 账期 + Σ(节点偏移天数 × 该节点概率)

LedgerLite 的 Net 15 用一组教学曲线表示:83% 在到期日付款,11% 晚 7 天,4% 晚 30 天,2% 晚 60 天。把偏移天数加权:

节点到账日概率累计已付仍未付对期望天数的贡献
到期日第 15 天83%83%17%12.45 天
+7 天第 22 天11%94%6%2.42 天
+30 天第 45 天4%98%2%1.80 天
+60 天第 75 天2%100%0%1.50 天
合计—100%——18.17 天

期望到账天数是 18.17 天,比合同账期多 3.17 天。这个结果与 LedgerLite 的实际应收周转天数 18.2 天几乎一致,说明这组概率不是随手编的,而是按案例的实际账龄反推出来的教学曲线。注意“逾期 30 天以上占 6%”对应的是 +30 的 4% 加 +60 的 2%,不是 17% 的未付比例;未付不等于逾期,到期日当天还有 17% 没付,但其中大部分会在 7 天内到账。

Nord Studio 的 Net 30 用另一组曲线:78% 在到期日付款,14% 晚 7 天,5% 晚 30 天,3% 晚 60 天。期望到账天数是 34.28 天,比合同多 4.28 天。但 Nord 的实际混合账龄是 26.6 天,低于 30 天,原因是 27.3% 的产品订阅按月预付;如果只看咨询业务,34.28 天才是更接近现实的数字。混合账龄把预付款的零账期和咨询的 Net 30 加权在一起,不能直接拿来判断催收效率。

Tidecast 是另一种结构。它没有客户应收,只有平台月结:Steam 销售在月末结算,30 天后到账,应收记为 0。期望到账天数就是 30 天,分布几乎是一个点,因为平台不会“忘记付款”,也不会因为对账争议拖到 90 天。平台结算的风险不是逾期,而是平台封禁、扣款或分成调整,那属于渠道依赖,不是应收管理。Tidecast 的 13 周现金表已经把 30 天延迟画成脉冲后的长尾,见 8.1 13 周现金流表 。

开票触发条件:先让债权成立

开票不是把金额发出去,而是把“我为你做了事”变成“你欠我一笔可执行的款”。如果开票时缺少关键字段,发票会在客户内部被搁置,到期日从“合同写的第 15 天”变成“客户内部流程走完的那一天”。这段延迟不在合同里,却在现金表里。

开票前至少要确认五件事:交付或验收已经发生、合同或采购订单编号可引用、开票主体与税号正确、金额与对账单一致、收件人是能处理发票的人。缺任何一项,发票都可能进入争议状态;争议期间客户不付款,也不一定告诉你缺什么。

把这段延迟换算成钱:一张 10,000 美元的发票,如果因为信息缺失晚开 30 天,按 8% 资金成本,成本是 65.75 美元,等于把 Net 30 又延长了一个月。LedgerLite 的月收入是 20,020 美元,晚开一个月等于多占用 20,020 美元 30 天,资金成本约 131.64 美元;Nord 的月收入是 12,860 美元,对应约 84.56 美元。这些数字不是罚款,而是现金被无谓地留在客户手里的价格。

场景延迟天数占用金额8% 年成本
单张 $10,000 发票30$10,000$65.75
LedgerLite 一个月收入30$20,020$131.64
Nord Studio 一个月收入30$12,860$84.56

开票条件的严谨程度,直接决定后面催收的起点。一张字段齐全、金额可对、收件人明确的发票,到期日就是真正的到期日;一张缺 PO、缺验收、金额有争议的发票,到期日只是一个写在纸上的日期。付款条款本身的设计,见 1.3 价格、调价与付款条款:金额、时点、前提与违约责任 ;本节只处理条款生效之后的执行。

对账单:把争议在到期前清零

对账单不是催款单,它是一张双方都能核对的余额表:期初余额、本期开票、本期收款、贷项通知单、期末余额。它的作用是在到期日之前把“哪张发票、多少钱、付了没有”对齐,而不是等到逾期后再解释。

如果对账只在逾期后做,争议会直接变成账龄。假设 10% 的发票存在争议,每个争议平均多花 21 天解决,那么期望到账天数会增加 0.1 × 21 = 2.1 天。这个比例是教学假设,不是案例事实;它的用途是展示对账节奏的现金价值。

案例年收入期望天数增加多占用现金8% 年成本
LedgerLite$240,2402.1$1,382$111
Nord Studio$154,3202.1$888$71

2.1 天看起来很小,但它会把账龄表里的当前档推向 31–60 档。一笔发票在争议中停留 21 天,可能从 0–30 档跳到 31–60 档,准备率从 1% 变成 5%;如果争议拖到 61 天,准备率变成 20%。对账的价值不只是提前收款,还包括阻止发票在账龄表里“变老”。

对账频率要与账期匹配。Net 15 的客户,月度对账太慢,至少要在月中和月末各对一次;Net 30 的客户,月度对账可以接受,但要在到期前完成,而不是到期后补。对账的对象不是所有客户,而是有多个项目、多个 PO、多个付款批次的客户;单一订阅、自动扣款的客户,对账成本高于收益。

催收节奏:每个节点对应一段概率

催收不是“到期后开始催”,而是一条从到期前到 90 天以上的阶梯。每个节点做一件事,把对应的一段概率往前推。下面以 LedgerLite 的 Net 15 曲线为例。

节点目的累计已付仍未付对期望天数的影响
到期前 3 天确认发票已收到、金额无争议0%100%把“没收到发票”的延迟移出分布
到期日确认付款已发起83%17%12.45 天
+7 天找到审批或付款流程的卡点94%6%2.42 天
+30 天升级到财务负责人或客户老板98%2%1.80 天
+60 天书面通知、暂停新工作或转正式催收100%0%1.50 天
+90 天进入法律或第三方催收评估——不再按普通应收管理

这张表的关键不是“每个节点发什么”,而是每个节点改变哪一段概率。到期前 3 天的提醒,不直接产生现金,但它把一部分“发票没收到”“金额对不上”的延迟从分布里拿掉;+7 天的跟进,把付款流程卡住的 11% 拉回来;+30 天和 +60 天的升级,处理的是剩下的 4% 和 2%。如果跳过前两个节点,直接在第 30 天催,前面本可以早收的钱已经变成了账龄。

把“有节奏”和“无节奏”放在一起比较,差距可以量化。无节奏的 LedgerLite 曲线是 65% 到期付、15% +7、12% +30、8% +60,期望到账 24.45 天;有节奏的曲线是 18.17 天,相差 6.28 天。按年收入 240,240 美元计算,6.28 天对应的平均现金占用是 4,133 美元,按 8% 计算的年成本是 331 美元。

案例有节奏无节奏节省天数释放现金8% 年成本节省
LedgerLite18.17 天24.45 天6.28$4,133$331
Nord 咨询34.28 天40.86 天6.58$2,023$162

Nord 的咨询收入是 112,200 美元,6.58 天对应 2,023 美元的现金释放,年成本节省 162 美元。数字不大,但这是每年重复的余额效应,不是一次性收益。更重要的是尾部:LedgerLite 的无节奏曲线有 20% 的发票超过 30 天,有节奏曲线降到 6%;Nord 从 22% 降到 8%;90+ 的金额减少,坏账准备和催收成本也跟着下降。4.1 节会把这些尾部损失折算成准备,4.2 节会处理催收函、律师函和仲裁的成本。

催收节奏还有一条边界:不是越早越好,也不是越硬越好。到期前 3 天提醒是确认,不是施压;+7 天跟进是解决问题,不是指责;+30 天升级是找对人,不是威胁;+60 天书面通知是保留权利,不是撕破脸。每一步的力度要和合同条款匹配;合同没有约定暂停服务或加速到期的权利,催收动作就只能停留在沟通层面。条款层面的救济,见 4.2 催收升级与法律途径的成本收益 。

把开票与催收写成代码

下面的 invoice.ts 把回款分布和催收节奏固化成两个函数:measureCurve 输入账期和每个节点的概率,输出期望到账天数、按时比例、逾期比例和 90+ 比例;cadenceGain 比较两条曲线,输出节省天数、释放现金和年资金成本节省。金额用整数美分,概率用基点,概率之和必须是 10000。

// file: src/contract/invoice.ts
// tier: L1
// verify: node --test test/contract/invoice.test.ts

import { type Cents } from "../core/money.ts";
import { type Bps, applyRate } from "../core/rate.ts";
import { type Result, fail, ok } from "../core/result.ts";

export const DAYS_PER_YEAR = 365;

export type CollectionStage = {
  /** Days after the due date. Zero is paid on the due date. */
  dayOffset: number;
  probabilityBps: Bps;
};

export type CurveMetrics = {
  expectedLateDays: number;
  expectedDaysToCash: number;
  onTimeBps: Bps;
  overdueBps: Bps;
  ninetyPlusBps: Bps;
};

export type CadenceGain = {
  daysSaved: number;
  cashReleased: Cents;
  annualCostSaved: Cents;
};

/**
 * Cash arrival is a distribution. Each stage carries a probability, and the
 * expected day is the probability-weighted average of invoice date plus terms
 * plus the stage offset.
 */
export function measureCurve(
  termsDays: number,
  stages: CollectionStage[],
): Result<CurveMetrics> {
  if (!Number.isInteger(termsDays) || termsDays < 0 || termsDays > 365) {
    return fail("terms must be an integer between 0 and 365 days");
  }
  if (stages.length === 0) {
    return fail("at least one collection stage is required");
  }
  let totalBps = 0;
  let weightedOffset = 0;
  let onTimeBps = 0;
  let overdueBps = 0;
  let ninetyPlusBps = 0;
  for (const stage of stages) {
    if (
      !Number.isInteger(stage.dayOffset) ||
      stage.dayOffset < -365 ||
      stage.dayOffset > 365
    ) {
      return fail("day offset must be an integer between -365 and 365");
    }
    if (
      !Number.isInteger(stage.probabilityBps) ||
      stage.probabilityBps < 0 ||
      stage.probabilityBps > 10000
    ) {
      return fail("probability must be an integer between zero and 10000 bps");
    }
    totalBps += stage.probabilityBps;
    weightedOffset += stage.dayOffset * stage.probabilityBps;
    if (stage.dayOffset <= 0) {
      onTimeBps += stage.probabilityBps;
    }
    if (stage.dayOffset >= 30) {
      overdueBps += stage.probabilityBps;
    }
    if (stage.dayOffset > 90) {
      ninetyPlusBps += stage.probabilityBps;
    }
  }
  if (totalBps !== 10000) {
    return fail("collection probabilities must sum to 10000 bps");
  }
  const expectedLateDays = weightedOffset / 10000;
  const expectedDaysToCash = termsDays + expectedLateDays;
  return ok({
    expectedLateDays: Math.round(expectedLateDays * 100) / 100,
    expectedDaysToCash: Math.round(expectedDaysToCash * 100) / 100,
    onTimeBps,
    overdueBps,
    ninetyPlusBps,
  });
}

/**
 * Compares two curves for the same terms. The cash released is the annual
 * revenue multiplied by the days saved and divided by 365; it is an average
 * balance, not a one-off cash receipt.
 */
export function cadenceGain(
  annualRevenue: Cents,
  baseline: CurveMetrics,
  cadence: CurveMetrics,
  costOfCapitalBps: Bps,
): Result<CadenceGain> {
  if (!Number.isInteger(annualRevenue) || annualRevenue < 0) {
    return fail("annual revenue must be a non-negative integer");
  }
  if (
    !Number.isInteger(costOfCapitalBps) ||
    costOfCapitalBps < 0 ||
    costOfCapitalBps > 10000
  ) {
    return fail("cost of capital must be an integer between zero and 10000 bps");
  }
  const daysSaved =
    Math.round((baseline.expectedDaysToCash - cadence.expectedDaysToCash) * 100) /
    100;
  const cashReleased = Math.round((annualRevenue * daysSaved) / DAYS_PER_YEAR);
  return ok({
    daysSaved,
    cashReleased,
    annualCostSaved: applyRate(cashReleased, costOfCapitalBps),
  });
}

把 LedgerLite 的有节奏曲线代入 measureCurve(15, ...),得到期望迟到 3.17 天、期望到账 18.17 天、按时 8,300 基点、逾期 600 基点;无节奏曲线得到 24.45 天和 2,000 基点逾期。cadenceGain 比较两者,得到节省 6.28 天、释放 413,344 美分、年成本节省 33,068 美分。Nord 的咨询曲线得到 34.28 天和 800 基点逾期;无节奏曲线 40.86 天;节省 6.58 天、释放 202,267 美分、年成本节省 16,181 美分。这些数字与上表逐行对应。

代码里有三个刻意的设计。第一,节点偏移可以是负数,用来表示提前付款;这让“早付折扣”和“到期付款”能用同一个模型比较。第二,逾期比例按“到期后 30 天及以上”统计,与账龄表的 31–60 档起点对齐;如果按“大于 30 天”统计,+30 的 4% 会被漏掉。第三,cadenceGain 的现金释放是平均余额,不是一次性收入;把它当成利润会高估催收的价值。

小结

  • 现金到账是一条分布:期望到账天数 = 账期 + Σ(节点偏移 × 概率),不是合同上的到期日。
  • LedgerLite 的 Net 15 教学曲线得到 18.17 天,与案例实际 18.2 天吻合;Nord 的咨询曲线得到 34.28 天,混合账龄 26.6 天被产品预付款拉低。
  • 开票前缺少验收、PO、税号、金额或收件人,会把到期日推迟;一张 $10,000 发票晚开 30 天,资金成本是 $65.75。
  • 对账把争议在到期前清零;10% 发票争议、每个多 21 天,会把期望天数拉长 2.1 天,并推动账龄升级。
  • 催收阶梯为到期前 3 天、到期日、+7、+30、+60、+90;每个节点对应一段概率,不是一套话术。
  • LedgerLite 有节奏比无节奏少 6.28 天,释放 $4,133 现金,年资金成本节省 $331;Nord 咨询少 6.58 天,释放 $2,023,节省 $162。
  • 催收的主要收益不在 8% 的资金成本,而在减少 90+ 尾部和坏账准备;4.1 与 4.2 会继续算这部分。

阅读导航:上一节:3.2 应收账龄表:从余额到风险分档 · 下一节:4.1 坏账准备:计提、核销与转回 。

常见错误与边界

  1. 把到期日当到账日。 LedgerLite 的合同是 Net 15,期望到账是 18.17 天;按 15 天排现金会低估占用。
  2. 用发票日期算账龄。 账龄和催收都按到期日计算;按开票日会把 60 天账期的发票误判为逾期。
  3. 开票前不确认验收和 PO。 缺少字段的发票会进入争议,30 天的延迟等于把账期延长一个月,单张 $10,000 发票多花 $65.75。
  4. 把对账留到逾期后。 对账的目标是到期前清零争议;拖到逾期后,发票已经进入 31–60 档,准备率从 1% 变成 5%。
  5. 跳过到期前提醒。 到期前 3 天的确认不产生现金,但它把“没收到发票”“金额不对”的延迟移出分布;跳过它,后面所有节点都要往后推。
  6. 催收只靠邮件模板。 模板不改变概率;改变概率的是找对人、确认卡点、给出明确的下一步和期限。
  7. 过早升级或过晚升级。 +7 天就发律师函会破坏关系;+60 天才第一次跟进,90+ 的尾部已经形成。
  8. 把现金释放当年利润。 6.28 天释放的 $4,133 是平均余额,不是一次性收入;它降低资金占用,不增加收入。
  9. 忽略平台结算与客户应收的区别。 Tidecast 的 30 天是平台结算,不是客户逾期;平台不会“忘记付款”,但可能封禁或调整分成。
  10. 催收动作超出合同权利。 合同没有约定暂停服务、加速到期或收取迟延利息,催收就只能停留在沟通层面;救济条款见 4.2 节。

本节的边界是“单张发票、给定概率曲线”的期望模型。它假设每个节点的概率稳定,不含客户信用差异、争议金额大小和催收成本。它足够回答“这个节奏值多少天、多少钱”,但不足以回答“这笔 90+ 的应收还值不值得追”。后者要把回收概率、催收成本和净回收额放在一起比较,是 4.1 和 4.2 节的内容。

继续阅读

探索更多技术文章

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

全部文章 返回首页

「books」更多文章

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