@tbox.cn/app-provider-mock 0.2.0 → 0.3.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.
- package/README.md +38 -0
- package/dist/server/index.d.ts +41 -38
- package/dist/server/index.js +258 -130
- package/package.json +6 -5
- package/schemas/mall-info.config.json +11 -0
- package/schemas/member.config.json +13 -0
- package/schemas/parking.config.json +19 -0
- package/schemas/promotion.config.json +13 -0
- package/schemas/shopping.config.json +12 -0
- package/src/server/config.ts +26 -0
- package/src/server/index.ts +16 -4
- package/src/server/mall-info.ts +27 -12
- package/src/server/member.ts +45 -17
- package/src/server/parking.ts +78 -49
- package/src/server/promotion.ts +77 -56
- package/src/server/shopping.ts +41 -3
- package/tbox.module.json +34 -17
- package/tests/config-schema.test.ts +92 -0
- package/tests/integrations-e2e.test.ts +117 -2
- package/tests/mall-info.test.ts +11 -2
- package/tests/mock-impl.test.ts +103 -0
package/tests/mock-impl.test.ts
CHANGED
|
@@ -346,3 +346,106 @@ describe('provider-mock:登出态(logout 路由 + 换码降级 GUEST 形态
|
|
|
346
346
|
await expect(member.getAccount(MOCK_SHELL_USER_ID)).resolves.toMatchObject({ state: 'active' });
|
|
347
347
|
});
|
|
348
348
|
});
|
|
349
|
+
|
|
350
|
+
describe('provider-mock:config 驱动(fail-soft 快照)', () => {
|
|
351
|
+
const enroll = (member: MockMemberServiceImpl, userId: string) =>
|
|
352
|
+
member.enroll({ userId, identity: 'enrollment_submission', fieldValues: {}, acceptedConsentIds: [], idempotencyKey: `k-${userId}` });
|
|
353
|
+
|
|
354
|
+
it('member 全字段:卡名/初值积分/等级/名称/深链/流水同源', async () => {
|
|
355
|
+
const member = new MockMemberServiceImpl({}, { cardName: '黑金卡', initialPoints: 800, levelName: '金卡', memberName: '张三' });
|
|
356
|
+
await enroll(member, 'u1');
|
|
357
|
+
await expect(member.getCard('u1')).resolves.toMatchObject({ cardDisplay: '黑金卡', levelName: '金卡' });
|
|
358
|
+
await expect(member.queryPoints('u1')).resolves.toMatchObject({ balance: 800, membershipLabel: '黑金卡' });
|
|
359
|
+
await expect(member.getProfile('u1')).resolves.toMatchObject({ labels: [{ labelRef: 'demo:label:1', name: '张三' }] });
|
|
360
|
+
const usage = await member.listPointUsage('u1', 1, 10);
|
|
361
|
+
expect(usage.items[0].amount).toBe('+800');
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('member 非法值回退缺省:类型族/空串/NaN/分式整数(intKeys)', async () => {
|
|
365
|
+
const member = new MockMemberServiceImpl({}, { initialPoints: 'x', cardName: '', enrollUrl: '', levelName: '' });
|
|
366
|
+
await enroll(member, 'u1');
|
|
367
|
+
await expect(member.queryPoints('u1')).resolves.toMatchObject({ balance: 300, membershipLabel: '示例会员卡' });
|
|
368
|
+
const card = (await member.getCard('u1'))!;
|
|
369
|
+
expect('levelName' in card).toBe(false);
|
|
370
|
+
expect(member.enrollUrl).toMatch(/^alipays:\/\//);
|
|
371
|
+
const nan = new MockMemberServiceImpl({}, { initialPoints: Number.NaN });
|
|
372
|
+
await enroll(nan, 'u2');
|
|
373
|
+
await expect(nan.queryPoints('u2')).resolves.toMatchObject({ balance: 300 });
|
|
374
|
+
const fractional = new MockMemberServiceImpl({}, { initialPoints: 1500.5 });
|
|
375
|
+
await enroll(fractional, 'u3');
|
|
376
|
+
await expect(fractional.queryPoints('u3')).resolves.toMatchObject({ balance: 300 });
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it('member 余额惰性读:入会不写种子 + 显式增减单源(queryPoints ≡ getBenefits)', async () => {
|
|
380
|
+
const member = new MockMemberServiceImpl({}, { initialPoints: 500 });
|
|
381
|
+
await expect(member.queryPoints('nobody')).resolves.toMatchObject({ balance: 0 });
|
|
382
|
+
await enroll(member, 'u1');
|
|
383
|
+
await expect(member.queryPoints('u1')).resolves.toMatchObject({ balance: 500 });
|
|
384
|
+
await expect(member.getBenefits('u1')).resolves.toMatchObject({ availablePoints: 500 });
|
|
385
|
+
await expect(member.deductPoints('u1', 100, 'd1')).resolves.toBe(true);
|
|
386
|
+
await expect(member.deductPoints('u1', 1000, 'd2')).resolves.toBe(false);
|
|
387
|
+
await expect(member.getBenefits('u1')).resolves.toMatchObject({ availablePoints: 400 });
|
|
388
|
+
await expect(member.awardPoints('u1', 50)).resolves.toBe(true);
|
|
389
|
+
await expect(member.refundPoints('u1', 25, 'r1')).resolves.toBe(true);
|
|
390
|
+
await expect(member.queryPoints('u1')).resolves.toMatchObject({ balance: 475 });
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
it('parking 驱动:费用/权益开关/开票开关/信息/记录/寻车同源', async () => {
|
|
394
|
+
const parking = new MockParkingServiceImpl('mall-demo', {
|
|
395
|
+
feeCents: 200, parkName: 'P2 停车场', memberBenefitEnabled: false, invoiceEnabled: false, remainingSpaces: 56, vehicleSpaceNo: 'C-9',
|
|
396
|
+
});
|
|
397
|
+
const quote = (await parking.queryFee(parkCtx, { plateNo: 'demo-plate' }))[0];
|
|
398
|
+
expect(quote).toMatchObject({ totalCents: 200, payableCents: 200 });
|
|
399
|
+
expect(quote.feeItems[0]).toEqual({ name: '停车费(3 小时)', amountCents: 200 });
|
|
400
|
+
const benefits = await parking.queryApplicableBenefits(parkCtx, 'demo-plate');
|
|
401
|
+
expect(benefits).toHaveLength(1);
|
|
402
|
+
expect(benefits[0].kind).toBe('points');
|
|
403
|
+
await expect(parking.getInvoiceSettings()).resolves.toEqual({ parkingInvoiceEnabled: false });
|
|
404
|
+
const info = await parking.queryParkingInfo(parkCtx, 'chargeRules');
|
|
405
|
+
expect(info.title).toBe('P2 停车场');
|
|
406
|
+
expect(info.entries).toContainEqual({ label: '剩余车位', value: '56' });
|
|
407
|
+
expect(info.entries).toContainEqual({ label: '会员权益', value: '—' });
|
|
408
|
+
await expect(parking.getOrderInfo(parkCtx, 'r')).resolves.toMatchObject({ payableFeeCents: 200 });
|
|
409
|
+
const records = await parking.listPaymentRecords(parkCtx, 1, 10);
|
|
410
|
+
expect(records.items[0]).toMatchObject({ parkName: 'P2 停车场', amountCents: 200 });
|
|
411
|
+
expect((await parking.locateVehicle(parkCtx, 'demo-plate'))?.spaceNo).toBe('C-9');
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it('promotion 驱动:券标题/面额/门槛 + 团购开关 + 资产/活动派生', async () => {
|
|
415
|
+
const promotion = new MockPromotionServiceImpl({
|
|
416
|
+
couponTitle: '满 200 减 50 元券', couponValueCents: 5000, couponThresholdCents: 20000, couponValidityDays: 7, grouponEnabled: false, activityTitle: '周年庆',
|
|
417
|
+
});
|
|
418
|
+
const page = await promotion.searchOffers(promoCtx, { keyword: '' });
|
|
419
|
+
expect(page.items).toHaveLength(1);
|
|
420
|
+
expect(page.items[0].title).toBe('满 200 减 50 元券');
|
|
421
|
+
expect(page.items[0].facets[0]).toMatchObject({ value: 5000, thresholdCents: 20000 });
|
|
422
|
+
const ents = await promotion.listEntitlements(promoCtx, {});
|
|
423
|
+
expect(ents.items[0].title).toBe('满 200 减 50 元券');
|
|
424
|
+
const acts = await promotion.listActivities();
|
|
425
|
+
expect(acts.items[0].title).toBe('周年庆');
|
|
426
|
+
await expect(promotion.getOfferDetail(promoCtx, { offerRef: 'demo:coupon:1001' })).resolves.toMatchObject({ title: '满 200 减 50 元券' });
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
it('shopping 驱动:追加门店进列表/类目/楼层目录', async () => {
|
|
430
|
+
const impl = new MockShoppingServiceImpl(undefined, {
|
|
431
|
+
extraShopName: '新店', extraShopFloor: '2 楼', extraShopCategory: '文创', extraShopUnit: '203', extraShopOpen: false,
|
|
432
|
+
});
|
|
433
|
+
const page = await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 20 });
|
|
434
|
+
expect(page.total).toBe(13);
|
|
435
|
+
const extra = page.items.find((s) => s.merchantId === 'demo-shop-1013');
|
|
436
|
+
expect(extra).toMatchObject({ name: '新店', location: { floorName: '2 楼', unitName: '203' }, status: { code: 'disabled', label: '休息中' } });
|
|
437
|
+
const cats = await impl.listCategories(shopCtx);
|
|
438
|
+
expect(cats.find((c) => c.name === '文创')?.merchantCount).toBe(1);
|
|
439
|
+
const dir = await impl.getDirectory(shopCtx);
|
|
440
|
+
expect(dir?.buildings[0].floors.find((f) => f.floorName === '2 楼')?.merchantCount).toBe(3);
|
|
441
|
+
expect((await impl.searchShops({ ctx: shopCtx, page: 1, pageSize: 20, floorCode: '2 楼' })).total).toBe(3);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('shopping 消费点守卫:空白名不追加 + 未知楼层回退 3 楼', async () => {
|
|
445
|
+
const blank = new MockShoppingServiceImpl(undefined, { extraShopName: ' ' });
|
|
446
|
+
expect((await blank.searchShops({ ctx: shopCtx, page: 1, pageSize: 20 })).total).toBe(12);
|
|
447
|
+
const badFloor = new MockShoppingServiceImpl(undefined, { extraShopName: 'X 店', extraShopFloor: '5 楼' });
|
|
448
|
+
const page = await badFloor.searchShops({ ctx: shopCtx, page: 1, pageSize: 20, floorCode: '3 楼' });
|
|
449
|
+
expect(page.items.some((s) => s.merchantId === 'demo-shop-1013')).toBe(true);
|
|
450
|
+
});
|
|
451
|
+
});
|