@tbox.cn/app-contracts-mall 0.1.1 → 0.9.0

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 (78) hide show
  1. package/README.md +16 -2
  2. package/dist/alipay-activity/cards.d.ts +305 -0
  3. package/dist/assembly.d.ts +54 -0
  4. package/dist/chunk-KIZD6FZ2.js +43 -0
  5. package/dist/deployment.d.ts +42 -0
  6. package/dist/domain/activity.d.ts +64 -0
  7. package/dist/domain/coupon.d.ts +2 -0
  8. package/dist/domain/entitlement.d.ts +53 -0
  9. package/dist/domain/offer.d.ts +111 -0
  10. package/dist/domain/operation.d.ts +22 -0
  11. package/dist/domain/parking.d.ts +195 -12
  12. package/dist/domain/points.d.ts +3 -2
  13. package/dist/domain/shopping-guide.d.ts +201 -0
  14. package/dist/guard.d.ts +50 -0
  15. package/dist/index.d.ts +22 -2
  16. package/dist/member/cards.d.ts +940 -0
  17. package/dist/parking/cards.d.ts +1271 -0
  18. package/dist/promotion/cards.d.ts +2529 -0
  19. package/dist/query-context.d.ts +47 -0
  20. package/dist/runtime.d.ts +20 -0
  21. package/dist/runtime.js +1281 -0
  22. package/dist/scope.d.ts +76 -0
  23. package/dist/server/index.d.ts +11 -0
  24. package/dist/server/index.js +80 -0
  25. package/dist/server/mall-context.d.ts +24 -0
  26. package/dist/server/mall-control-plane.d.ts +8 -0
  27. package/dist/server/request-credentials.d.ts +22 -0
  28. package/dist/services/customer-service.d.ts +11 -0
  29. package/dist/services/mall-info.d.ts +26 -0
  30. package/dist/services/member.d.ts +243 -9
  31. package/dist/services/parking.d.ts +105 -12
  32. package/dist/services/promotion.d.ts +100 -4
  33. package/dist/services/shopping-guide.d.ts +97 -0
  34. package/dist/shared/cards.d.ts +42 -0
  35. package/dist/shopping-guide/cards.d.ts +7039 -0
  36. package/package.json +19 -8
  37. package/src/alipay-activity/cards.ts +66 -0
  38. package/src/assembly.ts +103 -0
  39. package/src/deployment.ts +44 -0
  40. package/src/domain/activity.ts +66 -0
  41. package/src/domain/coupon.ts +3 -0
  42. package/src/domain/entitlement.ts +56 -0
  43. package/src/domain/offer.ts +120 -0
  44. package/src/domain/operation.ts +40 -0
  45. package/src/domain/parking.ts +206 -12
  46. package/src/domain/points.ts +3 -2
  47. package/src/domain/shopping-guide.ts +242 -0
  48. package/src/guard.ts +48 -0
  49. package/src/index.ts +22 -2
  50. package/src/member/cards.ts +209 -0
  51. package/src/parking/cards.ts +233 -0
  52. package/src/promotion/cards.ts +290 -0
  53. package/src/query-context.ts +81 -0
  54. package/src/runtime.ts +41 -0
  55. package/src/scope.ts +92 -0
  56. package/src/server/index.ts +11 -0
  57. package/src/server/mall-context.ts +39 -0
  58. package/src/server/mall-control-plane.ts +56 -0
  59. package/src/server/request-credentials.ts +43 -0
  60. package/src/service-slots.json +23 -0
  61. package/src/services/customer-service.ts +12 -0
  62. package/src/services/mall-info.ts +31 -0
  63. package/src/services/member.ts +354 -9
  64. package/src/services/parking.ts +161 -11
  65. package/src/services/promotion.ts +124 -4
  66. package/src/services/shopping-guide.ts +138 -0
  67. package/src/shared/cards.ts +25 -0
  68. package/src/shopping-guide/cards.ts +438 -0
  69. package/tests/assembly-validate-scope.test.ts +132 -0
  70. package/tests/cards.test.ts +586 -0
  71. package/tests/context-unification.test.ts +73 -0
  72. package/tests/mall-control-plane.test.ts +86 -0
  73. package/tests/member-status.test.ts +197 -0
  74. package/tests/query-context.test.ts +111 -0
  75. package/tests/request-credentials.test.ts +38 -0
  76. package/tests/resolve-primitives.test.ts +55 -0
  77. package/tests/service-slots.test.ts +78 -0
  78. package/tsconfig.json +1 -1
@@ -0,0 +1,438 @@
1
+ /**
2
+ * 导购域卡片数据契约(011 S7:shopping-guide 11 卡 zod schema,TIER1)。
3
+ * 源:POC @tbox/shopping-guide contracts(TypeBox)→ zod 重写(coerce 语义按 D11 字符串化收窄)。
4
+ */
5
+ import { z } from 'zod';
6
+
7
+ // ===== 通用显示值 =====
8
+
9
+ export const httpsUrlSchema = z.string().regex(/^https:\/\//).max(2048);
10
+ export const isoDateTimeSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/);
11
+
12
+ export const moneySchema = z.object({
13
+ currency: z.literal('CNY'),
14
+ amountMinor: z.number().int().min(0),
15
+ display: z.string().min(1),
16
+ });
17
+
18
+ export const priceSummarySchema = z.object({
19
+ sale: moneySchema.optional(),
20
+ original: moneySchema.optional(),
21
+ promotion: moneySchema.optional(),
22
+ });
23
+
24
+ export const evidenceSchema = z.object({
25
+ code: z.string().min(1),
26
+ label: z.string().min(1),
27
+ value: z.string().optional(),
28
+ observedAt: isoDateTimeSchema,
29
+ expiresAt: isoDateTimeSchema.optional(),
30
+ sourceKind: z.enum(['catalog', 'merchant', 'mall', 'policy']),
31
+ });
32
+
33
+ export const reasonItemSchema = z.object({ code: z.string().min(1), label: z.string().min(1) });
34
+
35
+ export const merchantLocationSchema = z.object({
36
+ buildingName: z.string().optional(),
37
+ floorName: z.string().optional(),
38
+ unitName: z.string().optional(),
39
+ displayText: z.string().min(1),
40
+ poiId: z.string().optional(),
41
+ });
42
+ /** @deprecated 使用 merchantLocationSchema(字段保持兼容,类型语义已升级为 shop location) */
43
+ export const shopLocationSchema = merchantLocationSchema;
44
+
45
+ export const merchantStatusSchema = z.object({
46
+ code: z.enum(['enabled', 'disabled', 'unknown']),
47
+ label: z.string().min(1),
48
+ observedAt: isoDateTimeSchema,
49
+ });
50
+ /** @deprecated 使用 merchantStatusSchema(字段保持兼容) */
51
+ export const shopStatusSchema = merchantStatusSchema;
52
+
53
+ export const pageInfoSchema = z.object({
54
+ page: z.number().int().min(1),
55
+ // max 200(0.7.5 放宽,原 100):品牌导览常驻入口经 port 直调 pageSize=200 一次拉全量,
56
+ // provider 回显值入 pageInfo 需过卡 schema(card-emitter safeParse);向后兼容(旧值域 ⊂ 新值域)
57
+ pageSize: z.number().int().min(1).max(200),
58
+ total: z.number().int().min(0),
59
+ hasMore: z.boolean(),
60
+ });
61
+
62
+ export const refinementSchema = z.object({ label: z.string().min(1), queryText: z.string().min(1) });
63
+
64
+ const shopCouponSchema = z.object({
65
+ name: z.string().min(1),
66
+ imageUrl: httpsUrlSchema.optional(),
67
+ description: z.string().optional(),
68
+ faceValue: z.number().optional(),
69
+ salePrice: z.number().optional(),
70
+ applicableCategories: z.array(z.string()).optional(),
71
+ validHours: z.number().optional(),
72
+ usageTimeText: z.string().optional(),
73
+ purchaseUrl: z.string().max(2048).optional(),
74
+ freeCouponClaimMode: z.enum(['DIRECT', 'REDIRECT']).optional(),
75
+ offerRef: z.string().min(1).optional(),
76
+ });
77
+
78
+ /**
79
+ * 卡内条目动作草稿(0.3.2 D4:单源移至 shared/cards.ts,本处 import + re-export 保既有消费方)。
80
+ */
81
+ import { actionDraftSchema } from '../shared/cards';
82
+ export { actionDraftSchema };
83
+ export type { ActionDraft } from '../shared/cards';
84
+
85
+ // ===== 11 卡数据 schema =====
86
+
87
+ /** product-list(context: recommendation/search/similar/history) */
88
+ export const productListCardDataSchema = z.object({
89
+ context: z.enum(['recommendation', 'search', 'similar', 'history']),
90
+ summary: z.string().optional(),
91
+ items: z.array(
92
+ z.object({
93
+ productId: z.string().min(1),
94
+ name: z.string().min(1),
95
+ imageUrl: httpsUrlSchema.optional(),
96
+ brand: z.string().optional(),
97
+ categoryPath: z.array(z.string()),
98
+ price: priceSummarySchema,
99
+ availability: z.object({
100
+ status: z.enum(['in_stock', 'out_of_stock', 'limited', 'unknown']),
101
+ label: z.string(),
102
+ marketable: z.boolean(),
103
+ observedAt: isoDateTimeSchema,
104
+ }),
105
+ merchant: z.object({
106
+ merchantId: z.string(),
107
+ name: z.string(),
108
+ location: merchantLocationSchema.optional(),
109
+ }),
110
+ badges: z.array(z.string()),
111
+ reasons: z.array(reasonItemSchema).optional(),
112
+ evidence: z.array(evidenceSchema).optional(),
113
+ caveats: z.array(z.string()),
114
+ itemActions: z.array(actionDraftSchema),
115
+ }),
116
+ ),
117
+ noResultsReason: z.string().optional(),
118
+ pageInfo: pageInfoSchema.optional(),
119
+ refinements: z.array(refinementSchema),
120
+ });
121
+ export type ProductListCardData = z.infer<typeof productListCardDataSchema>;
122
+
123
+ /** shop-list(context: recommendation/search,原 merchant-list) */
124
+ export const shopListCardDataSchema = z.object({
125
+ context: z.enum(['recommendation', 'search']),
126
+ /** mall scope 透传:供客户端侧边栏详情 HTTP 请求携带 mallId 与 miniAppId(工具执行时注入) */
127
+ mallId: z.string().optional(),
128
+ /** 小程序 appId(miniAppId 唯一真值透传;HTTP /shop-detail 鉴权用) */
129
+ miniAppId: z.string().optional(),
130
+ summary: z.string().optional(),
131
+ categoryTabs: z.array(z.object({ code: z.string(), name: z.string() })).optional(),
132
+ items: z.array(
133
+ z.object({
134
+ merchantId: z.string(),
135
+ name: z.string(),
136
+ logoUrl: httpsUrlSchema.optional(),
137
+ coverUrl: httpsUrlSchema.optional(),
138
+ brand: z.string().optional(),
139
+ businessCategories: z.array(z.string()),
140
+ location: merchantLocationSchema,
141
+ status: merchantStatusSchema,
142
+ contactInfo: z
143
+ .array(z.object({ channel: z.string(), value: z.string() }))
144
+ .optional(),
145
+ services: z.array(z.string()),
146
+ promotionFacts: z.array(z.string()),
147
+ reasons: z.array(reasonItemSchema).optional(),
148
+ evidence: z.array(evidenceSchema).optional(),
149
+ itemActions: z.array(actionDraftSchema),
150
+ /** 门店关联优惠券(列表卡透传,供点击下钻时直接展示,避免二次请求) */
151
+ coupons: z.array(shopCouponSchema).optional(),
152
+ }),
153
+ ),
154
+ noResultsReason: z.string().optional(),
155
+ pageInfo: pageInfoSchema.optional(),
156
+ refinements: z.array(refinementSchema),
157
+ });
158
+ export type ShopListCardData = z.infer<typeof shopListCardDataSchema>;
159
+ /** @deprecated 使用 ShopListCardData */
160
+ export type MerchantListCardData = ShopListCardData;
161
+ /** @deprecated 使用 shopListCardDataSchema */
162
+ export const merchantListCardDataSchema = shopListCardDataSchema;
163
+
164
+ /** product-detail */
165
+ export const productDetailCardDataSchema = z.object({
166
+ productId: z.string().min(1),
167
+ name: z.string().min(1),
168
+ description: z.string().optional(),
169
+ imageUrls: z.array(httpsUrlSchema),
170
+ brand: z.string().optional(),
171
+ categoryPath: z.array(z.string()),
172
+ price: priceSummarySchema,
173
+ availability: z.object({
174
+ status: z.enum(['in_stock', 'out_of_stock', 'limited', 'unknown']),
175
+ label: z.string(),
176
+ marketable: z.boolean(),
177
+ observedAt: isoDateTimeSchema,
178
+ }),
179
+ merchant: z.object({
180
+ merchantId: z.string(),
181
+ name: z.string(),
182
+ location: merchantLocationSchema.optional(),
183
+ }),
184
+ attributes: z.array(z.object({ name: z.string(), value: z.string() })),
185
+ promotionFacts: z.array(z.string()),
186
+ evidence: z.array(evidenceSchema),
187
+ humanGuideAvailable: z.boolean(),
188
+ itemActions: z.array(actionDraftSchema),
189
+ });
190
+ export type ProductDetailCardData = z.infer<typeof productDetailCardDataSchema>;
191
+
192
+ /** shop-detail(原 merchant-detail) */
193
+ export const shopDetailCardDataSchema = z.object({
194
+ merchantId: z.string(),
195
+ name: z.string(),
196
+ /** mall scope 透传:供客户端 Sheet 后台 detail HTTP 请求携带(工具执行时注入,与 shop-list 同源) */
197
+ mallId: z.string().optional(),
198
+ /** 小程序 appId(miniAppId 唯一真值透传;HTTP /shop-detail 鉴权用) */
199
+ miniAppId: z.string().optional(),
200
+ logoUrl: httpsUrlSchema.optional(),
201
+ coverUrls: z.array(httpsUrlSchema),
202
+ brand: z.string().optional(),
203
+ businessCategories: z.array(z.string()),
204
+ location: merchantLocationSchema,
205
+ status: merchantStatusSchema,
206
+ description: z.string().optional(),
207
+ contactInfo: z
208
+ .array(z.object({ channel: z.string(), value: z.string() }))
209
+ .optional(),
210
+ services: z.array(z.string()),
211
+ promotionFacts: z.array(z.string()),
212
+ /** 门店关联优惠券列表(来自 couponList;name/imageUrl/faceValue/salePrice/applicableCategories/validHours) */
213
+ coupons: z.array(shopCouponSchema).optional(),
214
+ evidence: z.array(evidenceSchema),
215
+ itemActions: z.array(actionDraftSchema),
216
+ });
217
+ export type ShopDetailCardData = z.infer<typeof shopDetailCardDataSchema>;
218
+ /** @deprecated 使用 ShopDetailCardData */
219
+ export type MerchantDetailCardData = ShopDetailCardData;
220
+ /** @deprecated 使用 shopDetailCardDataSchema */
221
+ export const merchantDetailCardDataSchema = shopDetailCardDataSchema;
222
+
223
+ /** category-list */
224
+ export const categoryListCardDataSchema = z.object({
225
+ categories: z.array(
226
+ z.object({
227
+ code: z.string().min(1),
228
+ name: z.string().min(1),
229
+ description: z.string().optional(),
230
+ /** 类目图标 URL;undefined 时前端展示默认图标 */
231
+ iconUrl: httpsUrlSchema.optional(),
232
+ merchantCount: z.number().int().min(0),
233
+ subCategories: z
234
+ .array(z.object({ code: z.string(), name: z.string(), merchantCount: z.number().int().min(0) }))
235
+ .optional(),
236
+ /** 0.3.2 D4:类别条目动作(send 合成话术/外链) */
237
+ itemActions: z.array(actionDraftSchema).optional(),
238
+ }),
239
+ ),
240
+ /** 商场楼栋-楼层结构(与 mall-directory 同语义子集,供 category-list 卡片展示楼层导航) */
241
+ buildings: z
242
+ .array(
243
+ z.object({
244
+ buildingCode: z.string(),
245
+ buildingName: z.string(),
246
+ floors: z.array(
247
+ z.object({
248
+ floorCode: z.string(),
249
+ floorName: z.string(),
250
+ businessCategories: z.array(z.string()),
251
+ merchantCount: z.number().int().min(0),
252
+ /** 楼层条目动作(send:查看该楼层门店列表) */
253
+ itemActions: z.array(actionDraftSchema).optional(),
254
+ }),
255
+ ),
256
+ }),
257
+ )
258
+ .optional(),
259
+ observedAt: isoDateTimeSchema,
260
+ });
261
+ export type CategoryListCardData = z.infer<typeof categoryListCardDataSchema>;
262
+
263
+ /** product-comparison(2-4 商品并排) */
264
+ export const productComparisonCardDataSchema = z.object({
265
+ products: z.array(
266
+ z.object({
267
+ productId: z.string().min(1),
268
+ name: z.string().min(1),
269
+ imageUrl: httpsUrlSchema.optional(),
270
+ brand: z.string().optional(),
271
+ price: priceSummarySchema,
272
+ availability: z.object({
273
+ status: z.enum(['in_stock', 'out_of_stock', 'limited', 'unknown']),
274
+ label: z.string(),
275
+ marketable: z.boolean(),
276
+ observedAt: isoDateTimeSchema,
277
+ }),
278
+ merchant: z.object({ merchantId: z.string(), name: z.string() }),
279
+ attributes: z.array(z.object({ name: z.string(), value: z.string() })),
280
+ caveats: z.array(z.string()),
281
+ }),
282
+ ),
283
+ /** 0.3.2 D4:刷新截止时间(ISO);到期后渲染端禁用全部条目交互 */
284
+ refreshRequiredAt: isoDateTimeSchema.optional(),
285
+ observedAt: isoDateTimeSchema,
286
+ });
287
+ export type ProductComparisonCardData = z.infer<typeof productComparisonCardDataSchema>;
288
+
289
+ /** mall-directory(楼栋-楼层结构) */
290
+ export const mallDirectoryCardDataSchema = z.object({
291
+ mallId: z.string(),
292
+ mallName: z.string(),
293
+ buildings: z.array(
294
+ z.object({
295
+ buildingCode: z.string(),
296
+ buildingName: z.string(),
297
+ floors: z.array(
298
+ z.object({
299
+ floorCode: z.string(),
300
+ floorName: z.string(),
301
+ businessCategories: z.array(z.string()),
302
+ merchantCount: z.number().int().min(0),
303
+ /** 0.3.2 D4:楼层条目动作(send:查看楼层门店) */
304
+ itemActions: z.array(actionDraftSchema).optional(),
305
+ }),
306
+ ),
307
+ }),
308
+ ),
309
+ observedAt: isoDateTimeSchema,
310
+ });
311
+ export type MallDirectoryCardData = z.infer<typeof mallDirectoryCardDataSchema>;
312
+
313
+ /** nearby-mall-list */
314
+ export const nearbyMallListCardDataSchema = z.object({
315
+ query: z.string().optional(),
316
+ malls: z.array(
317
+ z.object({
318
+ mallId: z.string(),
319
+ name: z.string(),
320
+ address: z.string(),
321
+ distanceKm: z.number().min(0).optional(),
322
+ status: z.object({ code: z.enum(['open', 'closed', 'unknown']), label: z.string() }),
323
+ itemActions: z.array(actionDraftSchema),
324
+ }),
325
+ ),
326
+ });
327
+ export type NearbyMallListCardData = z.infer<typeof nearbyMallListCardDataSchema>;
328
+
329
+ /** customer-service-faq */
330
+ export const customerServiceFaqCardDataSchema = z.object({
331
+ query: z.string().min(1),
332
+ faqs: z.array(
333
+ z.object({
334
+ question: z.string().min(1),
335
+ answer: z.string().min(1),
336
+ category: z.string().optional(),
337
+ relevanceScore: z.number().min(0).max(1),
338
+ /** 0.3.2 D4:答案参考外链(openLink) */
339
+ referenceUrl: httpsUrlSchema.optional(),
340
+ }),
341
+ ),
342
+ /** 客服电话(015:deployment.malls 经 mall-directory 服务注入;缺省不渲染拨打条目) */
343
+ contactPhone: z.string().min(1).optional(),
344
+ noResultsReason: z.string().optional(),
345
+ });
346
+ export type CustomerServiceFaqCardData = z.infer<typeof customerServiceFaqCardDataSchema>;
347
+
348
+ /** human-guide-list */
349
+ export const humanGuideListCardDataSchema = z.object({
350
+ merchantId: z.string(),
351
+ merchantName: z.string(),
352
+ guides: z.array(
353
+ z.object({
354
+ guideId: z.string(),
355
+ name: z.string(),
356
+ title: z.string().optional(),
357
+ avatarUrl: httpsUrlSchema.optional(),
358
+ contactInfo: z
359
+ .array(z.object({ channel: z.string(), value: z.string() }))
360
+ .optional(),
361
+ available: z.boolean(),
362
+ itemActions: z.array(actionDraftSchema),
363
+ }),
364
+ ),
365
+ });
366
+ export type HumanGuideListCardData = z.infer<typeof humanGuideListCardDataSchema>;
367
+
368
+ /** location-handoff */
369
+ export const locationHandoffCardDataSchema = z.object({
370
+ merchantId: z.string(),
371
+ merchantName: z.string(),
372
+ location: merchantLocationSchema,
373
+ mapUrl: httpsUrlSchema.optional(),
374
+ navigation: z
375
+ .object({
376
+ provider: z.enum(['amap', 'builtin']),
377
+ launchParams: z.record(z.string(), z.string()).optional(),
378
+ })
379
+ .optional(),
380
+ itemActions: z.array(actionDraftSchema),
381
+ });
382
+ export type LocationHandoffCardData = z.infer<typeof locationHandoffCardDataSchema>;
383
+
384
+ /** recommend-merchants:类目 Tab + 门店推荐列表(按偏好/类目推荐场景专用) */
385
+ export const recommendMerchantsCardDataSchema = z.object({
386
+ summary: z.string().optional(),
387
+ /** 类目筛选 Tab(来自 list-categories;有值时渲染 Tab 栏) */
388
+ categoryTabs: z.array(
389
+ z.object({
390
+ code: z.string(),
391
+ name: z.string(),
392
+ }),
393
+ ).optional(),
394
+ items: z.array(
395
+ z.object({
396
+ merchantId: z.string(),
397
+ name: z.string(),
398
+ logoUrl: httpsUrlSchema.optional(),
399
+ coverUrl: httpsUrlSchema.optional(),
400
+ brand: z.string().optional(),
401
+ businessCategories: z.array(z.string()),
402
+ location: merchantLocationSchema,
403
+ status: merchantStatusSchema,
404
+ description: z.string().optional(),
405
+ contactInfo: z.array(z.object({ channel: z.string(), value: z.string() })).optional(),
406
+ services: z.array(z.string()).optional(),
407
+ /** 营业时间/经营状态文本(如 "10:00营业"),来自知识库 经营状态 字段 */
408
+ operatingStatus: z.string().optional(),
409
+ /** 评分(如 "4.8"),来自知识库 评分 字段 */
410
+ rating: z.string().optional(),
411
+ /** 人均消费文本(如 "¥202"),来自知识库 人均消费 字段(分为单位时自动格式化) */
412
+ avgSpend: z.string().optional(),
413
+ promotionFacts: z.array(z.string()),
414
+ itemActions: z.array(actionDraftSchema),
415
+ }),
416
+ ),
417
+ noResultsReason: z.string().optional(),
418
+ observedAt: isoDateTimeSchema,
419
+ });
420
+ export type RecommendMerchantsCardData = z.infer<typeof recommendMerchantsCardDataSchema>;
421
+
422
+ /** 11 卡 meta 数据 schema map(cardType → dataSchema,server 卡 meta 引用) */
423
+ export const shoppingGuideCardDataSchemas = {
424
+ 'product-list': productListCardDataSchema,
425
+ 'product-detail': productDetailCardDataSchema,
426
+ 'shop-list': shopListCardDataSchema,
427
+ 'shop-detail': shopDetailCardDataSchema,
428
+ 'category-list': categoryListCardDataSchema,
429
+ 'product-comparison': productComparisonCardDataSchema,
430
+ 'mall-directory': mallDirectoryCardDataSchema,
431
+ 'nearby-mall-list': nearbyMallListCardDataSchema,
432
+ 'customer-service-faq': customerServiceFaqCardDataSchema,
433
+ 'human-guide-list': humanGuideListCardDataSchema,
434
+ 'location-handoff': locationHandoffCardDataSchema,
435
+ 'recommend-merchants': recommendMerchantsCardDataSchema,
436
+ } as const;
437
+
438
+ export type ShoppingGuideCardType = keyof typeof shoppingGuideCardDataSchemas;
@@ -0,0 +1,132 @@
1
+ /**
2
+ * validateScope/createMallContextValidator 形状守卫(integrations v4 恒动态):
3
+ * 白名单/动态判定(dynamicMalls)已退役——参考表纯元数据,任意 mallId 建会话;
4
+ * 校验唯一职责 = mallId 形状(会话边界键非空)。两形态共享 isValidMallShape 单一守卫核。
5
+ */
6
+ import { describe, expect, it, vi } from 'vitest';
7
+
8
+ describe('validateScope:形状守卫(v4 恒动态——白名单判定退役)', () => {
9
+ it('mallId-only(无 miniAppId)→ true(v4:miniAppId 可选——N1 缺陷修复)', async () => {
10
+ const { createMallRuntimeAssembly } = await import('../src/assembly');
11
+ const assembly = createMallRuntimeAssembly({ malls: [{ mallId: 'mall-demo', name: 'x' }] });
12
+ expect(assembly.validateScope({ mall: { mallId: 'mall-demo' } })).toBe(true);
13
+ });
14
+
15
+ it('双参齐全 → true(存量形态回归)', async () => {
16
+ const { createMallRuntimeAssembly } = await import('../src/assembly');
17
+ const assembly = createMallRuntimeAssembly({ malls: [{ mallId: 'mall-demo', name: 'x' }] });
18
+ expect(assembly.validateScope({ mall: { miniAppId: 'demo-app', mallId: 'mall-demo' } })).toBe(true);
19
+ });
20
+
21
+ it('参考表外 mallId → true(恒动态:白名单不再拒绝——限制移交数据面 INSTANCE_NOT_FOUND)', async () => {
22
+ const { createMallRuntimeAssembly } = await import('../src/assembly');
23
+ const assembly = createMallRuntimeAssembly({ malls: [{ mallId: 'mall-demo', name: 'x' }] });
24
+ expect(assembly.validateScope({ mall: { miniAppId: 'a', mallId: '99999' } })).toBe(true);
25
+ });
26
+
27
+ it('空控制平面(无参考表)→ 任意 mallId 放行', async () => {
28
+ const { createMallRuntimeAssembly } = await import('../src/assembly');
29
+ const assembly = createMallRuntimeAssembly({ malls: [] });
30
+ expect(assembly.validateScope({ mall: { mallId: '任意动态id' } })).toBe(true);
31
+ });
32
+
33
+ it('mallId 空串 → console.error 提示 + false', async () => {
34
+ const { createMallRuntimeAssembly } = await import('../src/assembly');
35
+ const err = vi.spyOn(console, 'error').mockImplementation(() => undefined);
36
+ try {
37
+ const assembly = createMallRuntimeAssembly({ malls: [{ mallId: 'mall-demo', name: 'x' }] });
38
+ expect(assembly.validateScope({ mall: { mallId: '' } })).toBe(false);
39
+ expect(err).toHaveBeenCalledTimes(1);
40
+ expect(String(err.mock.calls[0][0])).toContain('形状非法');
41
+ } finally {
42
+ err.mockRestore();
43
+ }
44
+ });
45
+
46
+ it('mallId 缺失 → false(形状非法)', async () => {
47
+ const { createMallRuntimeAssembly } = await import('../src/assembly');
48
+ const err = vi.spyOn(console, 'error').mockImplementation(() => undefined);
49
+ try {
50
+ const assembly = createMallRuntimeAssembly({ malls: [{ mallId: 'mall-demo', name: 'x' }] });
51
+ expect(assembly.validateScope({ mall: { miniAppId: 'a' } })).toBe(false);
52
+ expect(err).toHaveBeenCalledTimes(1);
53
+ } finally {
54
+ err.mockRestore();
55
+ }
56
+ });
57
+
58
+ it("mall 键缺席 → true('' scopeKey = 全局会话放行)", async () => {
59
+ const { createMallRuntimeAssembly } = await import('../src/assembly');
60
+ const assembly = createMallRuntimeAssembly({ malls: [{ mallId: 'mall-demo', name: 'x' }] });
61
+ expect(assembly.validateScope({})).toBe(true);
62
+ expect(assembly.validateScope({ other: 1 })).toBe(true);
63
+ });
64
+ });
65
+
66
+ describe('extractScopeKey/buildScope:v4 语义', () => {
67
+ const reqCtxOf = (attributes: Record<string, unknown>) => ({
68
+ identity: { userId: 'u-1', source: 'external' as const },
69
+ attributes,
70
+ });
71
+
72
+ it('extractScopeKey:mall scope 在场 → mallId;防御兜底 \'\'(主通路:无 mall context 不 bootstrap)', async () => {
73
+ const { createMallRuntimeAssembly } = await import('../src/assembly');
74
+ const assembly = createMallRuntimeAssembly({ malls: [{ mallId: 'mall-demo', name: 'x' }] });
75
+ expect(assembly.extractScopeKey(reqCtxOf({ mall: { mallId: 'm1' } }))).toBe('m1');
76
+ expect(assembly.extractScopeKey(reqCtxOf({}))).toBe('');
77
+ });
78
+
79
+ it('buildScope:miniAppId 在场透传 / 缺席不产键(ActionGrant 双端同构)', async () => {
80
+ const { createMallRuntimeAssembly } = await import('../src/assembly');
81
+ const assembly = createMallRuntimeAssembly({ malls: [{ mallId: 'mall-demo', name: 'x' }] });
82
+ expect(assembly.buildScope({ mall: { miniAppId: 'app1', mallId: 'm1' } })).toEqual({
83
+ mallId: 'm1',
84
+ miniAppId: 'app1',
85
+ });
86
+ expect(assembly.buildScope({ mall: { mallId: 'm1' } })).toEqual({ mallId: 'm1' });
87
+ expect(assembly.buildScope({})).toEqual({});
88
+ });
89
+ });
90
+
91
+ describe('createMallContextValidator(HTTP 面形态——同核)', () => {
92
+ it('mall 键缺席 → true(非 mall 部署/无 context 放行)', async () => {
93
+ const { createMallContextValidator } = await import('../src/assembly');
94
+ const validate = createMallContextValidator();
95
+ expect(validate({})).toBe(true);
96
+ expect(validate({ other: 1 })).toBe(true);
97
+ });
98
+
99
+ it('mallId-only(无 miniAppId)→ true(N8:HTTP 面不要求 miniAppId)', async () => {
100
+ const { createMallContextValidator } = await import('../src/assembly');
101
+ const validate = createMallContextValidator();
102
+ expect(validate({ mall: { mallId: 'mall-demo' } })).toBe(true);
103
+ });
104
+
105
+ it('参考表外 mallId → true(恒动态同核)', async () => {
106
+ const { createMallContextValidator } = await import('../src/assembly');
107
+ const validate = createMallContextValidator();
108
+ expect(validate({ mall: { mallId: '99999' } })).toBe(true);
109
+ });
110
+
111
+ it('mallId 空串 → 形状不过 false', async () => {
112
+ const { createMallContextValidator } = await import('../src/assembly');
113
+ const validate = createMallContextValidator();
114
+ expect(validate({ mall: { mallId: '' } })).toBe(false);
115
+ });
116
+
117
+ it('与 WS validateScope 同核(isValidMallShape 单一来源锁死)', async () => {
118
+ const { createMallContextValidator, createMallRuntimeAssembly, isValidMallShape } =
119
+ await import('../src/assembly');
120
+ const validate = createMallContextValidator();
121
+ const assembly = createMallRuntimeAssembly({ malls: [] });
122
+ for (const attrs of [
123
+ {},
124
+ { mall: { mallId: 'm1' } },
125
+ { mall: { mallId: '' } },
126
+ { mall: { miniAppId: 'a' } },
127
+ ]) {
128
+ expect(validate(attrs)).toBe(assembly.validateScope(attrs));
129
+ expect(validate(attrs)).toBe(isValidMallShape(attrs));
130
+ }
131
+ });
132
+ });