forked from wangziqi/gongxue-base
feat: add commerce adjustment operations
This commit is contained in:
10
README.md
10
README.md
@@ -22,14 +22,14 @@
|
||||
- `apps/worker` 后台任务进程:CRM webhook 队列消费、generic/钉钉/飞书/企微机器人发送、签名、失败重试和日志;commerce worker 可补偿查询微信/支付宝支付和退款状态;provider-bills worker 可下载微信/支付宝官方账单并导入资金对账;assets worker 可复检托管资源元数据、执行内置安全扫描并自动下架异常资源;imports worker 可执行大批量导入;public-banks worker 可自动同步公共题库采纳副本;exports worker 可渲染 PDF/Word 导出文件和每日一练 ZIP 图片素材包。
|
||||
- 销售/代理分佣结算基础闭环:租户默认比例、成员比例、激活码批次比例、订单/激活码归因、结算单生成、审核、线下打款状态、CSV/JSON 导出、打款凭证登记/复核和权限隔离。
|
||||
- 订单售后基础闭环:退款请求、审核、处理状态流、微信/支付宝发起退款、微信/支付宝退款查询确认、微信/支付宝退款通知 webhook、退款金额累计、部分/全额退款订单状态、全额退款权益撤销、退款事件和审计日志。
|
||||
- 资金对账和差错工单闭环:租户财务/运营可通过 `/api/commerce/reconciliation/*` 导入或预览支付/退款账单行,也可创建微信/支付宝官方账单下载任务;后端按租户隔离比对本地订单、支付、退款记录,识别已匹配、金额不一致、状态不一致、供应商有本地无、本地有供应商无、重复行和无效行,并写入对账批次、明细和审计日志;异常明细可创建差错工单,支持分配、开始处理、升级、解决、忽略、重开和事件留痕。工单只做财务审核闭环,不直接修改订单、支付、退款或权益。
|
||||
- 资金对账、异常订单和财务凭证闭环:租户财务/运营可通过 `/api/commerce/reconciliation/*` 导入或预览支付/退款账单行,也可创建微信/支付宝官方账单下载任务;后端按租户隔离比对本地订单、支付、退款记录,识别已匹配、金额不一致、状态不一致、供应商有本地无、本地有供应商无、重复行和无效行,并写入对账批次、明细和审计日志;异常明细可创建差错工单,支持分配、开始处理、升级、解决、忽略、重开和事件留痕;`/api/commerce/operations/anomalies` 聚合异常订单风险,`/api/commerce/adjustment-vouchers*` 支持人工调整凭证、复核、事件轨迹和报表。工单和凭证只做财务审核闭环,不直接修改订单、支付、退款或权益。
|
||||
- PocketBase schema/数据导入器雏形和导入后校验脚本。
|
||||
- 本地 Supabase reset、烟测 seed、API 集成测试、完整重构检查命令。
|
||||
|
||||
还没有达到生产交付的部分:
|
||||
|
||||
- Supabase Auth/JWT、租户角色模板、班级/教师/学生范围权限已可联调;生产前还要做真实云端 Auth/JWKS 回归和 RLS 深测。
|
||||
- 阿里云/腾讯云短信、微信小程序登录、微信网页登录、QQ 登录、手机号绑定/换绑、微信支付、支付宝主链路、微信/支付宝发起退款/查询确认/退款通知、支付/退款补偿 worker 已完成本地适配;资金对账已支持手工/API 账单导入、微信/支付宝官方账单下载任务、provider-bills worker 自动导入比对和差错工单处理;异常订单运营台、人工调整凭证复核报表和真实生产账号联调还没接完。
|
||||
- 阿里云/腾讯云短信、微信小程序登录、微信网页登录、QQ 登录、手机号绑定/换绑、微信支付、支付宝主链路、微信/支付宝发起退款/查询确认/退款通知、支付/退款补偿 worker 已完成本地适配;资金对账已支持手工/API 账单导入、微信/支付宝官方账单下载任务、provider-bills worker 自动导入比对、差错工单处理、异常订单运营台和人工调整凭证复核报表;真实生产账号、真实回调域名和真实生产账单抽样验收还没接完。
|
||||
- OSS/COS/Supabase Storage 上传下载签名 provider 已接入;上传后校验、PDF/图片预览、资源访问事件、动态水印上下文、锁定资源 CDN 边界、资源复检 worker、内置 `metadata_rules` 安全扫描和外部 HTTP 杀毒/内容安全 scanner 接入层已完成。生产还要配置真实扫描服务 endpoint/token,并继续补转码/CDN 级水印、CDN 刷新和对象生命周期策略。
|
||||
- Excel/CSV 导入解析已完成并复用 `content_import_jobs/items/issues` 管线;大批量异步导入 worker 基础已接入,支持 queued job 消费、重试和审计;导入后复检、模板下载和字段映射 API 已完成,前端 UI 待接。
|
||||
- 题库导出已完成服务端结构化 payload、PDF/Word 二进制 worker、每日一练基础导出和每日一练 ZIP 图片素材包;后续还要补更精细试卷模板、多模板排版和导出操作台体验。
|
||||
@@ -316,7 +316,7 @@ API 身份上下文:
|
||||
- 题库入口和分类使用 `content_entries/content_nodes`;题目列表和练习规则使用 `question_collections/practice_blueprints`,前端不要再把旧树字段当成唯一业务结构。
|
||||
- 批量导入必须先写 `content_import_jobs/items/issues`,保留原始 payload、规范化 payload、逐行问题和审计记录。题目、单词、知识手册、分数线和视频 JSON/CSV/Excel 导入已走这套后台校验管线;大批量任务可提交 `executionMode=async`,由 imports worker 消费,前端只轮询 job 状态和展示 issues。
|
||||
- 题库导出必须由后端按权限生成,不允许前端直接读取数据库拼导出文件;不开启答案/解析时,顶层题目和复合题子题都必须脱敏;PDF/Word/每日一练 ZIP 只通过 exports worker 写入 `content_assets` 后再签名下载/预览。
|
||||
- 支付 webhook 必须先设计幂等键和验签流程,再进入生产使用;生产环境还应定时运行 commerce worker 兜底供应商漏通知和处理中退款,并定时运行 provider-bills worker 下载官方账单核对本地订单。官方账单下载任务只保存下载域名、hash 和对账批次 ID,不向前端暴露下载 URL 或商户密钥。对账差错工单只允许记录财务处理结论和凭证,不允许前端或工单接口直接篡改订单、支付、退款或权益状态。
|
||||
- 支付 webhook 必须先设计幂等键和验签流程,再进入生产使用;生产环境还应定时运行 commerce worker 兜底供应商漏通知和处理中退款,并定时运行 provider-bills worker 下载官方账单核对本地订单。官方账单下载任务只保存下载域名、hash 和对账批次 ID,不向前端暴露下载 URL 或商户密钥。对账差错工单和人工调整凭证只允许记录财务处理结论、附件引用和审计事件,不允许前端、工单接口或凭证审批接口直接篡改订单、支付、退款或权益状态。
|
||||
|
||||
## 最近一次验证
|
||||
|
||||
@@ -335,7 +335,7 @@ npm run audit:runtime
|
||||
git diff --check
|
||||
```
|
||||
|
||||
结果:通过。`npm run test:api` 覆盖资源访问事件、锁定 CDN 资源拒绝、provider-managed CDN 显式放行、学生短 TTL 下载/预览、访问记录查询、安全扫描门禁、官方账单下载任务权限和脱敏响应。`npm run test:worker:commerce` 覆盖支付/退款补偿、微信/支付宝官方账单下载、账单 hash 校验、导入 `provider_download` 对账批次和密钥不泄露。`npm run test:worker:assets` 覆盖托管资源复检、内置安全扫描、外部 HTTP scanner 通过/失败/不可用 fail-closed、扫描失败/跳过事件和异常资源自动下架。`npm run test:worker:exports` 覆盖导出 worker 生成可信资源并标记 `securityScanStatus=passed`。`npm run audit:runtime` 无 high/critical 漏洞;当前运行时依赖树仍有 `exceljs -> uuid` 的 moderate 级提示,修复需要破坏性降级 `exceljs`,后续应在导入 Excel 回归充分后单独处理。
|
||||
结果:通过。`npm run test:api` 覆盖资源访问事件、锁定 CDN 资源拒绝、provider-managed CDN 显式放行、学生短 TTL 下载/预览、访问记录查询、安全扫描门禁、官方账单下载任务权限和脱敏响应、异常订单运营台、人工调整凭证提交/复核/事件/报表、租户隔离,以及凭证审批不修改订单/支付/权益。`npm run test:worker:commerce` 覆盖支付/退款补偿、微信/支付宝官方账单下载、账单 hash 校验、导入 `provider_download` 对账批次和密钥不泄露。`npm run test:worker:assets` 覆盖托管资源复检、内置安全扫描、外部 HTTP scanner 通过/失败/不可用 fail-closed、扫描失败/跳过事件和异常资源自动下架。`npm run test:worker:exports` 覆盖导出 worker 生成可信资源并标记 `securityScanStatus=passed`。`npm run audit:runtime` 无 high/critical 漏洞;当前运行时依赖树仍有 `exceljs -> uuid` 的 moderate 级提示,修复需要破坏性降级 `exceljs`,后续应在导入 Excel 回归充分后单独处理。
|
||||
|
||||
注意:`apps/taro` 是静态构建工程,线上发布 `apps/taro/dist/**`,不发布 `node_modules`。Taro 4.2.0 当前构建工具链仍会触发 `npm run audit:taro:toolchain` 的上游 high/critical 提示,不能用 `npm audit fix --force` 降级到 Taro 3 破坏构建;上线验收时以 `audit:runtime`、构建产物、前端密钥检查和静态服务器配置为准,并持续跟进 Taro 官方修复。
|
||||
|
||||
@@ -347,4 +347,4 @@ git diff --check
|
||||
2. 继续补 Taro 前端:学生端视频/反馈/模考报告/订单收银台,租户后台写入表单/导入操作台/公共题库同步/角色模板 UI,平台后台租户详情/审计/自动计费增强,小程序兼容验证。
|
||||
3. 对象存储真实 AV/内容安全扫描服务联调、CDN 防盗链、转码/CDN 级水印和生命周期策略。
|
||||
4. 题库导出模板精排、导出操作台、真实数据 dry-run、导入字段映射 UI 和复检结果操作台。
|
||||
5. 真实 OAuth/短信/支付生产账号联调、异常订单运营台、人工调整凭证复核、公共题库版本通知/冲突处理操作台、积分活动深化,以及排行榜防刷/预聚合。
|
||||
5. 真实 OAuth/短信/支付生产账号联调、真实生产账单抽样验收、财务操作台前端体验、公共题库版本通知/冲突处理操作台、积分活动深化,以及排行榜防刷/预聚合。
|
||||
|
||||
1029
apps/api/src/features/commerce/adjustments.ts
Normal file
1029
apps/api/src/features/commerce/adjustments.ts
Normal file
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,14 @@ import {
|
||||
refundNotifyRoute,
|
||||
updateRefundStatusRoute,
|
||||
} from './routes.js';
|
||||
import {
|
||||
adjustmentVoucherEventsRoute,
|
||||
adjustmentVoucherReportRoute,
|
||||
adjustmentVouchersRoute,
|
||||
commerceOperationsAnomaliesRoute,
|
||||
createAdjustmentVoucherRoute,
|
||||
updateAdjustmentVoucherStatusRoute,
|
||||
} from './adjustments.js';
|
||||
import {
|
||||
createReconciliationIssueRoute,
|
||||
importReconciliationRoute,
|
||||
@@ -54,6 +62,12 @@ export const commerceRoutes: RouteDefinition[] = [
|
||||
['GET', '/api/commerce/reconciliation/batches', reconciliationBatchesRoute],
|
||||
['GET', '/api/commerce/reconciliation/items', reconciliationItemsRoute],
|
||||
['GET', '/api/commerce/reconciliation/anomalies', reconciliationAnomaliesRoute],
|
||||
['GET', '/api/commerce/operations/anomalies', commerceOperationsAnomaliesRoute],
|
||||
['GET', '/api/commerce/adjustment-vouchers', adjustmentVouchersRoute],
|
||||
['POST', '/api/commerce/adjustment-vouchers', createAdjustmentVoucherRoute],
|
||||
['POST', '/api/commerce/adjustment-vouchers/status', updateAdjustmentVoucherStatusRoute],
|
||||
['GET', '/api/commerce/adjustment-vouchers/events', adjustmentVoucherEventsRoute],
|
||||
['GET', '/api/commerce/adjustment-vouchers/report', adjustmentVoucherReportRoute],
|
||||
['POST', '/api/commerce/reconciliation/provider-bills/request', requestProviderBillDownloadRoute],
|
||||
['GET', '/api/commerce/reconciliation/provider-bills/jobs', providerBillDownloadJobsRoute],
|
||||
['POST', '/api/commerce/reconciliation/issues/create', createReconciliationIssueRoute],
|
||||
|
||||
@@ -92,6 +92,7 @@ export function tenantPermissionCatalog() {
|
||||
{ key: 'tenant:payment:write', label: '商户配置管理' },
|
||||
{ key: 'tenant:reconciliation:read', label: '资金对账查看' },
|
||||
{ key: 'tenant:reconciliation:write', label: '资金对账导入' },
|
||||
{ key: 'tenant:reconciliation:review', label: '资金差错复核' },
|
||||
{ key: 'tenant:reconciliation:download', label: '官方账单下载' },
|
||||
{ key: 'tenant:refund:read', label: '退款查看' },
|
||||
{ key: 'tenant:refund:write', label: '退款申请/处理' },
|
||||
|
||||
@@ -287,16 +287,23 @@ body: { "code": "<qq oauth code>", "redirectUri": "https://h5.example.com/auth/q
|
||||
- `GET /api/commerce/reconciliation/issues`:按状态、严重级别、负责人、批次、订单号筛选差错工单。
|
||||
- `POST /api/commerce/reconciliation/issues/status`:执行 `start/assign/resolve/ignore/escalate/reopen` 状态流转。
|
||||
- `GET /api/commerce/reconciliation/issues/events`:查看工单创建、分配、处理、解决、忽略、重开等事件轨迹。
|
||||
- `GET /api/commerce/operations/anomalies`:聚合未关闭对账工单、失败官方账单任务、支付事件错误、长时间 pending 支付/退款,供租户财务/售后运营台使用。
|
||||
- `GET /api/commerce/adjustment-vouchers`:查询人工调整凭证。
|
||||
- `POST /api/commerce/adjustment-vouchers`:创建人工调整凭证,可关联差错工单、对账明细、订单、支付或退款。
|
||||
- `POST /api/commerce/adjustment-vouchers/status`:执行 `submitted/approved/rejected/voided` 复核流转。
|
||||
- `GET /api/commerce/adjustment-vouchers/events`:查看凭证事件轨迹。
|
||||
- `GET /api/commerce/adjustment-vouchers/report`:按日期输出凭证状态、类型、来源和日趋势统计。
|
||||
|
||||
权限点:
|
||||
|
||||
- `tenant:reconciliation:read`:查看/预览对账。
|
||||
- `tenant:reconciliation:write`:导入对账批次、创建/处理差错工单。
|
||||
- `tenant:reconciliation:review`:审批、驳回或作废人工调整凭证。
|
||||
- `tenant:reconciliation:download`:创建微信/支付宝官方账单下载任务。
|
||||
|
||||
官方账单下载由 `apps/worker --job provider-bills` 执行。API 只创建 `commerce_bill_download_jobs`,不会在前端返回供应商 `download_url`、商户私钥、微信 API v3 key 或支付宝应用私钥。worker 使用租户 `tenant_payment_accounts.config_public.secretRef` 找到 `app_private.tenant_secrets(secret_scope='payment')`,在后端签名申请下载 URL,校验微信返回的 `hash_type/hash_value`,解析 JSON/CSV/ZIP 账单后复用 `importReconciliationBatch` 写入 `commerce_reconciliation_batches/items`,`source='provider_download'`。
|
||||
|
||||
对账和差错工单只生成差异台账、处理记录和审计,不自动修改订单、支付、退款和权益。`resolve/ignore` 只是财务审核结论,例如 `manual_adjustment`、`provider_confirmed`、`false_positive`,最终落账仍必须走退款状态机、支付补偿、手工支付确认或后续专门的人工调整命令。人工调整凭证附件、财务复核报表、异常订单运营台和真实生产账单格式抽样验收仍需要继续补。
|
||||
对账、差错工单和人工调整凭证只生成差异台账、处理记录、凭证附件引用和审计,不自动修改订单、支付、退款和权益。`resolve/ignore` 只是财务审核结论,例如 `manual_adjustment`、`provider_confirmed`、`false_positive`;调整凭证审批也只表示财务复核通过。最终落账仍必须走退款状态机、支付补偿、手工支付确认或后续专门的落账命令。真实生产账单格式抽样验收和前端财务操作台仍需要继续补。
|
||||
|
||||
B 端合作商年费、服务费、服务器资源费不走学生端 `orders`,而是走平台账务:
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@
|
||||
| 资金流水对账 | 可联调 | `commerce_reconciliation_batches/items` + `/api/commerce/reconciliation/preview/import/batches/items/anomalies`;租户后台需 `tenant:reconciliation:read/write`,支持支付/退款账单行手工或 API 导入、来源 hash、批次统计、逐行匹配、金额/状态差异、本地缺失、供应商缺失、重复行、无效行和审计;对账只生成差异,不自动改订单/权益 |
|
||||
| 对账差错工单 | 可联调 | `commerce_reconciliation_issues/events` + `/api/commerce/reconciliation/issues*`;异常明细可创建工单,支持分配、开始处理、升级、解决、忽略、重开、事件轨迹和审计;处理结论只作为财务审核记录,不直接修改订单、支付、退款或权益 |
|
||||
| 官方账单下载 | 可联调 | `commerce_bill_download_jobs` + `/api/commerce/reconciliation/provider-bills/request/jobs` + `apps/worker --job provider-bills`;租户后台需 `tenant:reconciliation:download` 创建任务,worker 后端使用租户商户密钥申请微信/支付宝官方账单下载 URL、校验 hash、解析 JSON/CSV/ZIP 账单并复用同一套 `provider_download` 对账导入;响应只暴露任务状态、下载域名、hash 和对账批次 ID,不暴露下载 URL 或密钥 |
|
||||
| 异常订单运营台和财务报表 | 待补齐 | 后续补人工调整凭证附件/复核、异常订单运营台、财务复核报表和真实生产账单格式抽样验收 |
|
||||
| 异常订单运营台和财务报表 | 可联调 | `commerce_adjustment_vouchers/events` + `/api/commerce/operations/anomalies`、`/api/commerce/adjustment-vouchers*`;支持聚合未关闭对账工单、失败官方账单任务、支付事件错误、长时间 pending 支付/退款,支持人工调整凭证提交、审批、驳回、作废、事件轨迹和复核报表;需 `tenant:reconciliation:read/write/review`,凭证只做审计证据,不直接修改订单、支付、退款或权益;真实生产账单格式抽样和前端财务操作台待继续验收 |
|
||||
|
||||
## 租户后台与平台后台
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
| 刷题题库 | 已建题库、题目、题目版本、内容入口、任意深度分类树、考试意向标记、题目集合、练习蓝图、导入任务台账、导出任务台账、公共题库授权/采纳表、租户内容通知表 | 已支持核心映射,JSON/CSV/Excel 导入可落到新入口/节点/集合,阅读理解/案例分析子题沿用 `subQuestions/sub_questions` | 题目列表、内容入口、分类树、集合题目、顺序/随机/全真模拟 session、答题提交、复合题 `subAnswers` 判分和报告明细、租户后台题目录入/更新、JSON/CSV/Excel 预览/导入、JSON/试卷 payload 导出、PDF/Word 异步导出 worker、每日一练九宫格 metadata、PDF/Word 运营版式、ZIP 图片素材包、异步导入 worker、平台公共题库授权、租户采纳快照、手动同步、自动同步 worker、同步通知、冲突查询和单条/批量冲突处理已实现 | 核心 API 集成测试含导航、组卷、复合题后台录入/练习/判分/报告、导入、导出权限/脱敏、每日一练导出 metadata、异步 PDF/Word/每日一练 ZIP job 创建、exports worker、公共题库授权、采纳后组卷、同步新增题、通知隔离/已读/自动 resolved、租户自改冲突保护、单条/批量冲突处理和 worker 自动同步断言 | 新题库导航和组卷基础闭环可跑,阅读理解/案例分析多小题第一版可联调,公共题库采纳/手动/自动同步、同步通知、冲突查询/处理、导入后复检、模板下载、字段映射 API、JSON/PDF/Word/每日一练 ZIP 基础导出可联调;公共题库生产调度/失败告警、更精细导出模板和更完整运营消息仍需补齐 |
|
||||
| 错题本 | 已建 `wrong_questions` | 已支持旧错题归一化 | 错题列表、答题自动入错题、移出错题已实现 | 仅烟测 | 基础功能已实现,复习计划和统计未完成 |
|
||||
| 收藏夹 | 已建 `favorite_questions` | 已支持旧收藏归一化 | 收藏/取消收藏、收藏列表已实现 | 仅烟测 | 基础功能已实现 |
|
||||
| 用户订阅/题库会员/SVIP | 已建 `orders`、`payments`、`entitlements`、`svip_plans`、激活码 | 已映射旧 SVIP/会员权益 | 下单、订单详情/状态轮询、手工支付确认权限保护、微信/支付宝支付、微信/支付宝发起退款、微信/支付宝退款查询确认、微信/支付宝退款通知 webhook、激活码预检查/兑换、优惠券抵扣、零元订单自动开通、权益查询、支付/退款补偿、微信/支付宝官方账单下载、资金对账和差错工单已实现 | API 集成测试、commerce worker 集成测试 | 商城主链路可联调,异常订单运营台、真实生产账单格式验收和生产账号联调待补 |
|
||||
| 用户订阅/题库会员/SVIP | 已建 `orders`、`payments`、`entitlements`、`svip_plans`、激活码 | 已映射旧 SVIP/会员权益 | 下单、订单详情/状态轮询、手工支付确认权限保护、微信/支付宝支付、微信/支付宝发起退款、微信/支付宝退款查询确认、微信/支付宝退款通知 webhook、激活码预检查/兑换、优惠券抵扣、零元订单自动开通、权益查询、支付/退款补偿、微信/支付宝官方账单下载、资金对账、差错工单、异常订单运营台和人工调整凭证已实现 | API 集成测试、commerce worker 集成测试 | 商城主链路可联调,真实生产账单格式验收、生产账号联调和前端售后/财务体验待补 |
|
||||
| 背单词 | 已建单词单元、单词、进度、收藏表,并可绑定 `content_entries/content_nodes` | 已支持内容和部分用户状态映射 | 单元/单词只读、进度、收藏、统计、每日复习计划、租户后台单词维护 API、旧模板/新模板 JSON 预览导入、排行榜已实现 | 核心 API 集成测试含导入和排行榜断言 | 学生端学习状态、后台维护、批量 JSON 导入和基础排行榜已实现,更细复习参数和后台统计待完善 |
|
||||
| 知识手册 | 已建手册科目、章节、条目,并可绑定 `content_entries/content_nodes` | 已支持内容导入 | 只读 API、租户后台手册科目/章节/条目维护 API、嵌套 JSON 预览导入已实现 | 核心 API 集成测试含导入断言 | 学生端阅读、后台维护和批量 JSON 导入基础可用,富文本资源/版本管理待补 |
|
||||
| 分数线 | 已建院校、专业、字段、记录表 | 已支持导入映射 | 字段、院校、专业、记录、趋势、年份、租户后台维护 API、JSON 预览导入已实现 | 核心 API 集成测试含导入断言 | 查询、后台维护和批量 JSON 导入基础闭环已实现,复杂动态筛选和 AI 择校上下文待补 |
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
| 背单词 | `VocabularyPage.tsx`、`VocabularyQuiz.tsx` | 部分覆盖 | 单词列表、进度、收藏、统计、每日计划和后端复习调度已覆盖;后续补收藏练习体验、发音/音频策略、排行榜和更精细的间隔算法参数 |
|
||||
| 知识手册 | `Handbook*.tsx` | 已覆盖 | 前端需做好 Markdown/公式/图片渲染和搜索体验 |
|
||||
| 分数线 | `ScorelinePage.tsx` | 已覆盖 | 动态字段/趋势、后台维护和 JSON 批量导入已有;后续补复杂筛选优化和 AI 择校数据上下文 |
|
||||
| 商城/SVIP | `Store.tsx`、`SvipModal.tsx` | 部分覆盖 | 套餐、订单、订单详情/状态轮询、权益、激活码预检查/兑换、优惠券领取/下单抵扣、微信支付/支付宝 provider 主链路、内部退款状态机、微信/支付宝发起退款、退款查询确认、退款通知 webhook、支付/退款补偿 worker、全额退款权益撤销、资金对账手工/API 导入比对、微信/支付宝官方账单下载 worker、异常查询、差错工单和事件轨迹已有;缺异常订单运营台、人工调整凭证复核报表和前端收银台/售后体验 |
|
||||
| 商城/SVIP | `Store.tsx`、`SvipModal.tsx` | 部分覆盖 | 套餐、订单、订单详情/状态轮询、权益、激活码预检查/兑换、优惠券领取/下单抵扣、微信支付/支付宝 provider 主链路、内部退款状态机、微信/支付宝发起退款、退款查询确认、退款通知 webhook、支付/退款补偿 worker、全额退款权益撤销、资金对账手工/API 导入比对、微信/支付宝官方账单下载 worker、异常查询、差错工单和事件轨迹、异常订单运营台、人工调整凭证复核报表已有;缺前端收银台/售后体验、真实生产账单抽样验收和财务操作台 UI |
|
||||
| 个人中心 | `Profile.tsx` | 部分覆盖 | 基本资料、手机号绑定/换绑、权益、订单统计、练习历史、学习统计、签到积分、考试倒计时、趋势和勋章展示 API 已有;缺学习报告可视化 |
|
||||
| 资料下载 | `QuestionExporterPublishModal.tsx` 等 | 部分覆盖 | 资源台账、上传确认、签名下载、PDF/图片预览、动态水印上下文、worker 复检、内置安全扫描和外部 HTTP scanner 接入层已有;缺前端水印渲染、深度防盗链、真实 AV/内容安全服务联调和生命周期策略 |
|
||||
| AI 择校推荐 | 业务规划新增 | 部分覆盖 | 已有 SVIP 门禁、学生输入 schema、地区/分数线上下文、稳定 JSON 输出、报告台账、审计和 Taro 学生端基础页;真实 AI provider、prompt 版本管理后台、报告 PDF 渲染和更细推荐算法待补 |
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
- 支付/退款补偿 worker 已完成:`apps/worker --job commerce` 可查询微信/支付宝支付和处理中退款,补偿漏通知订单,支付成功幂等开通权益,退款成功幂等更新退款/订单/支付并在全额退款时撤销订单权益。
|
||||
- 资金对账和差错工单闭环已完成:`commerce_reconciliation_batches/items` 和 `/api/commerce/reconciliation/*` 支持手工/API 导入供应商账单行、预览差异、生成批次统计、查询异常、租户隔离、权限点 `tenant:reconciliation:read/write` 和审计日志;`commerce_reconciliation_issues/events` 支持异常明细创建工单、分配、开始处理、升级、解决、忽略、重开和事件留痕,且不直接修改订单/支付/退款/权益。
|
||||
- 微信/支付宝官方账单下载地基已完成:`commerce_bill_download_jobs`、`POST /api/commerce/reconciliation/provider-bills/request`、`GET /api/commerce/reconciliation/provider-bills/jobs` 和 `apps/worker --job provider-bills` 已接入,worker 负责后端签名申请下载 URL、hash 校验、JSON/CSV/ZIP 账单解析、复用 `provider_download` 对账导入、任务状态回写和密钥脱敏。
|
||||
- 异常订单运营台和人工调整凭证已完成后端第一版:`/api/commerce/operations/anomalies` 聚合未关闭对账工单、失败官方账单任务、支付事件错误、长时间 pending 支付/退款;`/api/commerce/adjustment-vouchers*` 支持凭证提交、审批、驳回、作废、事件轨迹和复核报表,使用 `tenant:reconciliation:review` 做独立复核权限,且审批凭证不会直接修改订单、支付、退款或权益。
|
||||
- 内容资源复检与安全扫描 worker 已完成:`apps/worker --job assets` 可复检 `content_assets` 中的托管对象元数据,并执行内置 `metadata_rules` 和可选外部 HTTP scanner;正常资源写回复检/扫描证据,异常资源自动置为 `failed/skipped + draft` 或 `security_scan_status=failed`,外部 scanner 不可用默认 fail-closed,并写入审计、扫描事件和安全标记。
|
||||
- 题库导出 worker 已完成:`apps/worker --job exports` 可抢占 `pdf/docx/daily_practice_zip` 导出任务,渲染 PDF/Word、水印或每日一练图片素材包,写入对象存储或本地开发存储,创建 `content_assets` 并回填 `assetId/hash/size`;`daily_practice` 已支持每日一练九宫格 metadata、PDF/Word 基础版式、9 张 PNG/SVG 卡片和拼图 ZIP。
|
||||
- 租户后台媒体运营报表已完成:`/api/tenant-content/media-analytics/summary`、`asset-events`、`video-events` 可按 7/30/90 天、资源、视频、用户和水印 traceId 查询资料访问、视频播放、拒绝访问、Top 资源/视频和日趋势;仅开放给 owner/admin/operator 或 `content:analytics:read` 权限角色,前端不会拿到签名 URL 或播放 token。
|
||||
@@ -76,7 +77,7 @@
|
||||
- 已完成微信支付 JSAPI、支付宝 WAP/H5 的创建支付参数和 webhook 幂等开通权益。
|
||||
- 已完成内部退款状态机、退款申请/审核/处理接口、微信/支付宝发起退款、微信/支付宝退款查询确认、微信/支付宝退款通知 webhook、部分/全额退款状态、全额退款权益撤销和审计事件。
|
||||
- 已完成支付/退款补偿 worker,可兜底供应商漏通知、处理中退款和重复执行幂等。
|
||||
- 已完成资金对账手工/API 导入比对、微信/支付宝官方账单下载任务、批次/明细/异常查询、差错工单状态流和审计;继续补真实生产账单格式抽样验收、人工调整凭证附件、财务复核报表和异常订单运营台。
|
||||
- 已完成资金对账手工/API 导入比对、微信/支付宝官方账单下载任务、批次/明细/异常查询、差错工单状态流、异常订单运营台、人工调整凭证和财务复核报表;继续补真实生产账单格式抽样验收和前端财务操作台体验。
|
||||
- 租户自有商户收款和平台代收/服务商模式。
|
||||
|
||||
2. 国内登录和短信
|
||||
@@ -115,7 +116,7 @@
|
||||
|
||||
8. 订单和营销体验
|
||||
- 已完成订单详情、订单状态轮询、激活码预检查、优惠券前台领取、下单抵扣计算和内部退款状态机。
|
||||
- 已完成支付/退款补偿 worker、官方账单下载 worker、资金对账导入比对和差错工单;继续补异常订单运营台、优惠券核销报表和复杂活动规则。
|
||||
- 已完成支付/退款补偿 worker、官方账单下载 worker、资金对账导入比对、差错工单、异常订单运营台和人工调整凭证复核;继续补优惠券核销报表、复杂活动规则和前端售后操作台。
|
||||
|
||||
9. 积分和反馈增强
|
||||
- 已完成每日签到、积分流水、反馈提交、租户后台处理、奖励积分幂等。
|
||||
|
||||
@@ -1853,7 +1853,7 @@ approved/processing -> failed
|
||||
- 退款通知地址由支付账户或 `submit_provider_refund.providerNotifyUrl` 配置,后端公开接收路径为 `POST /api/commerce/refunds/notify/wechat_pay?tenantId=<tenantId>`、`POST /api/commerce/refunds/notify/alipay?tenantId=<tenantId>`。这是支付平台回调地址,Taro 前端不要主动调用。
|
||||
- 退款通知只会推进已经审核/处理中的退款申请;未审核的 `requested` 退款不能被外部通知直接落账。
|
||||
- 已经 `succeeded` 的退款不能再次查询或再次标记成功,避免订单退款金额重复累加。前端应按接口返回状态展示,不要假设点击后立即到账。
|
||||
- 自动补偿 worker 已接入:支付漏通知和处理中退款会由后端定时查询供应商并幂等落账。资金对账已支持租户后台手工/API 导入供应商账单、微信/支付宝官方账单下载任务、查询差异和差错工单处理;异常订单运营台和人工调整凭证复核后续继续补。生产联调时仍需保留人工确认/失败登记入口。
|
||||
- 自动补偿 worker 已接入:支付漏通知和处理中退款会由后端定时查询供应商并幂等落账。资金对账已支持租户后台手工/API 导入供应商账单、微信/支付宝官方账单下载任务、查询差异、差错工单处理、异常订单运营台、人工调整凭证和复核报表。生产联调时仍需保留人工确认/失败登记入口。
|
||||
|
||||
### 租户后台资金对账
|
||||
|
||||
@@ -2057,6 +2057,112 @@ ignored 无效行或不符合本次 billType
|
||||
- 对账差异和差错工单只是运营判断依据,`resolve/ignore` 不会落账。最终订单修正必须走退款、补偿、人工确认或后续专门的人工调整接口。
|
||||
- 当前后端支持 JSON 行手工导入;官方账单下载 worker 支持微信/支付宝账单 JSON/CSV/ZIP 解析,并复用同一套对账导入逻辑。真实生产接入时仍要用真实账单文件抽样验收字段映射。
|
||||
|
||||
### 异常订单运营台和调整凭证
|
||||
|
||||
租户后台财务/售后页可以用异常运营台作为入口。该页只展示待处理风险和凭证复核状态,不允许前端直接修改订单、支付、退款或权益。
|
||||
|
||||
异常运营台:
|
||||
|
||||
```text
|
||||
GET /api/commerce/operations/anomalies?provider=wechat_pay&limit=50
|
||||
权限:tenant:reconciliation:read
|
||||
```
|
||||
|
||||
返回中 `items[].type` 可能是:
|
||||
|
||||
```text
|
||||
reconciliation_issue 未关闭对账差错工单
|
||||
provider_bill_job 官方账单下载失败或运行超时
|
||||
payment_event_error 支付/退款通知处理异常
|
||||
stuck_pending_payment 长时间 pending 支付
|
||||
stuck_refund 长时间待处理/处理中退款
|
||||
```
|
||||
|
||||
创建人工调整凭证:
|
||||
|
||||
```text
|
||||
POST /api/commerce/adjustment-vouchers
|
||||
权限:tenant:reconciliation:write
|
||||
body: {
|
||||
"voucherNo": "ADJ-20260629-001",
|
||||
"reconciliationIssueId": "<issueId>",
|
||||
"adjustmentType": "write_off",
|
||||
"direction": "decrease",
|
||||
"amountCents": 990,
|
||||
"title": "供应商缺失账单人工核销凭证",
|
||||
"description": "仅作为财务复核证据",
|
||||
"assetId": "<contentAssetId,可选>",
|
||||
"externalUrl": "https://finance.example.com/proofs/ADJ-20260629-001",
|
||||
"metadata": {
|
||||
"operatorRemark": "后台上传凭证"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
可关联的来源字段:
|
||||
|
||||
```text
|
||||
reconciliationIssueId 对账差错工单
|
||||
reconciliationItemId 对账明细
|
||||
orderId / orderNo 订单
|
||||
paymentId 支付记录
|
||||
refundRequestId/refundNo 退款申请
|
||||
sourceType=manual 纯人工凭证
|
||||
```
|
||||
|
||||
凭证字段:
|
||||
|
||||
```text
|
||||
adjustmentType:
|
||||
manual_payment_confirm | refund_correction | provider_confirmed |
|
||||
local_corrected | write_off | duplicate | other
|
||||
|
||||
direction:
|
||||
increase | decrease | none
|
||||
|
||||
status:
|
||||
draft | submitted | approved | rejected | voided
|
||||
```
|
||||
|
||||
查询凭证:
|
||||
|
||||
```text
|
||||
GET /api/commerce/adjustment-vouchers?status=submitted&orderNo=<orderNo>
|
||||
权限:tenant:reconciliation:read
|
||||
```
|
||||
|
||||
复核凭证:
|
||||
|
||||
```text
|
||||
POST /api/commerce/adjustment-vouchers/status
|
||||
权限:tenant:reconciliation:review
|
||||
body: {
|
||||
"voucherId": "<voucherId>",
|
||||
"status": "approved | rejected | voided",
|
||||
"reviewNote": "财务复核意见",
|
||||
"metadata": {
|
||||
"reviewChannel": "tenant-admin"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
查看凭证轨迹和报表:
|
||||
|
||||
```text
|
||||
GET /api/commerce/adjustment-vouchers/events?voucherId=<voucherId>
|
||||
GET /api/commerce/adjustment-vouchers/report?startDate=2026-06-01&endDate=2026-06-29
|
||||
权限:tenant:reconciliation:read
|
||||
```
|
||||
|
||||
前端处理规则:
|
||||
|
||||
- 学生端不要接这些接口。
|
||||
- `tenant:reconciliation:write` 可创建凭证,`tenant:reconciliation:review` 才能审批、驳回或作废凭证。
|
||||
- 后端会校验凭证来源和附件 `content_assets` 必须属于当前租户。
|
||||
- 已 `approved/rejected/voided` 的凭证不能翻转到其它关闭状态。
|
||||
- 凭证审批不会自动改订单、支付、退款和权益;真正落账仍要走退款状态机、支付补偿、手工支付确认或后续专门落账命令。
|
||||
- 前端可在差错工单 `resolve` 时把 `metadata.voucherNo` 一并传入,用于人读检索,但不要把它当成落账动作。
|
||||
|
||||
### 激活码预检查与兑换
|
||||
|
||||
兑换前建议先调用:
|
||||
|
||||
@@ -2541,6 +2541,180 @@ async function testCommerce() {
|
||||
});
|
||||
assert.equal(crossTenantProviderBillJobsDenied.code, 'TENANT_ADMIN_REQUIRED', 'provider bill jobs must be tenant isolated');
|
||||
|
||||
const operationReconItems = await request('/api/commerce/reconciliation/items', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { batchId: reconciliationImport.item.id, matchStatus: 'amount_mismatch' },
|
||||
});
|
||||
const operationReconItem = operationReconItems.items?.find(item => item.matchStatus === 'amount_mismatch');
|
||||
assert.ok(operationReconItem?.id, 'operation anomaly test should have an open actionable reconciliation item');
|
||||
|
||||
const operationIssue = await request('/api/commerce/reconciliation/issues/create', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
itemId: operationReconItem.id,
|
||||
summary: '运营台展示未关闭对账差错',
|
||||
note: '保持 open 供运营台测试',
|
||||
metadata: { source: 'operation-anomaly-test' },
|
||||
},
|
||||
});
|
||||
assert.equal(operationIssue.item?.status, 'open', 'operation anomaly issue should remain open');
|
||||
|
||||
const studentOperationsDenied = await request('/api/commerce/operations/anomalies', {
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(studentOperationsDenied.code, 'TENANT_ADMIN_REQUIRED', 'students must not access finance operation anomalies');
|
||||
|
||||
const operationAnomalies = await request('/api/commerce/operations/anomalies', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { provider: 'manual' },
|
||||
});
|
||||
assert.ok(
|
||||
operationAnomalies.items?.some(item => item.type === 'reconciliation_issue' && item.id === operationIssue.item.id),
|
||||
'operation anomaly console should surface open reconciliation issues',
|
||||
);
|
||||
|
||||
const voucherSourceOrderBefore = await request('/api/commerce/orders/status', {
|
||||
query: { orderNo: reconMissingProviderOrder.item.orderNo },
|
||||
});
|
||||
const voucherSourcePaymentBefore = voucherSourceOrderBefore.item?.payment;
|
||||
const voucherEntitlementsBefore = await request('/api/commerce/entitlements', {});
|
||||
|
||||
const studentAdjustmentDenied = await request('/api/commerce/adjustment-vouchers', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
reconciliationIssueId: reconciliationIssue.item.id,
|
||||
title: 'student should not create adjustment voucher',
|
||||
},
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(studentAdjustmentDenied.code, 'TENANT_ADMIN_REQUIRED', 'students must not create adjustment vouchers');
|
||||
|
||||
const adjustmentVoucherNo = `ADJ-INTEGRATION-${Date.now()}`;
|
||||
const adjustmentVoucher = await request('/api/commerce/adjustment-vouchers', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
voucherNo: adjustmentVoucherNo,
|
||||
reconciliationIssueId: reconciliationIssue.item.id,
|
||||
adjustmentType: 'write_off',
|
||||
direction: 'decrease',
|
||||
amountCents: reconMissingProviderOrder.item.amountCents,
|
||||
title: '供应商缺失账单人工核销凭证',
|
||||
description: '仅作为财务复核证据,不直接修改订单状态',
|
||||
externalUrl: 'https://finance.example.test/proofs/adj-integration-001',
|
||||
metadata: { source: 'api-integration-test' },
|
||||
},
|
||||
});
|
||||
assert.ok(adjustmentVoucher.item?.id, 'tenant admin should create adjustment voucher');
|
||||
assert.equal(adjustmentVoucher.item?.status, 'submitted', 'adjustment voucher should default to submitted');
|
||||
assert.equal(adjustmentVoucher.item?.reconciliationIssueId, reconciliationIssue.item.id, 'voucher should link reconciliation issue');
|
||||
assert.equal(adjustmentVoucher.item?.orderNo, reconMissingProviderOrder.item.orderNo, 'voucher should copy order identity from issue');
|
||||
|
||||
const duplicateAdjustmentVoucher = await request('/api/commerce/adjustment-vouchers', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
voucherNo: adjustmentVoucherNo,
|
||||
reconciliationIssueId: reconciliationIssue.item.id,
|
||||
title: 'duplicate voucher number should be rejected',
|
||||
},
|
||||
expectStatus: 409,
|
||||
});
|
||||
assert.equal(duplicateAdjustmentVoucher.code, 'ADJUSTMENT_VOUCHER_DUPLICATE', 'duplicate voucher number should return business conflict');
|
||||
|
||||
const invalidAdjustmentSource = await request('/api/commerce/adjustment-vouchers', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
reconciliationIssueId: 'not-a-uuid',
|
||||
title: 'invalid source id should be rejected',
|
||||
},
|
||||
expectStatus: 400,
|
||||
});
|
||||
assert.equal(invalidAdjustmentSource.code, 'INVALID_UUID', 'adjustment voucher should reject malformed source UUIDs');
|
||||
|
||||
const listedAdjustmentVouchers = await request('/api/commerce/adjustment-vouchers', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { status: 'submitted', orderNo: reconMissingProviderOrder.item.orderNo },
|
||||
});
|
||||
assert.ok(
|
||||
listedAdjustmentVouchers.items?.some(item => item.id === adjustmentVoucher.item.id),
|
||||
'adjustment voucher list should filter submitted vouchers by order number',
|
||||
);
|
||||
|
||||
const adjustmentReport = await request('/api/commerce/adjustment-vouchers/report', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { startDate: billDate, endDate: billDate },
|
||||
});
|
||||
assert.ok(adjustmentReport.item?.totalCount >= 1, 'adjustment voucher report should include created voucher');
|
||||
assert.ok(
|
||||
adjustmentReport.item?.byStatus?.some(item => item.status === 'submitted' && item.count >= 1),
|
||||
'adjustment voucher report should aggregate by status',
|
||||
);
|
||||
|
||||
const approvedAdjustmentVoucher = await request('/api/commerce/adjustment-vouchers/status', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
voucherId: adjustmentVoucher.item.id,
|
||||
status: 'approved',
|
||||
reviewNote: '财务复核通过,仅保留审计凭证',
|
||||
metadata: { reviewedFrom: 'api-integration-test' },
|
||||
},
|
||||
});
|
||||
assert.equal(approvedAdjustmentVoucher.item?.status, 'approved', 'reviewer should approve submitted adjustment voucher');
|
||||
assert.equal(approvedAdjustmentVoucher.item?.reviewedBy, TENANT_ADMIN_USER_ID, 'voucher approval should record reviewer');
|
||||
|
||||
const adjustmentVoucherEvents = await request('/api/commerce/adjustment-vouchers/events', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { voucherId: adjustmentVoucher.item.id },
|
||||
});
|
||||
assert.ok(
|
||||
adjustmentVoucherEvents.items?.some(item => item.eventType === 'submitted' && item.toStatus === 'submitted'),
|
||||
'voucher events should include submit event',
|
||||
);
|
||||
assert.ok(
|
||||
adjustmentVoucherEvents.items?.some(item => item.eventType === 'approved' && item.toStatus === 'approved'),
|
||||
'voucher events should include approval event',
|
||||
);
|
||||
|
||||
const closedVoucherChangeDenied = await request('/api/commerce/adjustment-vouchers/status', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
method: 'POST',
|
||||
body: {
|
||||
voucherId: adjustmentVoucher.item.id,
|
||||
status: 'rejected',
|
||||
reviewNote: 'closed voucher should not flip to rejected',
|
||||
},
|
||||
expectStatus: 409,
|
||||
});
|
||||
assert.equal(closedVoucherChangeDenied.code, 'ADJUSTMENT_VOUCHER_CLOSED', 'closed adjustment voucher must be immutable');
|
||||
|
||||
const crossTenantVoucherDenied = await request('/api/commerce/adjustment-vouchers', {
|
||||
tenantId: PARTNER_TENANT_ID,
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
query: { voucherNo: adjustmentVoucherNo },
|
||||
expectStatus: 403,
|
||||
});
|
||||
assert.equal(crossTenantVoucherDenied.code, 'TENANT_ADMIN_REQUIRED', 'adjustment vouchers must be tenant isolated');
|
||||
|
||||
const voucherSourceOrderAfter = await request('/api/commerce/orders/status', {
|
||||
query: { orderNo: reconMissingProviderOrder.item.orderNo },
|
||||
});
|
||||
const voucherEntitlementsAfter = await request('/api/commerce/entitlements', {});
|
||||
assert.equal(voucherSourceOrderAfter.item?.status, voucherSourceOrderBefore.item?.status, 'adjustment voucher approval must not mutate order status');
|
||||
assert.equal(
|
||||
voucherSourceOrderAfter.item?.payment?.status,
|
||||
voucherSourcePaymentBefore?.status,
|
||||
'adjustment voucher approval must not mutate payment status',
|
||||
);
|
||||
assert.equal(
|
||||
voucherEntitlementsAfter.items?.length,
|
||||
voucherEntitlementsBefore.items?.length,
|
||||
'adjustment voucher approval must not grant or revoke entitlements',
|
||||
);
|
||||
|
||||
const fakeWechatPay = await startFakeWechatPayServer();
|
||||
const wechatAccount = await request('/api/tenant-admin/payment-accounts', {
|
||||
userId: TENANT_ADMIN_USER_ID,
|
||||
@@ -6210,6 +6384,7 @@ async function testTenantMemberPermissionsAndAudit() {
|
||||
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'badges:grant'), 'permission matrix should expose badge grant permission');
|
||||
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'tenant:reconciliation:read'), 'permission matrix should expose reconciliation read permission');
|
||||
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'tenant:reconciliation:write'), 'permission matrix should expose reconciliation write permission');
|
||||
assert.ok(permissionMatrix.permissions?.some(item => item.key === 'tenant:reconciliation:review'), 'permission matrix should expose reconciliation review permission');
|
||||
assert.ok(permissionMatrix.menuGroups?.some(item => item.key === 'sales'), 'permission matrix should expose menu groups');
|
||||
assert.ok(permissionMatrix.roleDefaults?.tenant_operator?.includes('dashboard:read'), 'tenant operator defaults should include dashboard read');
|
||||
assert.ok(permissionMatrix.roleDefaults?.tenant_operator?.includes('marketing:*'), 'permission matrix should include role defaults');
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
create table if not exists public.commerce_adjustment_vouchers (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
tenant_id uuid not null references public.tenants(id) on delete cascade,
|
||||
voucher_no text not null,
|
||||
source_type text not null default 'manual'
|
||||
check (source_type in ('reconciliation_issue', 'reconciliation_item', 'order', 'payment', 'refund', 'manual')),
|
||||
source_id uuid,
|
||||
reconciliation_issue_id uuid references public.commerce_reconciliation_issues(id) on delete set null,
|
||||
reconciliation_item_id uuid references public.commerce_reconciliation_items(id) on delete set null,
|
||||
order_id uuid references public.orders(id) on delete set null,
|
||||
payment_id uuid references public.payments(id) on delete set null,
|
||||
refund_request_id uuid references public.commerce_refund_requests(id) on delete set null,
|
||||
order_no text,
|
||||
refund_no text,
|
||||
provider text,
|
||||
provider_trade_no text,
|
||||
provider_refund_no text,
|
||||
adjustment_type text not null default 'other'
|
||||
check (adjustment_type in (
|
||||
'manual_payment_confirm',
|
||||
'refund_correction',
|
||||
'provider_confirmed',
|
||||
'local_corrected',
|
||||
'write_off',
|
||||
'duplicate',
|
||||
'other'
|
||||
)),
|
||||
direction text not null default 'none'
|
||||
check (direction in ('increase', 'decrease', 'none')),
|
||||
amount_cents integer not null default 0 check (amount_cents >= 0),
|
||||
status text not null default 'draft'
|
||||
check (status in ('draft', 'submitted', 'approved', 'rejected', 'voided')),
|
||||
title text not null,
|
||||
description text,
|
||||
asset_id uuid references public.content_assets(id) on delete set null,
|
||||
external_url text,
|
||||
submitted_by uuid references public.platform_users(id) on delete set null,
|
||||
submitted_at timestamptz,
|
||||
reviewed_by uuid references public.platform_users(id) on delete set null,
|
||||
reviewed_at timestamptz,
|
||||
review_note text,
|
||||
voided_by uuid references public.platform_users(id) on delete set null,
|
||||
voided_at timestamptz,
|
||||
metadata jsonb not null default '{}'::jsonb,
|
||||
created_by uuid references public.platform_users(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
unique (tenant_id, voucher_no)
|
||||
);
|
||||
|
||||
create table if not exists public.commerce_adjustment_voucher_events (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
tenant_id uuid not null references public.tenants(id) on delete cascade,
|
||||
voucher_id uuid not null references public.commerce_adjustment_vouchers(id) on delete cascade,
|
||||
from_status text,
|
||||
to_status text,
|
||||
event_type text not null,
|
||||
actor_user_id uuid references public.platform_users(id) on delete set null,
|
||||
note text,
|
||||
details jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default now(),
|
||||
check (from_status is null or from_status in ('draft', 'submitted', 'approved', 'rejected', 'voided')),
|
||||
check (to_status is null or to_status in ('draft', 'submitted', 'approved', 'rejected', 'voided'))
|
||||
);
|
||||
|
||||
create index if not exists idx_commerce_adjustment_vouchers_tenant_status
|
||||
on public.commerce_adjustment_vouchers(tenant_id, status, created_at desc);
|
||||
|
||||
create index if not exists idx_commerce_adjustment_vouchers_source
|
||||
on public.commerce_adjustment_vouchers(tenant_id, source_type, source_id, created_at desc);
|
||||
|
||||
create index if not exists idx_commerce_adjustment_vouchers_issue
|
||||
on public.commerce_adjustment_vouchers(tenant_id, reconciliation_issue_id, status, created_at desc);
|
||||
|
||||
create index if not exists idx_commerce_adjustment_vouchers_order
|
||||
on public.commerce_adjustment_vouchers(tenant_id, order_no, status, created_at desc);
|
||||
|
||||
create index if not exists idx_commerce_adjustment_voucher_events_voucher
|
||||
on public.commerce_adjustment_voucher_events(tenant_id, voucher_id, created_at desc);
|
||||
|
||||
alter table public.commerce_adjustment_vouchers enable row level security;
|
||||
alter table public.commerce_adjustment_voucher_events enable row level security;
|
||||
|
||||
drop policy if exists tenant_isolation on public.commerce_adjustment_vouchers;
|
||||
create policy tenant_isolation on public.commerce_adjustment_vouchers
|
||||
for all
|
||||
using (tenant_id = app.current_tenant_id() or app.is_platform_admin())
|
||||
with check (tenant_id = app.current_tenant_id() or app.is_platform_admin());
|
||||
|
||||
drop policy if exists tenant_isolation on public.commerce_adjustment_voucher_events;
|
||||
create policy tenant_isolation on public.commerce_adjustment_voucher_events
|
||||
for all
|
||||
using (tenant_id = app.current_tenant_id() or app.is_platform_admin())
|
||||
with check (tenant_id = app.current_tenant_id() or app.is_platform_admin());
|
||||
|
||||
drop trigger if exists set_updated_at on public.commerce_adjustment_vouchers;
|
||||
create trigger set_updated_at
|
||||
before update on public.commerce_adjustment_vouchers
|
||||
for each row execute function app.touch_updated_at();
|
||||
Reference in New Issue
Block a user