@youzan-cloud/zan-saas-cli 0.1.0-beta.7 → 0.1.0-beta.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/internal/registry/manifest.json +21 -17
  2. package/npm-shrinkwrap.json +2 -2
  3. package/package.json +1 -1
  4. package/skills/zan-saas-order/SKILL.md +12 -1
  5. package/skills/zan-saas-order/references/branch-counts.md +45 -0
  6. package/skills/zan-saas-order/references/order-get.md +2 -0
  7. package/skills/zan-saas-order/references/order-list.md +2 -0
  8. package/skills/zan-saas-order/references/order-scan.md +2 -0
  9. package/skills/zan-saas-org/SKILL.md +2 -0
  10. package/skills/zan-saas-org/references/unit-list.md +2 -0
  11. package/skills/zan-saas-shared/SKILL.md +4 -0
  12. package/skills/zan-saas-shared/references/gateway-errors.md +29 -0
  13. package/skills/zan-saas-shared/references/shop-context.md +3 -1
  14. package/skills/zan-saas-valuecard/SKILL.md +8 -2
  15. package/skills/zan-saas-valuecard/references/transaction-list.md +25 -0
  16. package/skills/zan-saas-valuecard/references/transaction-scan.md +23 -0
  17. package/src/cli/context-help.js +3 -1
  18. package/src/commands/native/access/commands.index.json +1 -1
  19. package/src/commands/native/access/role/list/command.js +4 -32
  20. package/src/commands/native/order/commands.index.json +3 -3
  21. package/src/commands/native/order/order/get/command.js +14 -9
  22. package/src/commands/native/order/order/get/schema.json +197 -41
  23. package/src/commands/native/order/order/list/command.js +9 -6
  24. package/src/commands/native/order/order/list/schema.json +140 -10
  25. package/src/commands/native/order/order/scan/command.js +8 -5
  26. package/src/commands/native/order/order/scan/scan.js +1 -1
  27. package/src/commands/native/order/order/scan/schema.json +135 -8
  28. package/src/commands/native/order/shared/query.js +57 -9
  29. package/src/commands/native/order/shared/search.js +1 -1
  30. package/src/commands/native/org/commands.index.json +4 -4
  31. package/src/commands/native/valuecard/commands.index.json +20 -2
  32. package/src/commands/native/valuecard/domain.json +1 -1
  33. package/src/commands/native/valuecard/transaction/list/command.js +110 -0
  34. package/src/commands/native/valuecard/transaction/list/projection.js +71 -0
  35. package/src/commands/native/valuecard/transaction/list/schema.json +888 -0
  36. package/src/commands/native/valuecard/transaction/scan/command.js +130 -0
  37. package/src/commands/native/valuecard/transaction/scan/projection.js +58 -0
  38. package/src/commands/native/valuecard/transaction/scan/scan.js +70 -0
  39. package/src/commands/native/valuecard/transaction/scan/schema.json +954 -0
  40. package/src/commands/system/shop-contracts.js +35 -4
  41. package/src/execution/error-details.js +13 -18
  42. package/src/execution/gateway-guidance.js +43 -0
  43. package/src/execution/shop-query.js +6 -3
  44. package/src/identity/shop-location.js +18 -13
  45. package/src/identity/unit-membership.js +42 -0
  46. package/src/transport/api.js +1 -1
@@ -0,0 +1,130 @@
1
+ 'use strict';
2
+
3
+ const { Option } = require('commander');
4
+ const { DiagnosticCommand } = require('../../../../../cli/diagnostic-command');
5
+ const { withTargetShopDefault } = require('../../../../../execution/target-shop-default');
6
+ const { resolveTargetShop } = require('../../../../../execution/target-shop');
7
+ const { withInputScope, inputIssue, parseIntegerOption } = require('../../../../../execution/error-details');
8
+ const { validateCommandValue } = require('../../../../../execution/command-schema');
9
+ const { parseDateTime, formatDateTime, apiDateTime } = require('../../../../../execution/datetime');
10
+ const { createShopQuery, failure, bindingPath } = require('../../../../../execution/shop-query');
11
+ const { scan, projectPage, maxItems, maxRequests } = require('./scan');
12
+ const schema = require('./schema.json');
13
+
14
+ const command = 'valuecard.transaction.scan';
15
+ const filterFields = ['mobile', 'billCardNo', 'transactionNo', 'tradeNo'];
16
+ const reject = (category, stage, scope, state, facts) => failure(command, category, stage, scope, state, facts);
17
+
18
+ function body(input, pageNo) {
19
+ return { biz_type: input.bizType.toUpperCase(), begin_time: apiDateTime(input.occurredAt.start),
20
+ end_time: apiDateTime(input.occurredAt.end), page: pageNo, page_size: input.pageSize };
21
+ }
22
+
23
+ function normalize(options, scope) {
24
+ const parts = options.occurredAt.split('..');
25
+ let occurredAt;
26
+ try {
27
+ if (parts.length !== 2) throw new TypeError();
28
+ const [start, end] = parts.map(value => {
29
+ apiDateTime(value);
30
+ return formatDateTime(parseDateTime(value));
31
+ });
32
+ if (start >= end) throw new TypeError();
33
+ occurredAt = { start, end };
34
+ } catch {
35
+ throw inputIssue('/occurredAt', 'invalid_format', { format: 'beijing-date-time' });
36
+ }
37
+ const input = {
38
+ ...scope, bizType: options.bizType, occurredAt,
39
+ filters: Object.fromEntries(filterFields.filter(field => options[field] !== undefined).map(field => [field, options[field]])),
40
+ pageNo: parseIntegerOption(options.pageNo, schema, '/pageNo'),
41
+ pageSize: parseIntegerOption(options.pageSize, schema, '/pageSize'),
42
+ format: options.format, dryRun: Boolean(options.dryRun)
43
+ };
44
+ validateCommandValue(schema, 'input', input);
45
+ if (input.pageNo * input.pageSize > 5000) {
46
+ throw inputIssue('/pageNo', 'constraint', { maximum: Math.floor(5000 / input.pageSize) }, ['/pageSize']);
47
+ }
48
+ return input;
49
+ }
50
+
51
+ async function execute(input) {
52
+ const scope = { kdtId: input.kdtId };
53
+ let responseBytes = 0;
54
+ const read = await createShopQuery(scope, command, {
55
+ transportOptions: { maxResponseBytes: () => Math.min(10 * 1024 * 1024, 20 * 1024 * 1024 - responseBytes),
56
+ limitStage: 'response', shopApp: false },
57
+ onResponse: response => { responseBytes += response.responseBytes; }
58
+ });
59
+ const output = await scan(input, async pageNo => {
60
+ const response = await read(schema.api.path, body(input, pageNo), 'transactions');
61
+ try { return projectPage(response.data, input, pageNo); }
62
+ catch { throw reject('protocol', 'transactions-model', scope, 'sent', response.facts); }
63
+ });
64
+ validateCommandValue(schema, 'output', output);
65
+ return output;
66
+ }
67
+
68
+ function createCommand({ context } = {}) {
69
+ const leaf = new DiagnosticCommand('scan', { path: command, schema, parameterCategory: 'parameter',
70
+ optionPaths: Object.fromEntries(filterFields.map(field => [`/filters/${field}`, field])) })
71
+ .description('按业务类型和时间逐页读取储值流水,按原始标识本地精确查找')
72
+ .allowExcessArguments(false)
73
+ .requiredOption('--biz-type <type>', '必填:单一业务类型,支持值见 help/schema,不接受 all 或 CSV')
74
+ .requiredOption('--occurred-at <START..END>', '必填:北京时间 YYYY-MM-DD HH:mm:ss..YYYY-MM-DD HH:mm:ss,开始早于结束')
75
+ .option('--kdt-id <id>', '目标店铺正安全整数;省略取有效上下文 shopId,非总部不可跨店')
76
+ .option('--mobile <text>', '本地精确匹配流水手机号,可选 + 及 5..20 位数字,保留前导零')
77
+ .option('--bill-card-no <text>', '本地精确匹配流水卡号;不等于系统资产卡号,不改写原值')
78
+ .option('--transaction-no <text>', '本地精确匹配储值流水号;不承诺全局唯一')
79
+ .option('--trade-no <text>', '本地精确匹配有赞交易单号;不是外部支付请求号')
80
+ .option('--page-no <n>', '起始页,默认 1;页码乘容量不超过 5000,不证明前页已核实', '1')
81
+ .option('--page-size <n>', '固定每页容量 1..50,默认 20', '20')
82
+ .addOption(new Option('--format <format>', 'json 或 pretty,仅改变缩进').choices(['json', 'pretty']).default('json'))
83
+ .option('--dry-run', '零认证零请求预览,所有筛选值遮盖')
84
+ .addHelpText('after', `
85
+ 四个精确匹配条件至少一个,多条件为 AND;筛选仅在本地执行,不发送给服务端。
86
+ 固定最多 200 条匹配记录、21 次业务请求,包含一次同凭证店铺绑定;串行,不重试或自动切店。
87
+ 为完整页预留容量,可能少于 200 条;末页耗尽优先于预算停止。移除的预算选项不接受。
88
+ 单响应 10 MiB、累计响应 20 MiB、最终输出含换行 5 MiB;超限整体失败,不返回部分页。
89
+ transactions 保留无效记录、重复项及原始未知字典值,金额为 CNY 元两位字符串,时间为北京时间毫秒文本。
90
+ reportedBalance 时点未明确,amount 不代表统一增减方向,不推算余额变动或自动对账。
91
+ pagination 不提供匹配 total;原始页非空但匹配为空不等于无历史。traversalCompleted 只指本次起始页之后。
92
+ stopReason: pages_exhausted 已核实耗尽;max_items/max_requests 预算停止;window_limit 窗口停止;
93
+ total_unknown 总数未知;inconsistent_page 总数漂移,未接纳该页。后三者也不代表无历史。
94
+ 未完成且 nextPageNo 非空时,保持同店、类型、时间、筛选和 pageSize,用 --page-no 继续。
95
+ nextPageNo=null 不单独证明结束;窗口满时缩小时间范围,漂移时建议缩小范围从第一页重查。
96
+ 不承诺稳定排序、快照、全历史或稳定客户身份;手机号不是 yzOpenId,流水卡号不是 card.cardNo。
97
+ biz-type: ${schema.definitions.bizType.enum.join(', ')}
98
+ 真实调用需有效四字段上下文及同店凭证,at: all 不扩大产品、员工 OAuth 或跨店授权。
99
+ 新储值商家可查全部卡交易,老储值商家仅支持礼品卡流水;不把查询结果扩大为所有储值资产历史。
100
+ 后页失败整体失败,stdout 为空;先核查原范围,不自动重试、二分、换身份或重放。
101
+ 契约:zan-saas schema valuecard.transaction.scan
102
+ 示例:zan-saas valuecard transaction scan --biz-type pay --occurred-at "2026-11-01 00:00:00..2026-11-11 12:00:00" --bill-card-no bill-card-demo --kdt-id 63077 --dry-run
103
+ `)
104
+ .action(async options => {
105
+ const scope = resolveTargetShop(options, context, command);
106
+ const input = withInputScope(scope, () => normalize(options, scope));
107
+ const preview = { command, kind: 'atomic', risk: 'read', target: scope,
108
+ requests: [{ method: 'POST', path: bindingPath, body: {} },
109
+ { method: 'POST', path: schema.api.path, body: body(input, input.pageNo) }],
110
+ localFilters: Object.fromEntries(Object.keys(input.filters).map(field => [field, '***'])),
111
+ limits: { maxItems, maxRequests, pageSize: input.pageSize } };
112
+ validateCommandValue(schema, 'preview', preview);
113
+ const output = input.dryRun ? preview : await execute(input);
114
+ const text = `${JSON.stringify(output, null, input.format === 'pretty' ? 2 : 0)}\n`;
115
+ if (Buffer.byteLength(text, 'utf8') > 5 * 1024 * 1024) {
116
+ throw reject('limit', 'output', scope, input.dryRun ? 'not_sent' : 'sent');
117
+ }
118
+ process.stdout.write(text);
119
+ });
120
+ const supplied = new Set();
121
+ for (const option of leaf.options) {
122
+ leaf.on(`option:${option.name()}`, () => {
123
+ if (supplied.has(option.name())) throw leaf.issue('constraint', option);
124
+ supplied.add(option.name());
125
+ });
126
+ }
127
+ return leaf;
128
+ }
129
+
130
+ module.exports = { kind: 'atomic', risk: 'read', at: 'all', createCommand: withTargetShopDefault(createCommand) };
@@ -0,0 +1,58 @@
1
+ 'use strict';
2
+
3
+ const { object } = require('../../../../../execution/query-api');
4
+ const { normalizeMoney } = require('../../../../../execution/money');
5
+ const { fromBeijingSeconds } = require('../../../../../execution/beijing-date-time');
6
+
7
+ const requiredFields = {
8
+ transactionNo: 'consumption_no', billCardNo: 'group_no', bizType: 'biz_type', validity: 'valid'
9
+ };
10
+ const optionalFields = {
11
+ mobile: 'mobile', tradeNo: 'trade_no', externalRequestNo: 'out_request_no', remark: 'remark',
12
+ externalRemark: 'out_remark', nickname: 'nick_name', acceptShopName: 'accept_shop_name'
13
+ };
14
+ const control = /[\u0000-\u001f\u007f-\u009f]/u;
15
+
16
+ function requiredText(value) {
17
+ if (typeof value !== 'string' || !value.trim() || control.test(value)) {
18
+ throw new TypeError('Invalid transaction text');
19
+ }
20
+ return value;
21
+ }
22
+
23
+ function money(value) {
24
+ if (!Number.isSafeInteger(value)) throw new TypeError('Invalid transaction amount');
25
+ return normalizeMoney(value, { $comment: 'upstream-money:fen' });
26
+ }
27
+
28
+ function projectTransaction(item, filters) {
29
+ if (!object(item)) throw new TypeError('Invalid transaction');
30
+ const transaction = {};
31
+ for (const [field, source] of Object.entries(requiredFields)) transaction[field] = requiredText(item[source]);
32
+ transaction.occurredAt = fromBeijingSeconds(item.consumption_time);
33
+ transaction.amount = {
34
+ totalAmount: money(item.consumption_amount), principalAmount: money(item.principal_consumption_amount),
35
+ bonusAmount: money(item.bonus_consumption_amount)
36
+ };
37
+ const balances = [item.sum_dnom, item.principal_dnom, item.bonus_dnom];
38
+ if (!balances.every(Number.isSafeInteger) || BigInt(balances[0]) !== BigInt(balances[1]) + BigInt(balances[2])) {
39
+ throw new TypeError('Invalid reported balance');
40
+ }
41
+ transaction.reportedBalance = {
42
+ totalBalance: money(balances[0]), principalBalance: money(balances[1]), bonusBalance: money(balances[2])
43
+ };
44
+ for (const [field, source] of Object.entries(optionalFields)) {
45
+ if (Object.hasOwn(item, source)) {
46
+ if (typeof item[source] !== 'string') throw new TypeError('Invalid optional transaction text');
47
+ transaction[field] = item[source];
48
+ }
49
+ }
50
+ if (Object.hasOwn(item, 'kdt_id')) {
51
+ if (!Number.isSafeInteger(item.kdt_id) || item.kdt_id < 1) throw new TypeError('Invalid transaction shop');
52
+ transaction.kdtId = String(item.kdt_id);
53
+ }
54
+ for (const field of Object.keys(filters)) requiredText(transaction[field]);
55
+ return transaction;
56
+ }
57
+
58
+ module.exports = { projectTransaction };
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ const { object } = require('../../../../../execution/query-api');
4
+ const { readTotal } = require('../../../../../execution/list-total');
5
+ const { projectTransaction } = require('./projection');
6
+
7
+ const maxItems = 200;
8
+ const maxRequests = 21;
9
+
10
+ function projectPage(data, input, pageNo) {
11
+ const invalid = () => new TypeError('Invalid transaction page');
12
+ if (!object(data) || !object(data.paginator) || !Array.isArray(data.items)) throw invalid();
13
+ const paginator = data.paginator;
14
+ const total = readTotal(paginator, 'total_count', invalid);
15
+ if (paginator.page !== pageNo || paginator.page_size !== input.pageSize || data.items.length > input.pageSize
16
+ || (total !== null && data.items.length > Math.max(0, total - (pageNo - 1) * input.pageSize))) throw invalid();
17
+ const transactions = data.items.map(item => projectTransaction(item, input.filters));
18
+ return { total, transactions };
19
+ }
20
+
21
+ async function scan(input, readPage) {
22
+ const transactions = [];
23
+ const pagination = {
24
+ startPageNo: input.pageNo, pageSize: input.pageSize, scannedPages: 0, scannedRecords: 0,
25
+ returned: 0, requestCount: 1, hasMore: null, nextPageNo: null, traversalCompleted: false, stopReason: 'total_unknown'
26
+ };
27
+ let previousTotal;
28
+ let pageNo = input.pageNo;
29
+ while (true) {
30
+ const page = await readPage(pageNo);
31
+ pagination.requestCount += 1;
32
+ if (previousTotal !== undefined && page.total !== null && page.total !== previousTotal) {
33
+ pagination.hasMore = null;
34
+ pagination.nextPageNo = pageNo;
35
+ pagination.stopReason = 'inconsistent_page';
36
+ break;
37
+ }
38
+ pagination.scannedPages += 1;
39
+ pagination.scannedRecords += page.transactions.length;
40
+ transactions.push(...page.transactions.filter(transaction =>
41
+ Object.entries(input.filters).every(([field, value]) => transaction[field] === value)));
42
+ pagination.returned = transactions.length;
43
+ pagination.hasMore = page.total === null ? null : pageNo * input.pageSize < page.total;
44
+ if (pagination.hasMore === false) {
45
+ pagination.stopReason = 'pages_exhausted';
46
+ pagination.traversalCompleted = true;
47
+ break;
48
+ }
49
+ if (page.total === null) break;
50
+ if ((pageNo + 1) * input.pageSize > 5000) {
51
+ pagination.stopReason = 'window_limit';
52
+ break;
53
+ }
54
+ pagination.nextPageNo = pageNo + 1;
55
+ if (maxItems - transactions.length < input.pageSize) {
56
+ pagination.stopReason = 'max_items';
57
+ break;
58
+ }
59
+ if (pagination.requestCount >= maxRequests) {
60
+ pagination.stopReason = 'max_requests';
61
+ break;
62
+ }
63
+ previousTotal = page.total;
64
+ pageNo += 1;
65
+ pagination.nextPageNo = null;
66
+ }
67
+ return { data: { transactions, pagination } };
68
+ }
69
+
70
+ module.exports = { scan, projectPage, maxItems, maxRequests };