订场「批准之后才付款」PRD 附录 v0.1 · 给机器跑的

订场「批准之后才付款」· 附录

这一页是给机器跑的,你不用看。正文在 reserve-pay-after-approval.html,状态机和每一封信的正文在工作流文档。这里只放可跑的检查、新函数怎么接线、数字出处。

功能 → 检查(可跑)

都在仓库根目录跑(npm run test:functions 内部是 node scripts/run-function-tests.mjs,逐个跑 netlify/functions/__tests__/*.test.mjsnpm test 内部是 node scripts/run-tests.mjs,逐个跑 site/tests/*.js)。过关线=命令输出最后一行要出现的字。编号跟正文那张表一一对应,按重要度排。施工已完成(2026-09-22):每一条的检查都真跑过,末行都是 TOTAL: 0 failures;每格的「现状」写的是跑完之后的实际状态。

编号功能检查(命令 · 过关线 · 用例 · 现状)
F01批准信不再是能直接付款的链接,改成「建账号或登录」的邀请信npm run test:functions(node 跑的套件,含 netlify/functions/__tests__/booking-approve.test.mjs)· 过关线 TOTAL: 0 failures · 新增用例:approved 邮件正文里断言不出现可以直接打开的 Stripe checkout 链接,断言出现账号页链接、以及「请用提交时那个邮箱」的字样 · 现状:approvedEmailText()netlify/functions/booking-approve.mjs 第 260–276 行)现在还在拼 Pay now — $X: <link>,要连测试一起改
F02订场表恢复不用先登录也能提交npm test -- lead-forms-test.js(node 跑的 jsdom 套件;全量 npm test 也会跑到)· 过关线 TOTAL: 0 failures · 新增用例:断言 STEP2_REQUIRES_AUTH.reserve === false,订场表第 2 步不登录也能交;已有的「没登录挡住提交」用例(TC-PG3-auth 附近,site/tests/lead-forms-test.js 第 1350 行)改成只验其余五张表仍然挡 · 现状:site/js/lead-forms.js 第 543 行现在写的是 reserve:true,要改成 false
F03用提交时那个邮箱注册或登录后,请求自动挂到账号名下,账号页看得到npm run test:functions(认领函数本身 netlify/functions/__tests__/account-claim-history.test.mjs,逻辑不改)+ npm test -- account-history-test.js(node 跑的 jsdom 套件)· 过关线都是 TOTAL: 0 failures · 新增用例:账号页登录后认领到一条 reserve 请求,#acct-leads 列表里能看到这一行 · 现状:认领函数已有且绿;账号页新用例 AH-5a/b/c 已补,绿
F04账号页出现「Pay now $X」,点了真的到 Stripe 付款页,金额和批准时算的一致新函数 netlify/functions/booking-checkout.mjs + 新测试 netlify/functions/__tests__/booking-checkout.test.mjs,跑法 npm run test:functions(node 跑的套件)· 过关线 TOTAL: 0 failures · 用例:状态是「等付款」且请求属于当前登录用户时,返回的付款链接对应的金额,等于批准时存在这条请求上的 payload.booking.amount_cents(不是按当前单价重新算的)· 现状:函数已建(booking-checkout.mjs),用例 TC-BC1/2/3 绿
F05只有这条请求本人、且还在等付款状态时,才点得动付款同上 netlify/functions/__tests__/booking-checkout.test.mjsnpm run test:functions(node 跑的套件)· 用例:①别人的登录令牌调这条请求 → 403;②状态已经不是「等付款」(已付款/已拒绝/已取消)时调用 → 409,不新建付款页 · 现状:已建,用例 TC-BC4~BC14 绿
F06重复点付款、或者旧付款页过期了,都不出乱子同上 netlify/functions/__tests__/booking-checkout.test.mjsnpm run test:functions(node 跑的套件)· 用例:①连续调用两次且 Stripe 那边的付款会话仍然开着 → 第二次返回同一个付款链接,不重复建;②记录着的付款会话已经过期或已完成 → 新建一个并覆盖记录里的会话号 · 现状:已建,用例 TC-BC4~BC14 绿
F07店长拒绝或取消时,客人手上正等付款的付款页要跟着失效扩展 netlify/functions/__tests__/booking-approve.test.mjs 现有的 VOID 系列用例 · npm run test:functions(node 跑的套件)· 新增用例:客人已经点过「Pay now」(付款会话是 booking-checkout.mjs 建的那个),店长拒绝或取消之后,那个付款会话要被作废,客人再打开旧页面付不了款 · 现状:已补两条(客人点过付款后被拒绝 / 付完款被取消),绿
F08付款成功之后的确认信 + 日历邀请,跟现在完全一样netlify/functions/__tests__/booking-payment-webhook.test.mjs,不改代码、不改测试 · npm run test:functions(node 跑的套件)· 过关线 TOTAL: 0 failures · 现状:已有,本卡不碰这个文件,跑绿即算回归通过

接线

东西写法
新函数 booking-checkout.mjsPOST,body { lead_id },header Authorization: Bearer <客人自己的登录令牌>。顺序照抄 account-claim-history.mjs 的安全手势:①令牌去 /auth/v1/user 换出「这是谁」(无效 → 401);②用 service key 查这条 lead,customer_id 必须等于这个用户的 id(不是 → 403)且 status 必须是「等付款」那个状态(不是 → 409);③金额直接读这条 lead 上 payload.booking.amount_cents(批准时存的),不重新按当前单价算。不能照抄 shop-checkout.mjs 的写法——那个函数的 customer_id 是直接从请求体里拿的、不校验,本函数必须验令牌。
Stripe 会话怎么取舍booking-checkout.mjs 每次被调用:先看这条 lead 的 payload.booking.stripe_checkout_session_id 记的那个会话是否还开着(问 Stripe);还开着就把同一个 checkout_url 原样返回,不新建;不开着(过期或已经作废)就新建一个,把新的会话号写回 payload.booking.stripe_checkout_session_id,旧的不用再管。批准这一步(booking-approve.mjshandleApproved)之后不再在批准时就创建 Stripe 会话,只负责算金额、查时段冲突、写状态、发邀请信——创建会话完全移到 booking-checkout.mjs 里,等客人真的点了付款才建。
批准邮件文案netlify/functions/booking-approve.mjsapprovedEmailText()(第 260–276 行)整段换成工作流文档里的两个新版本(未建账号 / 已有账号),按批准那一刻这条 lead 的 customer_id 是否已经非空来选用哪个版本。
邀请信里的链接未建账号版本:https://yummy-future.com/account.html#signup;已有账号版本:https://yummy-future.com/account.html#signin。两个都是站内固定路径,不带姓名、邮箱等个人信息(跟现有 Stripe 回跳地址 SUCCESS_URL/CANCEL_URL 同一条规矩)。不用现有的 #return= 机制——那个是「登录完跳回原来那个页面」,这里登录完就停在账号页本身,账号页就是终点。
账号页「Pay now」按钮site/js/account.jsrenderLeads()(第 610–630 行一带):intent==='reserve'status==='approved_awaiting_payment' 的那一行,在状态徽章旁边加一个按钮,文案 Pay now $XX 来自 payload.booking.amount_cents)。点击调 POST /.netlify/functions/booking-checkout,带当前登录令牌和这条 lead 的 id,拿到 checkout_urllocation.href 跳过去(跟 shop-checkout.mjs 客户端那一半的调用方式一致,只是这里的令牌是真的会被后端校验)。
拒绝 / 取消时作废付款会话booking-approve.mjshandleDeclined / handleCancelledFlow 现有的 expireStripeCheckoutSession 逻辑不用改——它认的是 payload.booking.stripe_checkout_session_id,不管这个会话号是批准时建的还是 booking-checkout.mjs 后来建的,只要字段里有就会去作废。
账号页视图名site/js/account.js 第 360 行 VIEWS 数组已经有 signinsignup 两个视图,邀请信链接直接用现成的 hash,不用新增视图。

数字从哪来