@tbox.cn/app-provider-mock 0.2.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.
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Mock promotion 实现(provider-mock 自持——R3-1 裁决)。演示面最小集:搜索/精查/推荐/
3
+ * 资产列表/活动列表/领取闭环(acquire 恒 SUCCEEDED)。demo ref 恒 `demo:` 前缀,厂商中立。
4
+ */
5
+ import type {
6
+ PromotionOffer,
7
+ PromotionOfferPage,
8
+ PromotionEntitlement,
9
+ PromotionEntitlementPage,
10
+ PromotionActivity,
11
+ PromotionActivityPage,
12
+ PromotionActivityDetail,
13
+ PromotionOperation,
14
+ PromotionQueryContext,
15
+ PromotionSearchInput,
16
+ PromotionAcquisitionPort,
17
+ PromotionActivityPort,
18
+ PromotionEntitlementPort,
19
+ PromotionOfferQueryPort,
20
+ } from '@tbox.cn/app-contracts-mall';
21
+
22
+ export type MockPromotionService = PromotionOfferQueryPort &
23
+ PromotionEntitlementPort &
24
+ PromotionActivityPort &
25
+ PromotionAcquisitionPort;
26
+
27
+ const now = () => new Date().toISOString();
28
+ const inDays = (d: number) => new Date(Date.now() + d * 86400_000).toISOString();
29
+
30
+ const DEMO_OFFERS: PromotionOffer[] = [
31
+ {
32
+ offerRef: 'demo:coupon:1001',
33
+ offerType: 'COUPON',
34
+ title: '满 100 减 20 元券',
35
+ subtitle: '全场通用(mock 演示)',
36
+ tags: ['通用'],
37
+ validity: { start: now(), end: inDays(30) },
38
+ facets: [{ kind: 'COUPON', discountType: 'AMOUNT', value: 2000, thresholdCents: 10000 }],
39
+ availability: { status: 'AVAILABLE', reasonCodes: [], evaluatedAt: now(), authoritative: false },
40
+ participation: { mode: 'CLAIM_THEN_PURCHASE', steps: [{ title: '领取', description: '点击领取后自动存入卡包' }] },
41
+ },
42
+ {
43
+ offerRef: 'demo:groupon:2001',
44
+ offerType: 'GROUPON',
45
+ title: '餐饮双人套餐 5 折',
46
+ subtitle: 'mock 演示团购',
47
+ tags: ['餐饮'],
48
+ validity: { start: now(), end: inDays(15) },
49
+ facets: [{ kind: 'GROUPON', requiredParticipants: 2 }],
50
+ availability: { status: 'AVAILABLE', reasonCodes: [], evaluatedAt: now(), authoritative: false },
51
+ participation: { mode: 'CLAIM_THEN_PURCHASE', steps: [{ title: '购买', description: '按团购价下单' }] },
52
+ },
53
+ ];
54
+
55
+ const DEMO_ENTITLEMENTS: PromotionEntitlement[] = [
56
+ {
57
+ entitlementRef: 'demo:entitlement:1',
58
+ offerRef: 'demo:coupon:1001',
59
+ title: '满 100 减 20 元券',
60
+ entitlementType: 'COUPON',
61
+ status: 'EFFECTIVE',
62
+ codeMasked: 'DEMO-****-1001',
63
+ },
64
+ ];
65
+
66
+ const DEMO_ACTIVITIES: PromotionActivity[] = [
67
+ {
68
+ activityRef: 'demo:activity:3001',
69
+ title: '示例商场周年庆',
70
+ subtitle: 'mock 演示活动',
71
+ registrationRequired: false,
72
+ offerRefs: ['demo:coupon:1001'],
73
+ },
74
+ ];
75
+
76
+ const emptyPage = (): PromotionOfferPage & PromotionEntitlementPage & PromotionActivityPage => ({
77
+ items: [],
78
+ hasMore: false,
79
+ });
80
+
81
+ export class MockPromotionServiceImpl implements MockPromotionService {
82
+ async searchOffers(_q: PromotionQueryContext, input: PromotionSearchInput): Promise<PromotionOfferPage> {
83
+ const keyword = input.keyword?.trim();
84
+ const items = keyword
85
+ ? DEMO_OFFERS.filter((o) => o.title.includes(keyword) || o.tags.some((t) => t.includes(keyword)))
86
+ : DEMO_OFFERS;
87
+ return { items, hasMore: false };
88
+ }
89
+
90
+ async getOfferDetail(
91
+ _q: PromotionQueryContext,
92
+ input: { offerRef?: string; entitlementRef?: string; keyword?: string },
93
+ ): Promise<PromotionOffer | null> {
94
+ if (input.offerRef) return DEMO_OFFERS.find((o) => o.offerRef === input.offerRef) ?? null;
95
+ if (input.entitlementRef) {
96
+ const ent = DEMO_ENTITLEMENTS.find((e) => e.entitlementRef === input.entitlementRef);
97
+ if (ent?.offerRef) return DEMO_OFFERS.find((o) => o.offerRef === ent.offerRef) ?? null;
98
+ return null;
99
+ }
100
+ if (input.keyword) return DEMO_OFFERS.find((o) => o.title.includes(input.keyword!)) ?? null;
101
+ return null;
102
+ }
103
+
104
+ async recommendOffers(): Promise<readonly PromotionOffer[]> {
105
+ return DEMO_OFFERS;
106
+ }
107
+
108
+ async listEntitlements(_q: PromotionQueryContext, _input: PromotionSearchInput): Promise<PromotionEntitlementPage> {
109
+ return { items: DEMO_ENTITLEMENTS, hasMore: false };
110
+ }
111
+
112
+ async getEntitlementDetail(_q: PromotionQueryContext, entitlementRef: string): Promise<PromotionEntitlement | null> {
113
+ return DEMO_ENTITLEMENTS.find((e) => e.entitlementRef === entitlementRef) ?? null;
114
+ }
115
+
116
+ async listActivities(): Promise<PromotionActivityPage> {
117
+ return { items: DEMO_ACTIVITIES, hasMore: false };
118
+ }
119
+
120
+ async getActivityDetail(_q: PromotionQueryContext, activityRef: string): Promise<PromotionActivityDetail | null> {
121
+ const activity = DEMO_ACTIVITIES.find((a) => a.activityRef === activityRef);
122
+ if (!activity) return null;
123
+ return { ...activity, sessions: [], description: 'mock 演示活动详情' };
124
+ }
125
+
126
+ async listActivityCategories() {
127
+ return [{ categoryId: 'demo:category:1', name: '全部' }];
128
+ }
129
+
130
+ async acquire(
131
+ _q: PromotionQueryContext,
132
+ input: { offerRef: string; externalMemberRef: string; idempotencyKey: string },
133
+ ): Promise<PromotionOperation> {
134
+ return {
135
+ operationRef: `demo:operation:${input.offerRef}`,
136
+ status: 'SUCCEEDED',
137
+ offerRef: input.offerRef,
138
+ codeMasked: 'DEMO-****-0001',
139
+ observedAt: now(),
140
+ };
141
+ }
142
+
143
+ async getOperation(_q: PromotionQueryContext, operationRef: string): Promise<PromotionOperation | null> {
144
+ return { operationRef, status: 'SUCCEEDED', offerRef: 'demo:coupon:1001', observedAt: now() };
145
+ }
146
+ }
147
+
148
+ export { emptyPage };
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Mock shopping 实现(provider-mock 自持——R3-1 裁决:provider 不得依赖业务模块)。
3
+ * 演示面:门店/类目/楼层目录(remote 检索域四键形状的 catalog/shop/category/directory)。
4
+ *
5
+ * 设计决策:
6
+ * - 单一真源:DEMO_BUILDINGS 楼栋→楼层→门店 seed;类目/楼层业态/计数派生聚合;
7
+ * floorCode/buildingCode 派生同值(code=name,消除 mall-context id ↔ search-shops
8
+ * 过滤键隐式映射)。
9
+ * - 纯 mock:静态数组 + 内存过滤,零 IO、零外部依赖(不依赖知识库/TBOX_API_KEY/datasetId)。
10
+ * - merchantId 连字符格式(demo-shop-NNNN):LLM 从卡片 value 提取友好;getShop 入参 trim。
11
+ * - catalog 空实现(对齐真实档 C 端):演示面不覆盖商品目录,门店链路为演示重点。
12
+ * - 构造器接受 seed(默认 DEMO_BUILDINGS):测试可注入定制数据。
13
+ */
14
+ import type {
15
+ CatalogQueryPort,
16
+ CategoryRecord,
17
+ CommercialCategoryQueryPort,
18
+ MallDirectoryQueryPort,
19
+ MallDirectoryRecord,
20
+ BuildingRecord,
21
+ FloorRecord,
22
+ ProductFacetsRecord,
23
+ ProductRecord,
24
+ ProductSearchCriteria,
25
+ ShopDirectoryQueryPort,
26
+ ShopRecord,
27
+ ShopSearchCriteria,
28
+ ShoppingPage,
29
+ ShoppingQueryContext,
30
+ } from '@tbox.cn/app-contracts-mall';
31
+
32
+ interface ShopSeed {
33
+ merchantId: string;
34
+ name: string;
35
+ brand?: string;
36
+ businessCategories: string[];
37
+ unitName: string;
38
+ status: { code: 'enabled' | 'disabled'; label: string };
39
+ }
40
+
41
+ interface DemoFloor {
42
+ floorName: string;
43
+ shops: ShopSeed[];
44
+ }
45
+
46
+ interface DemoBuilding {
47
+ buildingName: string;
48
+ floors: DemoFloor[];
49
+ }
50
+
51
+ /** seed 单一真源:1 楼栋 A 栋 / 6 类目 / 12 店(西贝休息中演示营业状态多样性;NIKE 双店支撑快捷命令) */
52
+ const DEMO_BUILDINGS: DemoBuilding[] = [
53
+ {
54
+ buildingName: 'A 栋',
55
+ floors: [
56
+ {
57
+ floorName: 'B1',
58
+ shops: [
59
+ { merchantId: 'demo-shop-1001', name: '永辉超市', businessCategories: ['超市'], unitName: 'B1-01', status: { code: 'enabled', label: '营业中' } },
60
+ ],
61
+ },
62
+ {
63
+ floorName: '1 楼',
64
+ shops: [
65
+ { merchantId: 'demo-shop-1002', name: '星巴克', businessCategories: ['餐饮'], unitName: '101', status: { code: 'enabled', label: '营业中' } },
66
+ { merchantId: 'demo-shop-1003', name: '周大福', businessCategories: ['珠宝'], unitName: '102', status: { code: 'enabled', label: '营业中' } },
67
+ { merchantId: 'demo-shop-1004', name: '屈臣氏', businessCategories: ['美妆'], unitName: '103', status: { code: 'enabled', label: '营业中' } },
68
+ ],
69
+ },
70
+ {
71
+ floorName: '2 楼',
72
+ shops: [
73
+ { merchantId: 'demo-shop-1005', name: '华为', businessCategories: ['数码'], unitName: '201', status: { code: 'enabled', label: '营业中' } },
74
+ { merchantId: 'demo-shop-1006', name: '小米', businessCategories: ['数码'], unitName: '202', status: { code: 'enabled', label: '营业中' } },
75
+ ],
76
+ },
77
+ {
78
+ floorName: '3 楼',
79
+ shops: [
80
+ { merchantId: 'demo-shop-1007', name: 'NIKE 专卖店', brand: 'NIKE', businessCategories: ['运动服饰'], unitName: '301', status: { code: 'enabled', label: '营业中' } },
81
+ { merchantId: 'demo-shop-1008', name: 'NIKE 跑步体验店', brand: 'NIKE', businessCategories: ['运动服饰'], unitName: '302', status: { code: 'enabled', label: '营业中' } },
82
+ { merchantId: 'demo-shop-1009', name: '安踏', businessCategories: ['运动服饰'], unitName: '303', status: { code: 'enabled', label: '营业中' } },
83
+ ],
84
+ },
85
+ {
86
+ floorName: '4 楼',
87
+ shops: [
88
+ { merchantId: 'demo-shop-1010', name: '海底捞', businessCategories: ['餐饮'], unitName: '401', status: { code: 'enabled', label: '营业中' } },
89
+ { merchantId: 'demo-shop-1011', name: '外婆家', businessCategories: ['餐饮'], unitName: '402', status: { code: 'enabled', label: '营业中' } },
90
+ { merchantId: 'demo-shop-1012', name: '西贝莜面村', businessCategories: ['餐饮'], unitName: '403', status: { code: 'disabled', label: '休息中' } },
91
+ ],
92
+ },
93
+ ],
94
+ },
95
+ ];
96
+
97
+ /** seed → 扁平门店(location/status 派生;observedAt 同一时点快照) */
98
+ function flattenShops(buildings: readonly DemoBuilding[]): ShopRecord[] {
99
+ const ts = new Date().toISOString();
100
+ return buildings.flatMap((b) =>
101
+ b.floors.flatMap((f) =>
102
+ f.shops.map((seed) => ({
103
+ merchantId: seed.merchantId,
104
+ name: seed.name,
105
+ ...(seed.brand ? { brand: seed.brand } : {}),
106
+ businessCategories: [...seed.businessCategories],
107
+ location: {
108
+ displayText: [b.buildingName, f.floorName, seed.unitName].filter(Boolean).join(' '),
109
+ buildingName: b.buildingName,
110
+ floorName: f.floorName,
111
+ unitName: seed.unitName,
112
+ },
113
+ status: { code: seed.status.code, label: seed.status.label, observedAt: ts },
114
+ coverUrls: [],
115
+ services: [],
116
+ promotionFacts: [],
117
+ observedAt: ts,
118
+ }) satisfies ShopRecord,
119
+ )),
120
+ );
121
+ }
122
+
123
+ /** seed → 类目聚合(code=name;顺序 = 门店遍历首现序) */
124
+ function aggregateCategories(shops: readonly ShopRecord[]): CategoryRecord[] {
125
+ const counts = new Map<string, number>();
126
+ for (const s of shops) {
127
+ for (const c of s.businessCategories) counts.set(c, (counts.get(c) ?? 0) + 1);
128
+ }
129
+ return [...counts.entries()].map(([name, merchantCount]) => ({ code: name, name, merchantCount }));
130
+ }
131
+
132
+ /** seed → 楼层记录(code=name 派生同值;业态/计数从该层门店聚合) */
133
+ function toFloorRecord(floor: DemoFloor): FloorRecord {
134
+ const cats = [...new Set(floor.shops.flatMap((s) => s.businessCategories))];
135
+ return { floorCode: floor.floorName, floorName: floor.floorName, businessCategories: cats, merchantCount: floor.shops.length };
136
+ }
137
+
138
+ export class MockShoppingServiceImpl implements
139
+ CatalogQueryPort, ShopDirectoryQueryPort, CommercialCategoryQueryPort, MallDirectoryQueryPort {
140
+ private readonly buildings: readonly DemoBuilding[];
141
+ private readonly shops: ShopRecord[];
142
+ private readonly categories: CategoryRecord[];
143
+
144
+ constructor(seed: readonly DemoBuilding[] = DEMO_BUILDINGS) {
145
+ this.buildings = seed;
146
+ this.shops = flattenShops(seed);
147
+ this.categories = aggregateCategories(this.shops);
148
+ }
149
+
150
+ // ===== ShopDirectoryQueryPort =====
151
+
152
+ async searchShops(criteria: ShopSearchCriteria): Promise<ShoppingPage<ShopRecord>> {
153
+ const kws = (criteria.keywords ?? (criteria.keyword?.trim() ? [criteria.keyword.trim()] : []))
154
+ .map((k) => k.trim().toLowerCase())
155
+ .filter(Boolean);
156
+ const cat = criteria.businessCategory?.trim();
157
+ const floor = criteria.floorCode?.trim();
158
+ const bld = criteria.buildingCode?.trim();
159
+ const predicates: Array<(s: ShopRecord) => boolean> = [];
160
+ if (kws.length > 0) {
161
+ predicates.push((s) =>
162
+ kws.some((k) =>
163
+ s.name.toLowerCase().includes(k) ||
164
+ (s.brand ?? '').toLowerCase().includes(k) ||
165
+ s.businessCategories.some((c) => c.toLowerCase().includes(k)),
166
+ ),
167
+ );
168
+ }
169
+ if (cat) predicates.push((s) => s.businessCategories.includes(cat));
170
+ if (floor) predicates.push((s) => s.location.floorName === floor);
171
+ if (bld) predicates.push((s) => s.location.buildingName === bld);
172
+ const matched = predicates.length === 0 ? this.shops : this.shops.filter((s) => predicates.every((p) => p(s)));
173
+ const start = (criteria.page - 1) * criteria.pageSize;
174
+ return {
175
+ items: matched.slice(start, start + criteria.pageSize),
176
+ page: criteria.page,
177
+ pageSize: criteria.pageSize,
178
+ total: matched.length,
179
+ hasMore: start + criteria.pageSize < matched.length,
180
+ };
181
+ }
182
+
183
+ async getShop(shopId: string, _ctx: ShoppingQueryContext): Promise<ShopRecord | null> {
184
+ const id = shopId.trim();
185
+ return this.shops.find((s) => s.merchantId === id) ?? null;
186
+ }
187
+
188
+ // ===== CommercialCategoryQueryPort =====
189
+
190
+ async listCategories(_ctx: ShoppingQueryContext): Promise<readonly CategoryRecord[]> {
191
+ return this.categories;
192
+ }
193
+
194
+ // ===== MallDirectoryQueryPort =====
195
+
196
+ async getDirectory(ctx: ShoppingQueryContext): Promise<MallDirectoryRecord | null> {
197
+ return {
198
+ mallId: ctx.scope.mallId ?? 'mall-demo',
199
+ mallName: '示例商场',
200
+ buildings: this.buildings.map((b) => ({
201
+ buildingCode: b.buildingName,
202
+ buildingName: b.buildingName,
203
+ floors: b.floors.map(toFloorRecord),
204
+ }) satisfies BuildingRecord),
205
+ observedAt: new Date().toISOString(),
206
+ };
207
+ }
208
+
209
+ // ===== CatalogQueryPort(空实现:演示面不覆盖商品目录,对齐真实档 C 端) =====
210
+
211
+ async searchProducts(criteria: ProductSearchCriteria): Promise<ShoppingPage<ProductRecord>> {
212
+ return { items: [], page: criteria.page, pageSize: criteria.pageSize, total: 0, hasMore: false };
213
+ }
214
+
215
+ async getProduct(_productId: string, _ctx: ShoppingQueryContext): Promise<ProductRecord | null> {
216
+ return null;
217
+ }
218
+
219
+ async getFacets(_criteria: ProductSearchCriteria): Promise<ProductFacetsRecord> {
220
+ return { brands: [], categories: [], priceRanges: [], floors: [] };
221
+ }
222
+ }
@@ -0,0 +1,149 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "name": "provider-mock",
4
+ "version": "0.1.0",
5
+ "kind": "business",
6
+ "risk": {
7
+ "level": "low",
8
+ "writeBoundary": "source"
9
+ },
10
+ "distribution": {
11
+ "defaultMode": "codegen"
12
+ },
13
+ "contributes": {
14
+ "handlers": [],
15
+ "tools": [],
16
+ "cards": [],
17
+ "routes": [],
18
+ "pages": [],
19
+ "tabs": [],
20
+ "providers": {
21
+ "slots": [
22
+ {
23
+ "service": "member.account",
24
+ "credentialType": "-",
25
+ "local": true,
26
+ "provider": "mock",
27
+ "implementation": "mock-member@1"
28
+ },
29
+ {
30
+ "service": "member.profile",
31
+ "credentialType": "-",
32
+ "local": true,
33
+ "provider": "mock",
34
+ "implementation": "mock-member@1"
35
+ },
36
+ {
37
+ "service": "member.card",
38
+ "credentialType": "-",
39
+ "local": true,
40
+ "provider": "mock",
41
+ "implementation": "mock-member@1"
42
+ },
43
+ {
44
+ "service": "member.benefits",
45
+ "credentialType": "-",
46
+ "local": true,
47
+ "provider": "mock",
48
+ "implementation": "mock-member@1"
49
+ },
50
+ {
51
+ "service": "member.enrollment",
52
+ "credentialType": "-",
53
+ "local": true,
54
+ "provider": "mock",
55
+ "implementation": "mock-member@1"
56
+ },
57
+ {
58
+ "service": "parking.query",
59
+ "credentialType": "-",
60
+ "local": true,
61
+ "provider": "mock",
62
+ "implementation": "mock-parking@1"
63
+ },
64
+ {
65
+ "service": "parking.payment",
66
+ "credentialType": "-",
67
+ "local": true,
68
+ "provider": "mock",
69
+ "implementation": "mock-parking@1"
70
+ },
71
+ {
72
+ "service": "parking.vehicle",
73
+ "credentialType": "-",
74
+ "local": true,
75
+ "provider": "mock",
76
+ "implementation": "mock-parking@1"
77
+ },
78
+ {
79
+ "service": "promotion.offers",
80
+ "credentialType": "-",
81
+ "local": true,
82
+ "provider": "mock",
83
+ "implementation": "mock-promotion@1"
84
+ },
85
+ {
86
+ "service": "promotion.entitlements",
87
+ "credentialType": "-",
88
+ "local": true,
89
+ "provider": "mock",
90
+ "implementation": "mock-promotion@1"
91
+ },
92
+ {
93
+ "service": "promotion.activities",
94
+ "credentialType": "-",
95
+ "local": true,
96
+ "provider": "mock",
97
+ "implementation": "mock-promotion@1"
98
+ },
99
+ {
100
+ "service": "promotion.acquisition",
101
+ "credentialType": "-",
102
+ "local": true,
103
+ "provider": "mock",
104
+ "implementation": "mock-promotion@1"
105
+ },
106
+ {
107
+ "service": "shopping-guide.catalog",
108
+ "credentialType": "-",
109
+ "local": true,
110
+ "provider": "mock",
111
+ "implementation": "mock-shopping@1"
112
+ },
113
+ {
114
+ "service": "shopping-guide.shops",
115
+ "credentialType": "-",
116
+ "local": true,
117
+ "provider": "mock",
118
+ "implementation": "mock-shopping@1"
119
+ },
120
+ {
121
+ "service": "shopping-guide.categories",
122
+ "credentialType": "-",
123
+ "local": true,
124
+ "provider": "mock",
125
+ "implementation": "mock-shopping@1"
126
+ },
127
+ {
128
+ "service": "shopping-guide.directory",
129
+ "credentialType": "-",
130
+ "local": true,
131
+ "provider": "mock",
132
+ "implementation": "mock-shopping@1"
133
+ },
134
+ {
135
+ "service": "mall.info",
136
+ "credentialType": "-",
137
+ "local": true,
138
+ "provider": "mock",
139
+ "implementation": "mock-mall-info@1"
140
+ }
141
+ ]
142
+ },
143
+ "services": []
144
+ },
145
+ "dependencies": {
146
+ "modules": []
147
+ },
148
+ "env": []
149
+ }