@tbox.cn/app-module-promotion 0.2.0 → 0.9.4
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.
- package/AGENTS.md +16 -0
- package/README.md +90 -11
- package/dist/chunk-QOTX4XDS.js +1745 -0
- package/dist/chunk-S7CVFCML.js +1330 -0
- package/dist/client/index.d.ts +21 -3
- package/dist/client/index.js +4 -2
- package/dist/index.d.ts +4 -3
- package/dist/index.js +16 -4
- package/dist/promotion-6L7HLRC2.css +2135 -0
- package/dist/server/index.d.ts +2481 -41
- package/dist/server/index.js +13 -3
- package/package.json +28 -15
- package/skills/promotion/SKILL.md +137 -0
- package/skills/promotion/references/claim-loop.md +18 -0
- package/src/client/cards/index.ts +10 -0
- package/src/client/cards/promotion-activity-category-list/index.tsx +34 -0
- package/src/client/cards/promotion-activity-detail/index.tsx +84 -0
- package/src/client/cards/promotion-activity-list/index.tsx +56 -0
- package/src/client/cards/promotion-best-deal/index.tsx +18 -0
- package/src/client/cards/promotion-entitlement-list/index.tsx +161 -0
- package/src/client/cards/promotion-offer-detail/index.tsx +116 -0
- package/src/client/cards/promotion-offer-list/index.tsx +283 -0
- package/src/client/cards/view.ts +72 -0
- package/src/client/components/CouponQrCode.tsx +48 -0
- package/src/client/components/CouponSheet.tsx +124 -0
- package/src/client/components/ImageWithFallback.tsx +39 -0
- package/src/client/index.ts +23 -4
- package/src/client/styles/promotion.css +2135 -0
- package/src/client/types/css.d.ts +2 -0
- package/src/client/utils/sanitize-activity-html.ts +48 -0
- package/src/client/utils/scroll-card-bottom.ts +40 -0
- package/src/server/actions.ts +217 -0
- package/src/server/cards/index.ts +19 -0
- package/src/server/cards/promotion-activity-category-list/meta.ts +13 -0
- package/src/server/cards/promotion-activity-detail/meta.ts +29 -0
- package/src/server/cards/promotion-activity-list/meta.ts +13 -0
- package/src/server/cards/promotion-best-deal/meta.ts +20 -0
- package/src/server/cards/promotion-entitlement-list/meta.ts +17 -0
- package/src/server/cards/promotion-entitlement-list/samples.ts +51 -0
- package/src/server/cards/promotion-offer-detail/meta.ts +25 -0
- package/src/server/cards/promotion-offer-list/meta.ts +20 -0
- package/src/server/cards/promotion-offer-list/samples.ts +154 -0
- package/src/server/handler.ts +378 -16
- package/src/server/index.ts +117 -22
- package/src/server/navigation.ts +23 -0
- package/src/server/ports.ts +56 -0
- package/src/server/service.ts +243 -24
- package/src/server/tools.ts +789 -0
- package/tbox.module.json +126 -0
- package/tests/cards-render.test.tsx +656 -0
- package/tests/ports-resolve.test.ts +96 -0
- package/tests/pretool-coupons.test.ts +63 -0
- package/tests/promotion.test.ts +976 -0
- package/tests/samples.test.ts +42 -0
- package/tests/view.test.ts +61 -0
- package/tsup.config.ts +9 -1
- package/dist/chunk-AD6OLHSM.js +0 -86
- package/dist/chunk-LQGF6337.js +0 -32
- package/src/client/cards/coupon/index.tsx +0 -12
- package/src/server/cards/coupon/meta.ts +0 -23
- package/tbox.component.json +0 -20
- package/tests/service.test.ts +0 -31
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* promotion ports 017 槽位原子化回归锁(四槽同构矩阵):offers/entitlements/activities/acquisition。
|
|
3
|
+
* - 槽位命中 → adapter 产物 + service 参数 = 槽 id;
|
|
4
|
+
* - PROVIDER_NOT_IMPLEMENTED → 上抛(配置即声明);
|
|
5
|
+
* - 预期缺席三码 → InMemoryPromotionService 兜底;
|
|
6
|
+
* - 其余错误原样上抛;槽级隔离(acquisition 缺席不影响 offers 命中)。
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from 'vitest';
|
|
9
|
+
import type { RequestContext } from '@tbox.cn/app-contracts';
|
|
10
|
+
import { createServerContext } from '@tbox.cn/app-sdk/server';
|
|
11
|
+
import type { ServerContext } from '@tbox.cn/app-sdk/server';
|
|
12
|
+
import {
|
|
13
|
+
inMemoryPromotion,
|
|
14
|
+
resolvePromotionAcquisitionPort,
|
|
15
|
+
resolvePromotionActivitiesPort,
|
|
16
|
+
resolvePromotionEntitlementsPort,
|
|
17
|
+
resolvePromotionOffersPort,
|
|
18
|
+
} from '../src/server/ports';
|
|
19
|
+
|
|
20
|
+
function requestContext(mallId = 'mall-demo'): RequestContext {
|
|
21
|
+
return {
|
|
22
|
+
identity: { userId: 'u_1001', source: 'dev' },
|
|
23
|
+
credentials: { provider: 'dev', token: 'user-token' },
|
|
24
|
+
attributes: { mall: { miniAppId: 'app', mallId } },
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ctxWithIntegrations(resolveService: (input: unknown) => Promise<unknown>): ServerContext {
|
|
29
|
+
const ctx = createServerContext({});
|
|
30
|
+
(ctx as { integrations?: unknown }).integrations = { resolveService };
|
|
31
|
+
return ctx;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const sentinel = { kind: 'slot-hit' } as unknown as never;
|
|
35
|
+
|
|
36
|
+
const RESOLVERS = [
|
|
37
|
+
{ name: 'promotion.offers', resolve: resolvePromotionOffersPort },
|
|
38
|
+
{ name: 'promotion.entitlements', resolve: resolvePromotionEntitlementsPort },
|
|
39
|
+
{ name: 'promotion.activities', resolve: resolvePromotionActivitiesPort },
|
|
40
|
+
{ name: 'promotion.acquisition', resolve: resolvePromotionAcquisitionPort },
|
|
41
|
+
] as const;
|
|
42
|
+
|
|
43
|
+
describe('promotion ports 四槽解析(017 槽位原子化)', () => {
|
|
44
|
+
for (const { name, resolve } of RESOLVERS) {
|
|
45
|
+
it(`[${name}] T1 槽位命中 → adapter 产物 + service 参数 = 槽 id`, async () => {
|
|
46
|
+
let captured: unknown;
|
|
47
|
+
const ctx = ctxWithIntegrations(async (input) => {
|
|
48
|
+
captured = input;
|
|
49
|
+
return sentinel;
|
|
50
|
+
});
|
|
51
|
+
await expect(resolve(requestContext(), ctx)).resolves.toBe(sentinel);
|
|
52
|
+
expect(captured).toMatchObject({ service: name, scope: { mallId: 'mall-demo' } });
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it(`[${name}] T2 PROVIDER_NOT_IMPLEMENTED → 上抛不兜底`, async () => {
|
|
56
|
+
const ctx = ctxWithIntegrations(async () => {
|
|
57
|
+
throw Object.assign(new Error('无 adapter'), { code: 'PROVIDER_NOT_IMPLEMENTED' });
|
|
58
|
+
});
|
|
59
|
+
await expect(resolve(requestContext(), ctx)).rejects.toMatchObject({
|
|
60
|
+
code: 'PROVIDER_NOT_IMPLEMENTED',
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it(`[${name}] T3 预期缺席三码 → InMemoryPromotionService 单例`, async () => {
|
|
65
|
+
for (const code of ['PROVIDER_SERVICE_NOT_CONFIGURED', 'PROVIDER_INSTANCE_NOT_FOUND', 'PROVIDER_INSTANCE_DISABLED']) {
|
|
66
|
+
const ctx = ctxWithIntegrations(async () => {
|
|
67
|
+
throw Object.assign(new Error(code), { code });
|
|
68
|
+
});
|
|
69
|
+
await expect(resolve(requestContext(), ctx)).resolves.toBe(inMemoryPromotion);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it(`[${name}] T4 PROVIDER_NOT_FOUND → 原样上抛`, async () => {
|
|
74
|
+
const ctx = ctxWithIntegrations(async () => {
|
|
75
|
+
throw Object.assign(new Error('未命中 catalog'), { code: 'PROVIDER_NOT_FOUND' });
|
|
76
|
+
});
|
|
77
|
+
await expect(resolve(requestContext(), ctx)).rejects.toMatchObject({ code: 'PROVIDER_NOT_FOUND' });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it(`[${name}] T5 ctx 无 integrations → InMemory 兜底`, async () => {
|
|
81
|
+
const ctx = createServerContext({});
|
|
82
|
+
await expect(resolve(requestContext(), ctx)).resolves.toBe(inMemoryPromotion);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
it('T6 槽级隔离:acquisition 缺席兜底,offers 照常命中(只读厂商 + InMemory 领取的显式形态)', async () => {
|
|
87
|
+
const ctx = ctxWithIntegrations(async (input: unknown) => {
|
|
88
|
+
if ((input as { service: string }).service === 'promotion.acquisition') {
|
|
89
|
+
throw Object.assign(new Error('未配置'), { code: 'PROVIDER_SERVICE_NOT_CONFIGURED' });
|
|
90
|
+
}
|
|
91
|
+
return sentinel;
|
|
92
|
+
});
|
|
93
|
+
await expect(resolvePromotionOffersPort(requestContext(), ctx)).resolves.toBe(sentinel);
|
|
94
|
+
await expect(resolvePromotionAcquisitionPort(requestContext(), ctx)).resolves.toBe(inMemoryPromotion);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* W5 P1 / W2 / W7 promotion 切片验收(2026-09-01):
|
|
3
|
+
* - 出参体积门:listCoupons CLAIMABLE 档出参 JSON 实测 ≤ 2KB(超限即撤 preTool 的 go/no-go 门);
|
|
4
|
+
* - 15 工具 description 含预告知句(withCardEmissionHint 单点 wrap);
|
|
5
|
+
* - note 契约:列表卡 note 含数量+首项标题、best-deal note 含测算金额(不再静态句)。
|
|
6
|
+
*/
|
|
7
|
+
import { describe, expect, it } from 'vitest';
|
|
8
|
+
import { InMemoryPromotionService } from '../src/server/service';
|
|
9
|
+
import { createPromotionTools, offerSummary } from '../src/server/tools';
|
|
10
|
+
import { CARD_EMISSION_HINT_BASE } from '@tbox.cn/app-sdk/server';
|
|
11
|
+
import type { PromotionQueryContext } from '@tbox.cn/app-contracts-mall';
|
|
12
|
+
|
|
13
|
+
const q = {
|
|
14
|
+
scope: { miniAppId: 'app', mallId: 'mall-1' },
|
|
15
|
+
subjectId: 'u_1',
|
|
16
|
+
requestContext: {},
|
|
17
|
+
} as never as PromotionQueryContext;
|
|
18
|
+
|
|
19
|
+
describe('listCoupons 出参体积门(P1 go/no-go)', () => {
|
|
20
|
+
it('CLAIMABLE 档 mock 出参 JSON ≤ 2048 字符(offerSummary 投影形态)', async () => {
|
|
21
|
+
const service = new InMemoryPromotionService();
|
|
22
|
+
const page = await service.searchOffers(q, { offerTypes: ['COUPON'] });
|
|
23
|
+
const facts = {
|
|
24
|
+
presentation: 'coupon' as const,
|
|
25
|
+
mode: 'claimable' as const,
|
|
26
|
+
summary: '可领取优惠券',
|
|
27
|
+
items: page.items.map(offerSummary),
|
|
28
|
+
};
|
|
29
|
+
const json = JSON.stringify(facts);
|
|
30
|
+
// go/no-go:> 2048 字符 → preTool listCoupons 必须整体撤除(index.ts)并记录 Agent Note
|
|
31
|
+
expect(json.length).toBeLessThanOrEqual(2048);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe('withCardEmissionHint:出卡工具预告知(W2)', () => {
|
|
36
|
+
const tools = createPromotionTools({
|
|
37
|
+
getCtx: () => {
|
|
38
|
+
throw new Error('promotion 模块未注册');
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('15 个出卡工具 description 含预告知句', () => {
|
|
43
|
+
for (const tool of Object.values(tools)) {
|
|
44
|
+
expect(tool.description).toContain(CARD_EMISSION_HINT_BASE);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('note 契约(W7):关键事实前置', () => {
|
|
50
|
+
it('offerSummary 含 title(列表卡 note 的首项事实源)', () => {
|
|
51
|
+
const summary = offerSummary({
|
|
52
|
+
offerRef: 'o1',
|
|
53
|
+
title: '满 100 减 20',
|
|
54
|
+
offerType: 'COUPON',
|
|
55
|
+
tags: [],
|
|
56
|
+
validity: { end: '' },
|
|
57
|
+
facets: [],
|
|
58
|
+
availability: { status: 'AVAILABLE', reasonCodes: [], evaluatedAt: '', authoritative: true },
|
|
59
|
+
priceText: '免费',
|
|
60
|
+
} as never);
|
|
61
|
+
expect(summary.title).toBe('满 100 减 20');
|
|
62
|
+
});
|
|
63
|
+
});
|