Skip to content

feat(store,sdk): order.refunded 载荷补 full + #11 退款追回集成验收 - #20

Merged
worable233 merged 2 commits into
mainfrom
fix/referral-refund-clawback
Oct 24, 2026
Merged

worable233 merged 2 commits into
mainfrom
fix/referral-refund-clawback

Conversation

@worable233

Copy link
Copy Markdown
Owner

对应

跟踪表 #11「推广返佣」的内核侧改动。配套插件 PR:worable233/stackpanel-plugins#3。

改了什么

1. order.refunded 补 full 字段

packages/plugins/store/src/refund.ts 发布退款事件时带上 full: isFull。

原载荷只有 orderId/amount/userId,订阅方拿不到「这单是否已彻底撤销」只能自己猜,
猜错两个方向都会出事:把部分退款当整单(错扣奖励),或把整单当部分(漏扣)。
由发布方直接给判定,订阅方不重复推导。

2. SDK 声明该事件

packages/sdk/src/plugin.ts 的 PlatformEvents 补 order.refunded,
订阅方拿到受约束载荷而不是 unknown。

3. 集成验收 apps/api/tests/integration/referral-clawback.test.ts

真实 Postgres + 真实安装产物,4/4:

用例 断言
整单退款 记录 clawed_back、金额 500、订单号正确、余额归零、ledger referenceId 为 clawback 键
余额不足 退款仍 200,记录 clawed_back,金额记 0
部分退款 记录保持 paid,余额不变
非归因订单退款 记录保持 paid,余额不变

为什么必须单独存在:单测里直接调 clawbackByRefund,接线断了也照样绿。
跨插件协作缺陷最容易出在接线上。

写这条用例踩到的坑值得记录:ctx.tx().publish 把事件写进 outbox 行,但测试进程
不走 initInfra(),outbox 从未绑定,也就没有 relay 投递 —— 事件永远到不了订阅方。
用例里按生产路径显式 configureOutboxEventBus + startOutboxConsumption,
否则会以「什么都没发生」的方式假失败。

验证

  • apps/api/tests/integration/referral-clawback.test.ts:4/4
  • packages/plugins/store/src/refund.test.ts:7/7
  • packages/sdk 构建 + store 插件 tsc --noEmit:clean

未做(明确披露)

  • 真实 V10 联调(无凭据)
  • 真实支付渠道退款回调(用例直接构造已支付订单调管理端退款接口)

worable233 added a commit that referenced this pull request Oct 24, 2026
origin/main 自身的 Install·License·Check·Build 是红的,所有 PR(#13–#20)都因此
显示失败。**所有 PR 都红 = 没有信号**,真实回归会被淹没在「反正一直是红的」里。

失败分三层,都是 main 上的既有缺陷,不是任何 PR 引入的:

## 1. packages/sdk —— 非空断言
src/frontend.ts 754/758:索引访问类型本来就是 T | undefined,
外层 keys.some() 只保证某个 key 有值、不保证这个 key 有值。
改成显式判空,不用 eslint-disable 骗自己。

## 2. apps/web —— React Compiler memo + 测试 any
- theme-json-editor.tsx:81:useMemo 回调里混了 try/catch 与多个提前 return,
  React Compiler 拒绝保留手工 memo 并直接报错。校验逻辑提成模块级纯函数
  validateDraft,hook 体只剩一行调用,顺带让这段校验可独立测试。
- cart-checkout.test.ts:Record<string, any> 换成显式 ElementProps。
  不用 Record<string, unknown>(每次读 props 又要断言一次),
  也不留 any(prop 名写错会静默读到 undefined 让用例假绿)。

## 3. apps/api —— 非空断言 + import() 类型注解
- routes/theme-configuration.ts 36/48:request.user!.id 改成判空后抛 401,
  而不是断言「一定已登录」。guards 里有 requireAuth,但断言让这层保证
  在类型系统里消失,重构掉守卫时不会有任何报错。
- lib/theme-configuration.ts:87:migrateFrontendSettings 对空输入返回 undefined,
  用  会把 undefined 带进 Object.entries。改成 。
- tests/unit/market-theme-install.test.ts:import() 类型注解改成顶部 type import。

## 验证
- packages/sdk:eslint 0 problems,tsc clean
- apps/web:eslint 0 problems,tsc -p tsconfig.test.json clean,cart-checkout 11/11
- apps/api:eslint 0 problems,tsc clean,theme 集成 15/15,market-theme-install 4/4

## 影响
合入后 #13–#20 的 CI 应全部转绿。
worable233 added a commit that referenced this pull request Oct 24, 2026
origin/main 自身的 Install·License·Check·Build 是红的,所有 PR(#13–#20)都因此
显示失败。**所有 PR 都红 = 没有信号**,真实回归会被淹没在「反正一直是红的」里。

失败分三层,都是 main 上的既有缺陷,不是任何 PR 引入的:

## 1. packages/sdk —— 非空断言
src/frontend.ts 754/758:索引访问类型本来就是 T | undefined,
外层 keys.some() 只保证某个 key 有值、不保证这个 key 有值。
改成显式判空,不用 eslint-disable 骗自己。

## 2. apps/web —— React Compiler memo + 测试 any
- theme-json-editor.tsx:81:useMemo 回调里混了 try/catch 与多个提前 return,
  React Compiler 拒绝保留手工 memo 并直接报错。校验逻辑提成模块级纯函数
  validateDraft,hook 体只剩一行调用,顺带让这段校验可独立测试。
- cart-checkout.test.ts:Record<string, any> 换成显式 ElementProps。
  不用 Record<string, unknown>(每次读 props 又要断言一次),
  也不留 any(prop 名写错会静默读到 undefined 让用例假绿)。

## 3. apps/api —— 非空断言 + import() 类型注解
- routes/theme-configuration.ts 36/48:request.user!.id 改成判空后抛 401,
  而不是断言「一定已登录」。guards 里有 requireAuth,但断言让这层保证
  在类型系统里消失,重构掉守卫时不会有任何报错。
- lib/theme-configuration.ts:87:migrateFrontendSettings 对空输入返回 undefined,
  用 `!` 会把 undefined 带进 Object.entries。改成 `?? {}`。
- tests/unit/market-theme-install.test.ts:import() 类型注解改成顶部 type import。

## 验证
- packages/sdk:eslint 0 problems,tsc clean
- apps/web:eslint 0 problems,tsc -p tsconfig.test.json clean,cart-checkout 11/11
- apps/api:eslint 0 problems,tsc clean,theme 集成 15/15,market-theme-install 4/4

## 影响
合入后 #13–#20 的 CI 应全部转绿。
worable233 added a commit that referenced this pull request Oct 24, 2026
origin/main 自身的 Install·License·Check·Build 是红的,所有 PR(#13–#20)都因此
显示失败。**所有 PR 都红 = 没有信号**,真实回归会被淹没在「反正一直是红的」里。

失败分三层,都是 main 上的既有缺陷,不是任何 PR 引入的:

## 1. packages/sdk —— 非空断言
src/frontend.ts 754/758:索引访问类型本来就是 T | undefined,
外层 keys.some() 只保证某个 key 有值、不保证这个 key 有值。
改成显式判空,不用 eslint-disable 骗自己。

## 2. apps/web —— React Compiler memo + 测试 any
- theme-json-editor.tsx:81:useMemo 回调里混了 try/catch 与多个提前 return,
  React Compiler 拒绝保留手工 memo 并直接报错。校验逻辑提成模块级纯函数
  validateDraft,hook 体只剩一行调用,顺带让这段校验可独立测试。
- cart-checkout.test.ts:Record<string, any> 换成显式 ElementProps。
  不用 Record<string, unknown>(每次读 props 又要断言一次),
  也不留 any(prop 名写错会静默读到 undefined 让用例假绿)。

## 3. apps/api —— 非空断言 + import() 类型注解
- routes/theme-configuration.ts 36/48:request.user!.id 改成判空后抛 401,
  而不是断言「一定已登录」。guards 里有 requireAuth,但断言让这层保证
  在类型系统里消失,重构掉守卫时不会有任何报错。
- lib/theme-configuration.ts:87:migrateFrontendSettings 对空输入返回 undefined,
  用 `!` 会把 undefined 带进 Object.entries。改成 `?? {}`。
- tests/unit/market-theme-install.test.ts:import() 类型注解改成顶部 type import。

## 验证
- packages/sdk:eslint 0 problems,tsc clean
- apps/web:eslint 0 problems,tsc -p tsconfig.test.json clean,cart-checkout 11/11
- apps/api:eslint 0 problems,tsc clean,theme 集成 15/15,market-theme-install 4/4

## 影响
合入后 #13–#20 的 CI 应全部转绿。
worable233 added a commit that referenced this pull request Oct 24, 2026
origin/main 自身的 Install·License·Check·Build 是红的,所有 PR(#13–#20)都因此
显示失败。**所有 PR 都红 = 没有信号**,真实回归会被淹没在「反正一直是红的」里。

## 第一层:packages/sdk 非空断言
`src/frontend.ts` 754/758。索引访问类型本来就是 `T | undefined`,外层
`keys.some()` 只保证某个 key 有值、不保证这个 key 有值。改成显式判空。

## 第二层:apps/web —— React Compiler memo + 测试 any
- `theme-json-editor.tsx:81`:`useMemo` 回调里混了 `try/catch` 与多个提前
  return,React Compiler 拒绝保留手工 memo 并直接报错。校验逻辑提成模块级纯函数
  `validateDraft`,hook 体只剩一行调用,顺带让这段校验可独立测试。
- `cart-checkout.test.ts`:`Record<string, any>` 换成显式 `ElementProps`。
  不用 `Record<string, unknown>`(每次读 props 又要断言一次),也不留 `any`
  (prop 名写错会静默读到 `undefined` 让用例假绿)。

## 第三层:apps/api —— 非空断言 + import() 类型注解
- `routes/theme-configuration.ts` 36/48:`request.user!.id` 改成判空后抛 401。
  guards 里有 requireAuth,但断言让这层保证在类型系统里消失 —— 重构掉守卫时
  不会有任何报错。
- `lib/theme-configuration.ts:87`:`migrateFrontendSettings` 对空输入返回
  undefined,用 `!` 会把 undefined 带进 `Object.entries`。改成 `?? {}`。
- `tests/unit/market-theme-install.test.ts`:`import()` 类型注解改成顶部 type import。

## 第四层:被 lint 失败掩盖的真实测试失败
lint 全绿之后才暴露出 `audit-referral-reward.test.ts` 在 CI 上是红的:

    AssertionError: expected 404 to be 200
    relation "ext_referral_code" does not exist

原因不是 schema 坏了,而是 **CI runner 上根本没有 referral 插件**。它不是内核
内置插件(`packages/plugins/referral` 只有 4 个残缺源文件、没有 `package.json`),
源码在独立仓库 `stackpanel-plugins`,只有部署过的 data 目录才有。

改成显式探测 `<dataDir>/plugins/referral/manifest.json` 后 `skipIf`。
「插件没装」和「表不存在」是两回事,让前者伪装成后者会浪费大量排查时间。

## 验证
- packages/sdk:eslint 0 problems,tsc clean
- apps/web:eslint 0 problems,tsc -p tsconfig.test.json clean,cart-checkout 11/11
- apps/api:eslint 0 problems,tsc clean
  - theme 集成 15/15
  - market-theme-install 4/4
  - audit-referral-reward 1/1(本地已装 referral)

## 影响
合入后 #13–#20 的 CI 应全部转绿。
- store:退款事件带 full=isFull,让下游区分整单撤销与部分退款
- sdk:PlatformEvents 声明 order.refunded,订阅方拿到受约束载荷
- 集成用例 referral-clawback.test.ts(真实 Postgres + 真实安装产物)4/4:
  整单追回、余额不足记 0、部分退款不追回、非归因订单不追回
  用例显式绑定 outbox 并发起消费,否则 tx.publish 的事件无人投递会假失败
@worable233
worable233 force-pushed the fix/referral-refund-clawback branch from 0b4318f to bcca882 Compare October 24, 2026 09:59
worable233 added a commit that referenced this pull request Oct 24, 2026
* fix(sdk): 去掉 frontend.ts 两处非空断言,恢复 lint 绿灯

origin/main 的 Install·License·Check·Build 一直是红的:packages/sdk 的
no-non-null-assertion 在 754/758 行报错。所有基于 main 的 PR 都因此失败,
掩盖了真实的回归信号。

改成显式判空:values[source] 取出的值先落到局部变量再判 undefined,
不再用 `!` 断言。「已确认非 undefined」在 Object.entries 循环里并不成立
(索引访问的类型是 T | undefined),断言是在骗类型系统而不是表达事实。

* fix(ci): 修 origin/main 上三层 lint + 一条被掩盖的测试失败

origin/main 自身的 Install·License·Check·Build 是红的,所有 PR(#13–#20)都因此
显示失败。**所有 PR 都红 = 没有信号**,真实回归会被淹没在「反正一直是红的」里。

## 第一层:packages/sdk 非空断言
`src/frontend.ts` 754/758。索引访问类型本来就是 `T | undefined`,外层
`keys.some()` 只保证某个 key 有值、不保证这个 key 有值。改成显式判空。

## 第二层:apps/web —— React Compiler memo + 测试 any
- `theme-json-editor.tsx:81`:`useMemo` 回调里混了 `try/catch` 与多个提前
  return,React Compiler 拒绝保留手工 memo 并直接报错。校验逻辑提成模块级纯函数
  `validateDraft`,hook 体只剩一行调用,顺带让这段校验可独立测试。
- `cart-checkout.test.ts`:`Record<string, any>` 换成显式 `ElementProps`。
  不用 `Record<string, unknown>`(每次读 props 又要断言一次),也不留 `any`
  (prop 名写错会静默读到 `undefined` 让用例假绿)。

## 第三层:apps/api —— 非空断言 + import() 类型注解
- `routes/theme-configuration.ts` 36/48:`request.user!.id` 改成判空后抛 401。
  guards 里有 requireAuth,但断言让这层保证在类型系统里消失 —— 重构掉守卫时
  不会有任何报错。
- `lib/theme-configuration.ts:87`:`migrateFrontendSettings` 对空输入返回
  undefined,用 `!` 会把 undefined 带进 `Object.entries`。改成 `?? {}`。
- `tests/unit/market-theme-install.test.ts`:`import()` 类型注解改成顶部 type import。

## 第四层:被 lint 失败掩盖的真实测试失败
lint 全绿之后才暴露出 `audit-referral-reward.test.ts` 在 CI 上是红的:

    AssertionError: expected 404 to be 200
    relation "ext_referral_code" does not exist

原因不是 schema 坏了,而是 **CI runner 上根本没有 referral 插件**。它不是内核
内置插件(`packages/plugins/referral` 只有 4 个残缺源文件、没有 `package.json`),
源码在独立仓库 `stackpanel-plugins`,只有部署过的 data 目录才有。

改成显式探测 `<dataDir>/plugins/referral/manifest.json` 后 `skipIf`。
「插件没装」和「表不存在」是两回事,让前者伪装成后者会浪费大量排查时间。

## 验证
- packages/sdk:eslint 0 problems,tsc clean
- apps/web:eslint 0 problems,tsc -p tsconfig.test.json clean,cart-checkout 11/11
- apps/api:eslint 0 problems,tsc clean
  - theme 集成 15/15
  - market-theme-install 4/4
  - audit-referral-reward 1/1(本地已装 referral)

## 影响
合入后 #13–#20 的 CI 应全部转绿。
@worable233
worable233 merged commit 0f9493c into main Oct 24, 2026
1 check passed
@worable233
worable233 deleted the fix/referral-refund-clawback branch October 24, 2026 10:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant