@tbox.cn/app-contracts-mall 0.2.0 → 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 (81) hide show
  1. package/README.md +16 -2
  2. package/dist/alipay-activity/cards.d.ts +305 -0
  3. package/dist/assembly.d.ts +31 -6
  4. package/dist/chunk-KIZD6FZ2.js +43 -0
  5. package/dist/deployment.d.ts +32 -33
  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/index.d.ts +14 -2
  15. package/dist/member/cards.d.ts +636 -213
  16. package/dist/parking/cards.d.ts +1271 -0
  17. package/dist/promotion/cards.d.ts +2529 -0
  18. package/dist/query-context.d.ts +47 -0
  19. package/dist/runtime.d.ts +11 -2
  20. package/dist/runtime.js +910 -86
  21. package/dist/scope.d.ts +42 -15
  22. package/dist/server/index.d.ts +11 -0
  23. package/dist/server/index.js +80 -0
  24. package/dist/server/mall-context.d.ts +24 -0
  25. package/dist/server/mall-control-plane.d.ts +8 -0
  26. package/dist/server/request-credentials.d.ts +22 -0
  27. package/dist/services/customer-service.d.ts +11 -0
  28. package/dist/services/mall-info.d.ts +26 -0
  29. package/dist/services/member.d.ts +173 -31
  30. package/dist/services/parking.d.ts +105 -12
  31. package/dist/services/promotion.d.ts +100 -4
  32. package/dist/services/shopping-guide.d.ts +97 -0
  33. package/dist/shared/cards.d.ts +42 -0
  34. package/dist/shopping-guide/cards.d.ts +3399 -561
  35. package/package.json +18 -7
  36. package/src/alipay-activity/cards.ts +66 -0
  37. package/src/assembly.ts +103 -0
  38. package/src/deployment.ts +44 -0
  39. package/src/domain/activity.ts +66 -0
  40. package/src/domain/coupon.ts +11 -0
  41. package/src/domain/discount.ts +6 -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 +207 -0
  46. package/src/domain/points.ts +15 -0
  47. package/src/domain/shopping-guide.ts +242 -0
  48. package/src/events/index.ts +10 -0
  49. package/src/guard.ts +48 -0
  50. package/src/index.ts +33 -0
  51. package/src/member/cards.ts +209 -0
  52. package/src/parking/cards.ts +233 -0
  53. package/src/promotion/cards.ts +290 -0
  54. package/src/query-context.ts +81 -0
  55. package/src/runtime.ts +41 -0
  56. package/src/scope.ts +92 -0
  57. package/src/server/index.ts +11 -0
  58. package/src/server/mall-context.ts +39 -0
  59. package/src/server/mall-control-plane.ts +56 -0
  60. package/src/server/request-credentials.ts +43 -0
  61. package/src/service-slots.json +23 -0
  62. package/src/services/customer-service.ts +12 -0
  63. package/src/services/mall-info.ts +31 -0
  64. package/src/services/member.ts +366 -0
  65. package/src/services/parking.ts +166 -0
  66. package/src/services/promotion.ts +126 -0
  67. package/src/services/shopping-guide.ts +138 -0
  68. package/src/shared/cards.ts +25 -0
  69. package/src/shopping-guide/cards.ts +438 -0
  70. package/tests/assembly-validate-scope.test.ts +132 -0
  71. package/tests/cards.test.ts +586 -0
  72. package/tests/context-unification.test.ts +73 -0
  73. package/tests/mall-control-plane.test.ts +86 -0
  74. package/tests/member-status.test.ts +197 -0
  75. package/tests/query-context.test.ts +111 -0
  76. package/tests/request-credentials.test.ts +38 -0
  77. package/tests/resolve-primitives.test.ts +55 -0
  78. package/tests/service-slots.test.ts +78 -0
  79. package/tsconfig.build.json +15 -0
  80. package/tsconfig.json +5 -0
  81. package/dist/shopping-guide/ports.d.ts +0 -216
@@ -0,0 +1,23 @@
1
+ {
2
+ "$comment": "mall 域 service slot 词汇唯一真源。共址 TS 常量并集必须与本文件 slots 逐元素相等(tests/service-slots.test.ts 锁);manifest demand/supply ⊆ slots(scripts/guard-service-slots.mjs 锁)。旧词汇不在此——迁移归 cookbook 配方。",
3
+ "slots": [
4
+ "member.account",
5
+ "member.profile",
6
+ "member.card",
7
+ "member.benefits",
8
+ "member.enrollment",
9
+ "parking.query",
10
+ "parking.payment",
11
+ "parking.vehicle",
12
+ "promotion.offers",
13
+ "promotion.entitlements",
14
+ "promotion.activities",
15
+ "promotion.acquisition",
16
+ "shopping-guide.catalog",
17
+ "shopping-guide.shops",
18
+ "shopping-guide.categories",
19
+ "shopping-guide.directory",
20
+ "auth.alipay-login",
21
+ "mall.info"
22
+ ]
23
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * 客服承接域 Port 契约(2026-08-31 客服模块拆分:自 shopping-guide AssistanceQueryPort
3
+ * 切出 FAQ/知识库两方法;人工导购 listHumanGuides 留导购域 AssistanceQueryPort)。
4
+ * 客服承接 = 本地知识库域(与 provider 远程检索面正交,恒本地装配)。
5
+ */
6
+ import type { ShoppingQueryContext } from './shopping-guide';
7
+ import type { FaqEntry, KnowledgeEntry } from '../domain/shopping-guide';
8
+
9
+ export interface CustomerServiceQueryPort {
10
+ searchFaq(ctx: ShoppingQueryContext, query: string, topK?: number): Promise<readonly FaqEntry[]>;
11
+ searchKnowledge(ctx: ShoppingQueryContext, query: string, topK?: number): Promise<readonly KnowledgeEntry[]>;
12
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * 商场基础信息 Port(mall.info 槽服务契约;动态商场信息源)。
3
+ * 厂商 wire 词汇(如厂商侧 plaza 端点词)不进本层——投影后只余平台词汇。
4
+ * 结构类型子集接口——消费方(模板装配点/模块)仅依赖契约包。
5
+ */
6
+
7
+ // ===== mall.info service slot 词汇(与 service-slots.json 同锁;id 不变,017 常量化) =====
8
+
9
+ export const MALL_INFO_SERVICE = 'mall.info';
10
+
11
+ /** 商场基础信息(动态商场场景下由 mall.info 槽按 mallId 拉取) */
12
+ export interface MallInfo {
13
+ /** 商场 id(= scope.mallId;投影键) */
14
+ mallId: string;
15
+ /** 商场名(品牌区 mallName 动态真源;与 malls 参考表 name 同语义) */
16
+ name: string;
17
+ address?: string;
18
+ latitude?: number;
19
+ longitude?: number;
20
+ /** 城市 id(nearby-mall 动态化预留,本期不消费) */
21
+ cityId?: string;
22
+ }
23
+
24
+ /** mall.info 槽供给端口(provider 包 adapter 实现;模板装配点经 integrations 解析) */
25
+ export interface MallInfoPort {
26
+ /**
27
+ * 查询商场信息;未知/失败 → undefined(fail-soft,调用方负责缓存与降级)。
28
+ * mallId 形状守卫由实现方负责(防 path 注入类字符进上游 URL)。
29
+ */
30
+ getMallInfo(mallId: string): Promise<MallInfo | undefined>;
31
+ }
@@ -0,0 +1,366 @@
1
+ import type { Points } from '../domain/points';
2
+ import type { DiscountRule } from '../domain/discount';
3
+ import type { Identity, RequestContext } from '@tbox.cn/app-contracts';
4
+
5
+ /**
6
+ * 会员域 Port(011 K2:6 Port 替换旧 MemberQueryPort/MemberSettlementPort,breaking 0.2.0)。
7
+ * 结构类型子集接口——模块完整实现 extends 本接口,消费方仅依赖契约包。
8
+ */
9
+
10
+ // ===== 会员域 service slot 词汇(与 service-slots.json 同锁) =====
11
+
12
+ export const MEMBER_ACCOUNT_SERVICE = 'member.account';
13
+ export const MEMBER_PROFILE_SERVICE = 'member.profile';
14
+ export const MEMBER_CARD_SERVICE = 'member.card';
15
+ export const MEMBER_BENEFITS_SERVICE = 'member.benefits';
16
+ export const MEMBER_ENROLLMENT_SERVICE = 'member.enrollment';
17
+
18
+ /** 应用登录状态与会员状态相互独立;登录成功不代表已经入会。 */
19
+ export type MemberAuthState = 'authenticated' | 'unauthenticated';
20
+
21
+ /** unknown 表示当前身份无法向会员 Provider 确认状态;其余状态仅描述会员资格。 */
22
+ export type MemberState = 'unknown' | 'active' | 'not_enrolled' | 'closed';
23
+
24
+ // ===== 1. MemberAccountPort:账户/入会状态 =====
25
+
26
+ export interface MemberAccountSnapshot {
27
+ readonly authState: MemberAuthState;
28
+ readonly state: MemberState;
29
+ readonly memberRef?: string;
30
+ readonly primaryMallCardNo?: string;
31
+ readonly registeredAt?: string;
32
+ readonly observedAt: string;
33
+ }
34
+
35
+ export interface MemberAccountPort {
36
+ getAccount(userId: string): Promise<MemberAccountSnapshot>;
37
+ }
38
+
39
+ // ===== 2. MemberProfilePort:资料 =====
40
+
41
+ export interface MemberProfileSnapshot {
42
+ readonly displayName?: string;
43
+ readonly avatarUrl?: string;
44
+ readonly contactDisplay?: string;
45
+ readonly birthday?: string;
46
+ readonly gender?: 'unknown' | 'unspecified' | 'male' | 'female';
47
+ readonly labels: ReadonlyArray<{ readonly labelRef: string; readonly name: string }>;
48
+ readonly observedAt: string;
49
+ }
50
+
51
+ export interface MemberProfilePort {
52
+ getProfile(userId: string): Promise<MemberProfileSnapshot>;
53
+ }
54
+
55
+ // ===== 3. MemberCardPort:会员卡/卡名 =====
56
+
57
+ /** 通用卡面视觉变体;具体等级到变体的映射由 provider 负责。 */
58
+ export type MemberCardVisualVariant = 'blue' | 'silver' | 'gold' | 'black';
59
+
60
+ export interface MemberCardPresentationSnapshot {
61
+ readonly variant: MemberCardVisualVariant;
62
+ /** 背景资源是否已包含卡名、积分标签等静态文字。 */
63
+ readonly labelsEmbedded?: boolean;
64
+ }
65
+
66
+ /** 卡面消费进度;金额统一为分,提示文案由 provider 按厂商规则生成。 */
67
+ export interface MemberCardProgressSnapshot {
68
+ readonly daySpentCents: number;
69
+ readonly yearSpentCents: number;
70
+ readonly percent: number;
71
+ readonly message: string;
72
+ }
73
+
74
+ /** 卡片首屏权益摘要;规则正文与跳链不在摘要契约中直接暴露。 */
75
+ export interface MemberBenefitPreview {
76
+ readonly benefitRef: string;
77
+ readonly name: string;
78
+ readonly iconUrl?: string;
79
+ /** 厂商权益摘要标签,例如源码按 parkingFree 拼出的“可免 X 小时”。 */
80
+ readonly description?: string;
81
+ /** 厂商原生权益动作;客户端通过支付宝 startapp 深链交给宿主跳转。 */
82
+ readonly action?: {
83
+ readonly kind: 'miniapp' | 'dialog' | 'toast';
84
+ readonly url?: string;
85
+ readonly title?: string;
86
+ readonly text?: string;
87
+ };
88
+ }
89
+
90
+ export interface MemberCardSnapshot {
91
+ readonly cardRef: string;
92
+ readonly cardDisplay: string;
93
+ readonly status: 'active' | 'inactive' | 'frozen' | 'expired' | 'unknown';
94
+ readonly levelName?: string;
95
+ readonly cardImageUrl?: string;
96
+ /** 积分规则长图;客户端以只读图片弹层展示,不作为通用跳链执行。 */
97
+ readonly pointsRuleImageUrl?: string;
98
+ readonly pointsRuleContent?: string;
99
+ /** C 端原生“我的积分”明细页深链。 */
100
+ readonly pointsUrl?: string;
101
+ readonly presentation?: MemberCardPresentationSnapshot;
102
+ readonly progress?: MemberCardProgressSnapshot;
103
+ readonly benefits?: ReadonlyArray<MemberBenefitPreview>;
104
+ readonly observedAt: string;
105
+ }
106
+
107
+ export interface MemberCardPort {
108
+ getCard(userId: string): Promise<MemberCardSnapshot | null>;
109
+ }
110
+
111
+ // ===== 4. MemberBenefitsPort:积分/权益(吸收旧 MemberQueryPort + 结算写) =====
112
+
113
+ export interface MemberBenefitsSnapshot {
114
+ readonly availablePoints: number;
115
+ readonly usedPoints?: number;
116
+ readonly expiringPoints?: number;
117
+ readonly levelName?: string;
118
+ readonly observedAt: string;
119
+ }
120
+
121
+ export interface MemberBenefitsPort {
122
+ /** 积分查询(替代旧 MemberQueryPort.queryPoints) */
123
+ queryPoints(userId: string): Promise<Points>;
124
+ /** 权益快照 */
125
+ getBenefits(userId: string): Promise<MemberBenefitsSnapshot>;
126
+ /** 积分流水(points 卡数据源) */
127
+ listPointUsage(userId: string, page: number, pageSize: number): Promise<{
128
+ items: ReadonlyArray<PointUsageRecord>;
129
+ total: number;
130
+ }>;
131
+ /** 折扣查询(替代旧 MemberQueryPort.queryDiscount) */
132
+ queryDiscount(userId: string): Promise<DiscountRule | undefined>;
133
+ /** 结算写(替代旧 MemberSettlementPort;幂等键由调用方生成) */
134
+ deductPoints(userId: string, amount: number, idempotencyKey: string): Promise<boolean>;
135
+ refundPoints(userId: string, amount: number, idempotencyKey: string): Promise<boolean>;
136
+ awardPoints(userId: string, amount: number): Promise<boolean>;
137
+ }
138
+
139
+ export interface PointUsageRecord {
140
+ readonly usageRecordRef: string;
141
+ readonly title: string;
142
+ /** 字符串化金额(D11 展示层统一字符串) */
143
+ readonly amount: string;
144
+ readonly usedAt?: string;
145
+ readonly description?: string;
146
+ }
147
+
148
+ /**
149
+ * 会员服务解析工厂(上下文统一化 Stage 2):跨模块 'member.service' 注册面。
150
+ * member 模块 register 期注册 `(reqCtx) => resolveMemberBenefitsPort(reqCtx, ctx)`
151
+ * (017 槽位原子化:本面 = benefits 单槽契约,benefits 方法族);
152
+ * 跨模块消费方经 ctx.resolveService<MemberBenefitsFactory>('member.service')
153
+ * 结构解析后 `await factory(reqCtx)`——替代旧 'member' 全局门面(ctx.auth 并发缺陷)。
154
+ * 身份/会员资格判定请走 'member.status' 注册面(outcome + account),勿经本面取 account 域数据。
155
+ */
156
+ export type MemberBenefitsFactory = (reqCtx: RequestContext) => Promise<MemberBenefitsPort>;
157
+
158
+ // ===== 5. MemberCouponPort:优惠券(0.4.0 净删——券域由 promotion C 端承接,双真源不留) =====
159
+
160
+ // ===== 6. MemberEnrollmentPort:入会(写通道,enrollment grant 闭环) =====
161
+
162
+ /** 入会身份来源(F2:授权 profile 可信度高于手填) */
163
+ export type MemberEnrollmentIdentity = 'alipay_authorized' | 'enrollment_submission';
164
+
165
+ export interface MemberEnrollmentInput {
166
+ readonly userId: string;
167
+ readonly identity: MemberEnrollmentIdentity;
168
+ /** 表单字段(mobile 等;授权模式下 mobile 强制以授权 profile 为准) */
169
+ readonly fieldValues: Readonly<Record<string, string>>;
170
+ /** 已同意的 consent id 集合(模块侧校验必勾后传入) */
171
+ readonly acceptedConsentIds: readonly string[];
172
+ /** 幂等键(模块生成:sha256(userId\0mallId\0formVersion)) */
173
+ readonly idempotencyKey: string;
174
+ readonly formVersion?: string;
175
+ }
176
+
177
+ export type MemberEnrollmentResult =
178
+ | { readonly status: 'active'; readonly memberRef: string }
179
+ /** 厂商异步确认(pending 卡 + 显式刷新收口,R4) */
180
+ | { readonly status: 'pending_confirmation'; readonly attemptRef: string }
181
+ | { readonly status: 'rejected'; readonly reason?: string };
182
+
183
+ export type MemberEnrollmentStatus =
184
+ | 'pending_confirmation'
185
+ | 'confirmation_timeout'
186
+ | 'active'
187
+ | 'failed';
188
+
189
+ export interface MemberEnrollmentPort {
190
+ /** 入会(幂等:同 idempotencyKey 重入返回首果) */
191
+ enroll(input: MemberEnrollmentInput): Promise<MemberEnrollmentResult>;
192
+ /** 入会状态查询(attemptRef 归属校验由模块完成) */
193
+ getEnrollmentStatus(userId: string, attemptRef: string): Promise<{ status: MemberEnrollmentStatus; memberRef?: string; reason?: string }>;
194
+ /**
195
+ * 入会表单定义(厂商端点拉取,如厂商 enrollmentConfig 端点);无表单端点厂商可缺省——
196
+ * 调用方 typeof 守卫降级(calculateBestDeal? 同款先例)。017 R13 自 module-member
197
+ * fat 接口收编。
198
+ */
199
+ getEnrollmentFormDefinition?(userId: string): Promise<MemberEnrollmentFormDefinition>;
200
+ }
201
+
202
+ // ===== 7. SubjectBindingPort:主体绑定解析(跨模块,015 D9) =====
203
+
204
+ /**
205
+ * 解析平台 userId → 厂商会员外部引用(externalMemberRef)。
206
+ * member 模块注册实现(ctx.services.register('member.subject-binding', ...))。
207
+ * @deprecated 同步派生引用不承载会员身份语义(对任意 userId 恒返回派生值);
208
+ * 会员状态判定与真实外部会员引用请改用 'member.status' 服务(MemberStatusFactory)
209
+ * 与 externalMemberRefOf。保留仅为版本偏斜期兼容。
210
+ */
211
+ export interface SubjectBindingPort {
212
+ resolveExternalMemberRef(userId: string): string | undefined;
213
+ }
214
+
215
+ // ===== 8. 入会表单定义(F5 自 module-member 下沉:provider 出参 + 模块工具投影共用) =====
216
+
217
+ export interface MemberEnrollmentFormFieldDefinition {
218
+ readonly fieldId: string;
219
+ readonly label: string;
220
+ readonly type: 'text' | 'tel' | 'select' | 'date';
221
+ readonly required: boolean;
222
+ readonly placeholder?: string;
223
+ readonly options?: readonly string[];
224
+ }
225
+
226
+ export interface MemberEnrollmentConsentDefinition {
227
+ readonly consentId: string;
228
+ readonly version: string;
229
+ readonly title: string;
230
+ readonly required: boolean;
231
+ }
232
+
233
+ export interface MemberEnrollmentFormDefinition {
234
+ readonly formVersion: string;
235
+ readonly fields: readonly MemberEnrollmentFormFieldDefinition[];
236
+ readonly consents: readonly MemberEnrollmentConsentDefinition[];
237
+ readonly authorizationStatus: 'required' | 'unavailable';
238
+ }
239
+
240
+ // ===== 9. 会员状态折叠(outcome 单源) =====
241
+
242
+ /** 折叠结果:'unauthenticated'(平台匿名短路或凭证轴未认证)或原始会员资格态。 */
243
+ export type MemberOutcome = 'unauthenticated' | MemberState;
244
+
245
+ /**
246
+ * 双轴折叠纯函数(无 IO,唯一真源):identity 轴(平台登录态)优先短路;
247
+ * account 轴(凭证态)独立折叠——凭证轴语义由各 provider 自定义,
248
+ * tokenless 的合法登录可为 unauthenticated。
249
+ */
250
+ export function classifyMemberOutcome(
251
+ identity: Pick<Identity, 'source'> | null | undefined,
252
+ account: MemberAccountSnapshot | null | undefined,
253
+ ): MemberOutcome {
254
+ if (identity?.source === 'anonymous') return 'unauthenticated';
255
+ if (!account) return 'unknown';
256
+ if (account.authState === 'unauthenticated') return 'unauthenticated';
257
+ return account.state;
258
+ }
259
+
260
+ /**
261
+ * 厂商侧会员标识:真实卡号优先,opaque 引用兜底。
262
+ * 兜底引用仅对不校验真实卡号的 provider 安全——需要真实卡号的厂商接口
263
+ * 必须由 provider 提供 primaryMallCardNo。
264
+ */
265
+ export function externalMemberRefOf(account: MemberAccountSnapshot): string | undefined {
266
+ return account.primaryMallCardNo ?? account.memberRef;
267
+ }
268
+
269
+ /** 跨模块会员状态解析结果(窄读面;实现方注册产物可为超集)。 */
270
+ export interface MemberStatusResolution {
271
+ readonly outcome: MemberOutcome | 'unavailable';
272
+ readonly account?: MemberAccountSnapshot;
273
+ /** 入会引导深链(authenticated 未入会态;未配置缺省) */
274
+ readonly enrollUrl?: string;
275
+ /** 登录引导深链(unauthenticated 态;未配置缺省) */
276
+ readonly loginUrl?: string;
277
+ }
278
+
279
+ /** 跨模块 'member.status' 解析工厂(与 MemberBenefitsFactory 并列的注册面契约)。 */
280
+ export type MemberStatusFactory = (reqCtx: RequestContext) => Promise<MemberStatusResolution>;
281
+
282
+ // ===== 10. 跨模块会员门禁(卡型复用 + 编排单源) =====
283
+
284
+ /** 门禁卡描述子(形状 = SDK ToolCardSpec { cardType, data, note },出卡管线兼容)。 */
285
+ export interface MemberGateCard {
286
+ readonly cardType: 'member-enrollment-cta' | 'member-login-cta';
287
+ readonly data: Record<string, unknown>;
288
+ readonly note: string;
289
+ }
290
+
291
+ /** 门禁 notice 单源(工具 noResultsReason / handler text 两用;closed ≠ not_enrolled——对齐 member 先例语义拆分)。 */
292
+ const MEMBER_GATE_NOTICES: Readonly<Record<'unauthenticated' | 'not_enrolled' | 'closed', string>> = {
293
+ unauthenticated: '请先登录后再使用该服务',
294
+ not_enrolled: '完成入会后即可使用该服务',
295
+ closed: '重新入会后可恢复使用该服务',
296
+ };
297
+
298
+ /**
299
+ * 纯卡映射:outcome → 门禁卡描述子;硬门禁态恒出卡,深链为**可选透传素材**(未配置
300
+ * 时卡无 URL 字段,客户端禁用按钮/壳能力接管);软放行态 / userId 空 → undefined,
301
+ * 调用方退纯文字引导(notice)。
302
+ * 卡 schema 真源:memberEnrollmentCtaCardDataSchema / memberLoginCtaCardDataSchema(本包 member/cards.ts)。
303
+ */
304
+ export function memberGateCardOf(
305
+ userId: string,
306
+ status: Pick<MemberStatusResolution, 'outcome' | 'enrollUrl' | 'loginUrl'>,
307
+ ): MemberGateCard | undefined {
308
+ const { outcome } = status;
309
+ if (outcome === 'unauthenticated') {
310
+ const loginUrl = status.loginUrl?.trim() || undefined;
311
+ return {
312
+ cardType: 'member-login-cta',
313
+ data: { title: '登录后继续', ...(loginUrl ? { loginUrl } : {}) },
314
+ // note 契约(W7,2026-09-01):主体+动作(模型据此答话转述——「点击卡片登录」)
315
+ note: '未登录 · 点击卡片登录',
316
+ };
317
+ }
318
+ if (outcome !== 'not_enrolled' && outcome !== 'closed') return undefined;
319
+ if (!userId) return undefined;
320
+ const enrollUrl = status.enrollUrl?.trim() || undefined;
321
+ return {
322
+ cardType: 'member-enrollment-cta',
323
+ data: {
324
+ userId,
325
+ authState: 'authenticated' as const,
326
+ state: outcome,
327
+ title: '入会后继续',
328
+ ...(enrollUrl ? { enrollUrl } : {}),
329
+ },
330
+ note: outcome === 'not_enrolled' ? '未入会 · 点击卡片完成入会' : '已退会 · 点击卡片重新入会',
331
+ };
332
+ }
333
+
334
+ /** resolveMemberGate 返回的拦截块(undefined = 放行)。 */
335
+ export interface MemberGateBlock {
336
+ readonly card?: MemberGateCard;
337
+ readonly notice: string;
338
+ }
339
+
340
+ /**
341
+ * 门禁编排单源:工厂缺席 / 调用抛错 → fail-open(undefined 放行);
342
+ * active / unknown / unavailable → 软放行(读路径软语义——不误伤瞬时故障的会员);
343
+ * 硬门禁(unauthenticated/not_enrolled/closed)→ { card?(未配深链缺省), notice }。
344
+ * 实现方(member.status 工厂)应自收口异常;本函数对抛错实现兜底为软放行(横切面鲁棒性)。
345
+ */
346
+ export async function resolveMemberGate(
347
+ factory: MemberStatusFactory | undefined,
348
+ reqCtx: RequestContext,
349
+ ): Promise<MemberGateBlock | undefined> {
350
+ if (!factory) return undefined;
351
+ let status: MemberStatusResolution;
352
+ try {
353
+ status = await factory(reqCtx);
354
+ } catch {
355
+ return undefined; // fail-open(防御:契约实现方未自收口)
356
+ }
357
+ const { outcome } = status;
358
+ if (outcome !== 'unauthenticated' && outcome !== 'not_enrolled' && outcome !== 'closed') {
359
+ return undefined;
360
+ }
361
+ const card = memberGateCardOf(reqCtx.identity?.userId ?? '', status);
362
+ return {
363
+ notice: MEMBER_GATE_NOTICES[outcome],
364
+ ...(card ? { card } : {}),
365
+ };
366
+ }
@@ -0,0 +1,166 @@
1
+ /**
2
+ * 停车域 Port(015:三 Port 重写,breaking 0.3.0——替换旧 ParkingQueryPort/ParkingSettlementPort)。
3
+ * 结算写全部收口进 ParkingPaymentPort(厂商真实支付链);
4
+ * 旧 InMemory「calcFee/useCoupon/settlePayment」模拟结算面删除(S8)。
5
+ */
6
+ import type { MallScope } from '../scope';
7
+ import type { RequestContext } from '@tbox.cn/app-contracts';
8
+ import type {
9
+ ParkingVehicle,
10
+ ParkingPresence,
11
+ ParkingBenefit,
12
+ ParkingLocation,
13
+ ParkingPaymentRecord,
14
+ ParkingRecordPage,
15
+ ParkingInfo,
16
+ QuoteSnapshot,
17
+ } from '../domain/parking';
18
+
19
+ // ===== 停车域 service slot 词汇(与 service-slots.json 同锁) =====
20
+
21
+ export const PARKING_QUERY_SERVICE = 'parking.query';
22
+ export const PARKING_PAYMENT_SERVICE = 'parking.payment';
23
+ export const PARKING_VEHICLE_SERVICE = 'parking.vehicle';
24
+
25
+ export interface ParkingQueryContext {
26
+ readonly scope: MallScope;
27
+ readonly subjectId?: string;
28
+ readonly requestContext: RequestContext;
29
+ }
30
+
31
+ // ===== 1. ParkingQueryPort:查询(七合一) =====
32
+
33
+ export interface ParkingQueryPort {
34
+ /**
35
+ * 可选能力口:当前凭证是否不足以查询车辆(如已授权但非会员,缺 C 端会员令牌)。
36
+ * 返回 true 时调用方收口为「需先入会」,不再发出必然失败的厂商请求。
37
+ * 缺席 = provider 无此语义(调用方 typeof 守卫,照常查询)——厂商差异留在 provider 包。
38
+ */
39
+ requiresMembership?(q: ParkingQueryContext): boolean;
40
+ /**
41
+ * 可选能力口:批量入场探测(在场车 + 已停时长),仅含在场车辆。
42
+ * 缺席 = provider 无入场查询能力(调用方 typeof 守卫,按未入场渲染)——厂商差异留在 provider 包。
43
+ */
44
+ probeParkedVehicles?(
45
+ q: ParkingQueryContext,
46
+ plateNos: readonly string[],
47
+ ): Promise<readonly ParkingPresence[]>;
48
+ /** 计费报价(恒实时打厂商,W4;已支付态命中自动收口 PaymentOperation)。
49
+ * 指定车牌/vehicleRef/barcode 时返回 ≤1 条;无车辆线索("我的车"语义)扫描绑定车队时返回所有在场有费车辆(≥1 条)。空数组 = 无在场车。 */
50
+ queryFee(
51
+ q: ParkingQueryContext,
52
+ input: { plateNo?: string; vehicleRef?: string; barcode?: string },
53
+ ): Promise<readonly QuoteSnapshot[]>;
54
+ /** 绑定车辆列表(parked 可选:provider 有入场查询能力时给出) */
55
+ listVehicles(q: ParkingQueryContext): Promise<{
56
+ bound: readonly ParkingVehicle[];
57
+ }>;
58
+ /** 缴费记录(脱敏;detail 含 entry/exit/payOrderNoMasked)。
59
+ * orderStatus 可选:厂商交易状态过滤串(逗号分隔,语义由 provider 定义)。
60
+ * 缺省 = 不过滤(全量);仅在用户语义明确指定交易状态时由工具层下发。 */
61
+ listPaymentRecords(
62
+ q: ParkingQueryContext,
63
+ page: number,
64
+ pageSize: number,
65
+ orderStatus?: string,
66
+ ): Promise<ParkingRecordPage>;
67
+ /** 缴费记录详情;实现必须按当前用户记录列表解析不透明 recordRef */
68
+ getPaymentRecordDetail(
69
+ q: ParkingQueryContext,
70
+ recordRef: string,
71
+ ): Promise<ParkingPaymentRecord>;
72
+ /** 当前停车场开票能力 */
73
+ getInvoiceSettings(
74
+ q: ParkingQueryContext,
75
+ ): Promise<{ parkingInvoiceEnabled: boolean }>;
76
+ /** 可开票订单查询 */
77
+ listInvoiceableOrders(
78
+ q: ParkingQueryContext,
79
+ page: number,
80
+ pageSize: number,
81
+ ): Promise<ParkingRecordPage>;
82
+ /**
83
+ * 停车场综合信息。`pendingOrder` 是 provider 侧仍保留的查询能力,但**卡片层已无此形态**——
84
+ * 待支付单场景改由 queryParkingFee 出可缴费的报价卡,parking-info 的 kind 不含它。
85
+ */
86
+ queryParkingInfo(
87
+ q: ParkingQueryContext,
88
+ kind: 'chargeRules' | 'benefitPolicy' | 'pendingOrder' | 'vehicleLocation',
89
+ ): Promise<ParkingInfo>;
90
+ /** 可选权益(reprice 候选) */
91
+ queryApplicableBenefits(
92
+ q: ParkingQueryContext,
93
+ plateNo: string,
94
+ ): Promise<readonly ParkingBenefit[]>;
95
+ /** 寻车定位 */
96
+ locateVehicle(
97
+ q: ParkingQueryContext,
98
+ plateNo: string,
99
+ ): Promise<ParkingLocation | null>;
100
+ }
101
+
102
+ // ===== 2. ParkingPaymentPort:支付写通道(三步链 R2) =====
103
+
104
+ export interface ParkingPaymentPort {
105
+ /**
106
+ * step1:建单(quoteRef → 厂商支付单)。零元/免支付单可能无 payOrderId(直接 reconcile 收口)。
107
+ * detail:下单所需业务字段(真实链路可能必填 plateNo/payableCents;
108
+ * POC/mock 实现可忽略)。
109
+ */
110
+ createOrder(
111
+ q: ParkingQueryContext,
112
+ quoteRef: string,
113
+ detail?: {
114
+ plateNo?: string;
115
+ payableCents?: number;
116
+ barcode?: string;
117
+ /** 用户最终勾选的权益引用(跨请求无状态场景必填) */
118
+ benefitRefs?: readonly string[];
119
+ },
120
+ ): Promise<{ payOrderId?: string; paymentRef: string }>;
121
+ /** 查厂商单(channel:50 = 支付宝,不支持即拒;payableFeeCents 厂商侧权威金额) */
122
+ getOrderInfo(
123
+ q: ParkingQueryContext,
124
+ paymentRef: string,
125
+ ): Promise<{
126
+ orderInfo?: string;
127
+ channel: string;
128
+ payableFeeCents: number | undefined;
129
+ }>;
130
+ /** step2 前置:拉起签名串(orderInfo=my.tradePay tradeNO;payOrderFlowId 供结果上报回传) */
131
+ getPaymentSign(
132
+ q: ParkingQueryContext,
133
+ paymentRef: string,
134
+ ): Promise<{ orderInfo: string; payOrderFlowId?: string }>;
135
+ /** step3:客户端结果上报(status 仅上报,不作收口依据;flowId 来自 getPaymentSign) */
136
+ uploadResult(
137
+ q: ParkingQueryContext,
138
+ paymentRef: string,
139
+ status: 'success' | 'cancel' | 'failure' | 'unavailable',
140
+ payOrderFlowId?: string,
141
+ ): Promise<void>;
142
+ /** 权威收口唯一源(F5):厂商侧回查支付态 */
143
+ reconcile(
144
+ q: ParkingQueryContext,
145
+ paymentRef: string,
146
+ ): Promise<'PAID' | 'PAYING' | 'CANCELLED' | 'UNKNOWN'>;
147
+ /** 权益重选(reprice):新报价(revision 递增)。detail 透传试算业务字段(部分服务必填) */
148
+ reprice(
149
+ q: ParkingQueryContext,
150
+ quoteRef: string,
151
+ benefitRefs: readonly string[],
152
+ detail?: { plateNo?: string; payableCents?: number },
153
+ ): Promise<QuoteSnapshot>;
154
+ /** 申请停车发票;实现必须再次校验 recordRef 属于当前用户 */
155
+ applyInvoice(q: ParkingQueryContext, recordRef: string): Promise<void>;
156
+ }
157
+
158
+ // ===== 3. ParkingVehiclePort:车辆管理写通道 =====
159
+
160
+ export interface ParkingVehiclePort {
161
+ bindVehicle(q: ParkingQueryContext, plateNo: string): Promise<ParkingVehicle>;
162
+ unbindVehicle(
163
+ q: ParkingQueryContext,
164
+ input: { plateNo?: string; vehicleRef?: string },
165
+ ): Promise<void>;
166
+ }