@tbox.cn/app-module-promotion 0.2.0 → 0.9.3

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 (67) hide show
  1. package/AGENTS.md +16 -0
  2. package/README.md +90 -11
  3. package/dist/chunk-P7IEEV37.js +1347 -0
  4. package/dist/chunk-QOTX4XDS.js +1745 -0
  5. package/dist/client/index.d.ts +21 -3
  6. package/dist/client/index.js +4 -2
  7. package/dist/index.d.ts +4 -3
  8. package/dist/index.js +16 -4
  9. package/dist/promotion-CMKJ4DB3.css +2135 -0
  10. package/dist/promotion-activity-header-pea-v2-4CCHFCCM.jpg +0 -0
  11. package/dist/promotion-coupon-header-pea-v1-4CCHFCCM.jpg +0 -0
  12. package/dist/server/index.d.ts +2481 -41
  13. package/dist/server/index.js +13 -3
  14. package/package.json +28 -15
  15. package/skills/promotion/SKILL.md +137 -0
  16. package/skills/promotion/references/claim-loop.md +18 -0
  17. package/src/client/assets/promotion-activity-header-pea-v2.jpg +0 -0
  18. package/src/client/assets/promotion-coupon-header-pea-v1.jpg +0 -0
  19. package/src/client/assets.d.ts +5 -0
  20. package/src/client/cards/index.ts +10 -0
  21. package/src/client/cards/promotion-activity-category-list/index.tsx +34 -0
  22. package/src/client/cards/promotion-activity-detail/index.tsx +84 -0
  23. package/src/client/cards/promotion-activity-list/index.tsx +58 -0
  24. package/src/client/cards/promotion-best-deal/index.tsx +18 -0
  25. package/src/client/cards/promotion-entitlement-list/index.tsx +163 -0
  26. package/src/client/cards/promotion-offer-detail/index.tsx +116 -0
  27. package/src/client/cards/promotion-offer-list/index.tsx +285 -0
  28. package/src/client/cards/view.ts +72 -0
  29. package/src/client/components/CouponQrCode.tsx +48 -0
  30. package/src/client/components/CouponSheet.tsx +124 -0
  31. package/src/client/components/ImageWithFallback.tsx +39 -0
  32. package/src/client/index.ts +23 -4
  33. package/src/client/styles/promotion.css +2135 -0
  34. package/src/client/types/css.d.ts +2 -0
  35. package/src/client/utils/sanitize-activity-html.ts +48 -0
  36. package/src/client/utils/scroll-card-bottom.ts +40 -0
  37. package/src/server/actions.ts +217 -0
  38. package/src/server/cards/index.ts +19 -0
  39. package/src/server/cards/promotion-activity-category-list/meta.ts +13 -0
  40. package/src/server/cards/promotion-activity-detail/meta.ts +29 -0
  41. package/src/server/cards/promotion-activity-list/meta.ts +13 -0
  42. package/src/server/cards/promotion-best-deal/meta.ts +20 -0
  43. package/src/server/cards/promotion-entitlement-list/meta.ts +17 -0
  44. package/src/server/cards/promotion-entitlement-list/samples.ts +51 -0
  45. package/src/server/cards/promotion-offer-detail/meta.ts +25 -0
  46. package/src/server/cards/promotion-offer-list/meta.ts +20 -0
  47. package/src/server/cards/promotion-offer-list/samples.ts +154 -0
  48. package/src/server/handler.ts +378 -16
  49. package/src/server/index.ts +117 -22
  50. package/src/server/navigation.ts +23 -0
  51. package/src/server/ports.ts +56 -0
  52. package/src/server/service.ts +243 -24
  53. package/src/server/tools.ts +789 -0
  54. package/tbox.module.json +126 -0
  55. package/tests/cards-render.test.tsx +656 -0
  56. package/tests/ports-resolve.test.ts +96 -0
  57. package/tests/pretool-coupons.test.ts +63 -0
  58. package/tests/promotion.test.ts +976 -0
  59. package/tests/samples.test.ts +42 -0
  60. package/tests/view.test.ts +61 -0
  61. package/tsup.config.ts +9 -1
  62. package/dist/chunk-AD6OLHSM.js +0 -86
  63. package/dist/chunk-LQGF6337.js +0 -32
  64. package/src/client/cards/coupon/index.tsx +0 -12
  65. package/src/server/cards/coupon/meta.ts +0 -23
  66. package/tbox.component.json +0 -20
  67. package/tests/service.test.ts +0 -31
@@ -0,0 +1,42 @@
1
+ /**
2
+ * samples 双断言(迁移自 mall dev-gallery 优惠券链路画廊守卫):
3
+ * 全样本 dataSchema.safeParse 通过 + findSampleShapeViolations 无越界键(样本即契约)。
4
+ */
5
+ import { describe, expect, it } from 'vitest';
6
+ import { findSampleShapeViolations } from '@tbox.cn/app-sdk/server';
7
+ import offerListMeta from '../src/server/cards/promotion-offer-list/meta';
8
+ import entitlementListMeta from '../src/server/cards/promotion-entitlement-list/meta';
9
+
10
+ const metas = [offerListMeta, entitlementListMeta];
11
+
12
+ describe('promotion samples 双断言(样本即契约)', () => {
13
+ for (const meta of metas) {
14
+ it(`${meta.cardType}: 全样本过 schema + shape 无越界`, () => {
15
+ expect(meta.samples?.length ?? 0).toBeGreaterThan(0);
16
+ expect(findSampleShapeViolations(meta).violations).toEqual([]);
17
+ for (const s of meta.samples ?? []) {
18
+ expect(
19
+ meta.dataSchema.safeParse(s.data).success,
20
+ `${meta.cardType}/${s.label}`,
21
+ ).toBe(true);
22
+ }
23
+ });
24
+ }
25
+
26
+ it('样本集 9 格(offer-list 6 + entitlement-list 3)且券图为内联 SVG 占位', () => {
27
+ expect(offerListMeta.samples).toHaveLength(6);
28
+ expect(entitlementListMeta.samples).toHaveLength(3);
29
+ const serialized = JSON.stringify(metas.map((m) => m.samples));
30
+ expect(serialized).toContain('data:image/svg+xml');
31
+ // 结构性断言:样本图片只允许内联 SVG 或缺省——不允许任何外域图床
32
+ for (const meta of metas) {
33
+ for (const s of meta.samples ?? []) {
34
+ for (const item of (s.data as { items?: { imageUrl?: string }[] }).items ?? []) {
35
+ if (item.imageUrl) {
36
+ expect(item.imageUrl.startsWith('data:image/svg+xml'), `${meta.cardType}/${s.label}`).toBe(true);
37
+ }
38
+ }
39
+ }
40
+ }
41
+ });
42
+ });
@@ -0,0 +1,61 @@
1
+ /**
2
+ * promotion 展示纯函数单测(0.4.0 D6:node 环境)。
3
+ */
4
+ import { describe, expect, it } from 'vitest';
5
+ import {
6
+ entitlementStatusLabel,
7
+ entitlementTone,
8
+ offerTypeLabel,
9
+ resultStatusText,
10
+ resultTone,
11
+ sessionStatusText,
12
+ sessionTone,
13
+ } from '../src/client/cards/view';
14
+
15
+ describe('offerTypeLabel', () => {
16
+ it('五种类型 + 兜底', () => {
17
+ expect(offerTypeLabel('COUPON')).toBe('优惠券');
18
+ expect(offerTypeLabel('BUNDLE')).toBe('组合券');
19
+ expect(offerTypeLabel('PROMOTION')).toBe('促销');
20
+ expect(offerTypeLabel('ACTIVITY')).toBe('活动');
21
+ expect(offerTypeLabel('GROUPON')).toBe('团购');
22
+ expect(offerTypeLabel('NOPE')).toBe('优惠');
23
+ });
24
+ });
25
+
26
+ describe('entitlement 状态映射', () => {
27
+ it('文案', () => {
28
+ expect(entitlementStatusLabel('EFFECTIVE')).toBe('可用');
29
+ expect(entitlementStatusLabel('USED')).toBe('已使用');
30
+ expect(entitlementStatusLabel('EXPIRED')).toBe('已过期');
31
+ expect(entitlementStatusLabel('INVALID')).toBe('已失效');
32
+ expect(entitlementStatusLabel('X')).toBe('未知');
33
+ });
34
+ it('tone', () => {
35
+ expect(entitlementTone('EFFECTIVE')).toBe('available');
36
+ expect(entitlementTone('USED')).toBe('unavailable');
37
+ expect(entitlementTone('UNKNOWN')).toBe('neutral');
38
+ });
39
+ });
40
+
41
+ describe('operation-result 三态', () => {
42
+ it('tone + 文案', () => {
43
+ expect(resultTone('SUCCEEDED')).toBe('succeeded');
44
+ expect(resultTone('PENDING_AUTHORITY')).toBe('pending');
45
+ expect(resultTone('REJECTED')).toBe('rejected');
46
+ expect(resultStatusText('SUCCEEDED')).toBe('成功');
47
+ expect(resultStatusText('PENDING_AUTHORITY')).toBe('确认中');
48
+ expect(resultStatusText('REJECTED')).toBe('失败');
49
+ });
50
+ });
51
+
52
+ describe('activity session 状态', () => {
53
+ it('tone + 文案', () => {
54
+ expect(sessionTone('OPEN')).toBe('available');
55
+ expect(sessionTone('FULL')).toBe('unavailable');
56
+ expect(sessionTone('CLOSED')).toBe('unavailable');
57
+ expect(sessionStatusText('OPEN')).toBe('可报名');
58
+ expect(sessionStatusText('FULL')).toBe('已满');
59
+ expect(sessionStatusText('CLOSED')).toBe('已结束');
60
+ });
61
+ });
package/tsup.config.ts CHANGED
@@ -12,7 +12,9 @@ export default defineConfig({
12
12
  outDir: 'dist',
13
13
  clean: true,
14
14
  sourcemap: false,
15
- dts: true,
15
+ // dts worker 以命令行式传参会撞 TS5074(incremental 需单文件 emit 或 tsBuildInfoFile);
16
+ // 声明产物一次性构建无需增量——仅 dts 关闭,typecheck 的增量缓存不受影响。
17
+ dts: { compilerOptions: { incremental: false } },
16
18
  external: [
17
19
  '@tbox.cn/app-sdk',
18
20
  '@tbox.cn/app-contracts',
@@ -22,7 +24,13 @@ export default defineConfig({
22
24
  'zod',
23
25
  '@mastra/core',
24
26
  'express',
27
+ '@phosphor-icons/react',
25
28
  ],
29
+ // 0.3.2/0.4.0 D1:模块自持 CSS 出货链。
30
+ // 顶层 loader 写法(实测:esbuildOptions 内设 loader 被 tsup 覆盖失效——CSS 提取断链静默丢失)。
31
+ loader: {
32
+ '.css': 'copy',
33
+ },
26
34
  esbuildOptions(options) {
27
35
  options.jsx = 'automatic';
28
36
  },
@@ -1,86 +0,0 @@
1
- // src/server/service.ts
2
- var InMemoryPromotionService = class {
3
- constructor(getMember) {
4
- this.getMember = getMember;
5
- }
6
- getMember;
7
- async queryEligibleCoupons(userId) {
8
- const member = this.getMember();
9
- const points = member ? await member.queryPoints(userId) : { level: "silver" };
10
- const coupons = [
11
- {
12
- couponId: `coupon_${userId}_${points.level}`,
13
- title: points.level === "platinum" ? "\u94C2\u91D1\u4E13\u4EAB\u505C\u8F66\u5238" : points.level === "gold" ? "\u91D1\u5361\u505C\u8F66\u5238" : "\u505C\u8F66\u4F18\u60E0\u5238",
14
- amount: points.level === "platinum" ? 10 : points.level === "gold" ? 5 : 2,
15
- status: "AVAILABLE"
16
- }
17
- ];
18
- return coupons;
19
- }
20
- };
21
-
22
- // src/server/cards/coupon/meta.ts
23
- import { z } from "zod";
24
- var dataSchema = z.object({
25
- couponId: z.string(),
26
- title: z.string(),
27
- amount: z.number(),
28
- status: z.enum(["AVAILABLE", "USED", "EXPIRED"])
29
- });
30
- var meta = {
31
- cardType: "coupon",
32
- dataSchema,
33
- allowedTools: [],
34
- displayName: "\u4F18\u60E0\u5238\u5361",
35
- description: "\u5C55\u793A\u63A8\u8350\u53D1\u653E\u7684\u4F18\u60E0\u5238",
36
- sampleData: { couponId: "coupon_u_1_gold", title: "\u91D1\u5361\u505C\u8F66\u5238", amount: 5, status: "AVAILABLE" },
37
- schemaVersion: 1
38
- };
39
- var meta_default = meta;
40
-
41
- // src/server/handler.ts
42
- function createPromotionHandler(ctx) {
43
- return {
44
- id: "promotion",
45
- handle: async (_action, payload) => {
46
- const promotion = ctx.resolveService("promotion");
47
- if (!promotion) return { cards: [], text: "\u8425\u9500\u670D\u52A1\u4E0D\u53EF\u7528" };
48
- const { userId } = payload ?? {};
49
- if (!userId) return { cards: [], text: "\u7F3A\u5C11 userId" };
50
- const coupons = await promotion.queryEligibleCoupons(userId);
51
- if (coupons.length === 0) return { cards: [], text: "\u6682\u65E0\u53EF\u7528\u4F18\u60E0\u5238" };
52
- return {
53
- cards: [{ cardType: "coupon", data: coupons[0] }],
54
- text: `\u63A8\u8350 ${coupons[0].title}\uFF08\u53EF\u62B5 ${coupons[0].amount} \u5143\uFF09`
55
- };
56
- }
57
- };
58
- }
59
-
60
- // src/server/index.ts
61
- var currentCtx = null;
62
- var promotionSingleton = null;
63
- var cards = {
64
- coupon: meta_default
65
- };
66
- var serverModule = {
67
- cards,
68
- register(ctx) {
69
- currentCtx = ctx;
70
- if (!promotionSingleton) {
71
- promotionSingleton = new InMemoryPromotionService(
72
- () => currentCtx?.resolveService("member")
73
- );
74
- }
75
- const promotion = promotionSingleton;
76
- ctx.services.register("promotion", promotion);
77
- ctx.cards.registerMap(cards);
78
- ctx.handlers.register(createPromotionHandler(ctx));
79
- }
80
- };
81
-
82
- export {
83
- InMemoryPromotionService,
84
- meta_default,
85
- serverModule
86
- };
@@ -1,32 +0,0 @@
1
- // src/client/cards/coupon/index.tsx
2
- import { jsx, jsxs } from "react/jsx-runtime";
3
- function CouponCard({ data }) {
4
- return /* @__PURE__ */ jsxs("div", { className: "mall-card mall-card--coupon", children: [
5
- /* @__PURE__ */ jsx("div", { className: "mall-card__title", children: data.title }),
6
- /* @__PURE__ */ jsxs("div", { className: "mall-card__amount", children: [
7
- "-",
8
- data.amount,
9
- " \u5143"
10
- ] }),
11
- /* @__PURE__ */ jsxs("div", { className: "mall-card__meta", children: [
12
- data.couponId,
13
- " \xB7 ",
14
- data.status
15
- ] })
16
- ] });
17
- }
18
-
19
- // src/client/index.ts
20
- var cards = {
21
- coupon: CouponCard
22
- };
23
- var clientModule = {
24
- cards,
25
- register(ctx) {
26
- ctx.cards.registerMap(cards);
27
- }
28
- };
29
-
30
- export {
31
- clientModule
32
- };
@@ -1,12 +0,0 @@
1
- import type { CouponCardData } from '../../../server/cards/coupon/meta';
2
-
3
- /** 优惠券卡组件 */
4
- export function CouponCard({ data }: { data: CouponCardData; cardId: string; isHistory: boolean }) {
5
- return (
6
- <div className="mall-card mall-card--coupon">
7
- <div className="mall-card__title">{data.title}</div>
8
- <div className="mall-card__amount">-{data.amount} 元</div>
9
- <div className="mall-card__meta">{data.couponId} · {data.status}</div>
10
- </div>
11
- );
12
- }
@@ -1,23 +0,0 @@
1
- import { z } from 'zod';
2
- import type { CardMeta } from '@tbox.cn/app-contracts';
3
-
4
- export const dataSchema = z.object({
5
- couponId: z.string(),
6
- title: z.string(),
7
- amount: z.number(),
8
- status: z.enum(['AVAILABLE', 'USED', 'EXPIRED']),
9
- });
10
-
11
- export type CouponCardData = z.infer<typeof dataSchema>;
12
-
13
- const meta: CardMeta<typeof dataSchema> = {
14
- cardType: 'coupon',
15
- dataSchema,
16
- allowedTools: [],
17
- displayName: '优惠券卡',
18
- description: '展示推荐发放的优惠券',
19
- sampleData: { couponId: 'coupon_u_1_gold', title: '金卡停车券', amount: 5, status: 'AVAILABLE' },
20
- schemaVersion: 1,
21
- };
22
-
23
- export default meta;
@@ -1,20 +0,0 @@
1
- {
2
- "schemaVersion": 1,
3
- "name": "module-promotion",
4
- "version": "0.2.0",
5
- "kind": "business",
6
- "risk": { "level": "low", "writeBoundary": "source" },
7
- "distribution": { "defaultMode": "codegen" },
8
- "contributes": {
9
- "handlers": ["promotion"],
10
- "tools": [],
11
- "cards": [{ "cardType": "coupon", "schemaVersion": 1 }],
12
- "routes": [],
13
- "pages": [],
14
- "tabs": []
15
- },
16
- "dependencies": {
17
- "modules": [{ "id": "contracts-mall", "required": true }]
18
- },
19
- "env": []
20
- }
@@ -1,31 +0,0 @@
1
- import { describe, expect, it } from 'vitest';
2
- import { createServerContext } from '@tbox.cn/app-sdk/server';
3
- import { serverModule } from '../src/server/index';
4
-
5
- describe('module-promotion 资格校验与降级链', () => {
6
- it('serverModule.register 注册 card/service/handler', () => {
7
- const ctx = createServerContext();
8
- serverModule.register(ctx);
9
- expect(ctx.cards.has('coupon')).toBe(true);
10
- expect(ctx.handlers.has('promotion')).toBe(true);
11
- expect(ctx.resolveService('promotion')).toBeDefined();
12
- });
13
-
14
- it('降级链:silver → 2 元券 / gold → 5 元券 / platinum → 10 元券', async () => {
15
- const ctx = createServerContext();
16
- // stub member 供 promotion 查询等级
17
- ctx.services.register('member', {
18
- queryPoints: async (userId: string) => {
19
- const level = userId === 'u_gold' ? 'gold' : userId === 'u_platinum' ? 'platinum' : 'silver';
20
- return { userId, balance: 100, level: level as 'silver' | 'gold' | 'platinum' };
21
- },
22
- queryDiscount: async () => undefined,
23
- });
24
- serverModule.register(ctx);
25
- const promotion = ctx.resolveService<{ queryEligibleCoupons(u: string): Promise<{ amount: number }[]> }>('promotion')!;
26
-
27
- expect((await promotion.queryEligibleCoupons('u_silver'))[0].amount).toBe(2);
28
- expect((await promotion.queryEligibleCoupons('u_gold'))[0].amount).toBe(5);
29
- expect((await promotion.queryEligibleCoupons('u_platinum'))[0].amount).toBe(10);
30
- });
31
- });