feat(round-10): 新增出海潜客多时区最佳联系窗口算法与Stripe回调幂等性防重处理 (79套件957测试全绿)
这个提交包含在:
@@ -0,0 +1,45 @@
|
||||
import {
|
||||
resolveBestContactWindow,
|
||||
ContactWindowInfo,
|
||||
} from '../../src/comm/i18n/geo';
|
||||
|
||||
describe('Point 39: 出海潜客多时区识别与最佳联系时间窗口推荐 (Best Contact Time Window)', () => {
|
||||
describe('1. 国际多国家/时区时间计算', () => {
|
||||
it('应准确计算中东阿联酋 (UTC+4) 当地时间与联系窗口', () => {
|
||||
// 假设 UTC 时间为 06:00 (北京时间 14:00,阿联酋当地时间为 10:00)
|
||||
const baseUtcDate = new Date(Date.UTC(2026, 8, 21, 6, 0, 0));
|
||||
|
||||
const windowInfo: ContactWindowInfo = resolveBestContactWindow(
|
||||
'AE',
|
||||
baseUtcDate
|
||||
);
|
||||
|
||||
expect(windowInfo.localHour).toBe(10);
|
||||
expect(windowInfo.isWorkHour).toBe(true);
|
||||
expect(windowInfo.status).toBe('recommended');
|
||||
expect(windowInfo.suggestionText).toContain('当地工作时间');
|
||||
});
|
||||
|
||||
it('当海外客商处于深夜休息时段 (如欧洲当地 23:00) 时,建议勿扰', () => {
|
||||
// 假设 UTC 时间为 22:00 (欧洲 CET UTC+1 当地为 23:00)
|
||||
const baseUtcDate = new Date(Date.UTC(2026, 8, 21, 22, 0, 0));
|
||||
|
||||
const windowInfo: ContactWindowInfo = resolveBestContactWindow(
|
||||
'DE',
|
||||
baseUtcDate
|
||||
);
|
||||
|
||||
expect(windowInfo.localHour).toBe(23);
|
||||
expect(windowInfo.isWorkHour).toBe(false);
|
||||
expect(windowInfo.status).toBe('do_not_disturb');
|
||||
expect(windowInfo.suggestionText).toContain('休息时段');
|
||||
});
|
||||
|
||||
it('对于未识别国家时区时,默认安全降级返回可接受状态', () => {
|
||||
const windowInfo = resolveBestContactWindow(undefined);
|
||||
expect(windowInfo).toBeDefined();
|
||||
expect(typeof windowInfo.localHour).toBe('number');
|
||||
expect(typeof windowInfo.suggestionText).toBe('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -436,4 +436,63 @@ describe('Point 21: Stripe 国际信用卡订阅支付与 Webhook 自动化入
|
||||
expect(res.message).toContain('签名验证失败');
|
||||
});
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// 5. Webhook 幂等去重与异常事件状态机 (Idempotency & Failure Grace Period)
|
||||
// =========================================================================
|
||||
describe('5. Webhook 幂等去重与异常事件状态机', () => {
|
||||
it('5.1 同一 event.id 重复推送时触发幂等拦截,防止重复入账', async () => {
|
||||
const event: StripeWebhookEvent = {
|
||||
id: 'evt_unique_1001',
|
||||
type: 'checkout.session.completed',
|
||||
created: 1700000000,
|
||||
data: {
|
||||
object: {
|
||||
client_reference_id: 88,
|
||||
metadata: { tenantId: '88', plan: 'pro' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const firstRes = await stripeService.handleWebhookEvent(event);
|
||||
expect(firstRes.action).toBe('upgraded');
|
||||
|
||||
// 重复推送相同事件
|
||||
const secondRes = await stripeService.handleWebhookEvent(event);
|
||||
expect(secondRes.success).toBe(true);
|
||||
expect(secondRes.action).toBe('idempotent_ignored');
|
||||
});
|
||||
|
||||
it('5.2 收到 invoice.payment_failed 扣款失败时流转为 past_due 宽限期', async () => {
|
||||
// 先建立活跃订阅
|
||||
subscriptions.push({
|
||||
id: 99,
|
||||
tenantId: 66,
|
||||
planType: 'pro',
|
||||
billingCycle: 'monthly',
|
||||
status: 'active',
|
||||
stripeSubscriptionId: 'sub_pastdue_test',
|
||||
currentPeriodEnd: new Date().toISOString(),
|
||||
} as TeamSubscriptionEntity);
|
||||
|
||||
const failEvent: StripeWebhookEvent = {
|
||||
id: 'evt_fail_999',
|
||||
type: 'invoice.payment_failed',
|
||||
created: 1700000000,
|
||||
data: {
|
||||
object: {
|
||||
subscription: 'sub_pastdue_test',
|
||||
metadata: { tenantId: '66' },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const res = await stripeService.handleWebhookEvent(failEvent);
|
||||
expect(res.status).toBe('past_due');
|
||||
expect(res.action).toBe('payment_failed');
|
||||
|
||||
const updated = subscriptions.find(s => s.tenantId === 66);
|
||||
expect(updated?.status).toBe('past_due');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
在新工单中引用
屏蔽一个用户