feat(M08-D): 补加盟申请与跟进运营

This commit is contained in:
Codex
2026-08-10 13:17:19 +08:00
parent e0e17159a7
commit aa4a6bd014
28 changed files with 894 additions and 13 deletions
@@ -16,6 +16,7 @@ import { RbacRepository } from '../dist/auth/rbac-repository.js';
import { UserManagementRepository } from '../dist/auth/user-management-repository.js';
import { StoreRoomRepository, StoreRoomError } from '../dist/stores/store-room-repository.js';
import { ContentRepository, ContentError } from '../dist/content/content-repository.js';
import { FranchiseRepository, FranchiseError } from '../dist/franchise/franchise-repository.js';
import { StoreDiscoveryRepository } from '../dist/stores/store-discovery-repository.js';
import { StoreAccessRepository, StoreAccessError } from '../dist/stores/access-repository.js';
import { PricingRepository, PricingError } from '../dist/orders/pricing-repository.js';
@@ -60,6 +61,8 @@ const expectedTables = [
'qipai_device_status_snapshots',
'qipai_devices',
'qipai_direct_bookings',
'qipai_franchise_applications',
'qipai_franchise_follow_ups',
'qipai_group_redemptions',
'qipai_group_vouchers',
'qipai_holiday_calendar',
@@ -129,13 +132,13 @@ async function readMigrationVersions(pool) {
const [rows] = await pool.query(
`SELECT version, name
FROM qipai_schema_migrations
WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
WHERE version IN (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ORDER BY version`,
['2026061601', '2026061802', '2026061803', '2026061804',
'2026061805', '2026061806', '2026061807', '2026061808', '2026061809',
'2026061810', '2026061811', '2026062012', '2026062013', '2026062014',
'2026062015', '2026062216', '2026062217', '2026062218', '2026062219',
'2026062220', '2026081002']
'2026062220', '2026081002', '2026081003']
);
return rows;
}
@@ -1748,6 +1751,50 @@ async function assertContentManagement(pool, context) {
);
}
async function assertFranchiseManagement(pool, context) {
const [adminRows] = await pool.query(
`SELECT u.id FROM qipai_users u
INNER JOIN qipai_user_roles ur ON ur.tenant_id = u.tenant_id AND ur.user_id = u.id
INNER JOIN qipai_roles r ON r.id = ur.role_id AND r.tenant_id = ur.tenant_id
WHERE u.tenant_id = ? AND r.code = 'TENANT_ADMIN' LIMIT 1`, [context.tenantId]
);
const adminId = String(adminRows[0].id);
const access = await new RbacRepository(pool).getAccessProfile(context.tenantId, adminId);
const actor = { tenantId: context.tenantId, userId: adminId, access,
traceId: 'm08d-franchise-live', ip: '127.0.0.1', userAgent: 'M08-D franchise live test' };
const repository = new FranchiseRepository(pool);
const input = { tenantId: context.tenantId, city: '上海市', contactName: '测试联系人',
contactPhone: '13800138000', message: '计划开设两家门店', source: 'MINIAPP',
clientRequestId: 'm08d-franchise-idempotent', traceId: actor.traceId,
ip: actor.ip, userAgent: actor.userAgent };
const first = await repository.submitApplication(input);
const duplicate = await repository.submitApplication(input);
assert.equal(first.idempotent, false);
assert.equal(duplicate.idempotent, true);
assert.equal(duplicate.applicationId, first.applicationId);
assert.equal((await repository.listApplications({ tenantId: context.tenantId,
page: 1, pageSize: 20, status: 'NEW' })).total, 1);
await assert.rejects(
() => repository.assignApplication(actor, context.tenantId, first.applicationId, '999999999'),
(error) => error instanceof FranchiseError && error.code === 'FRANCHISE_ASSIGNEE_INVALID'
);
await repository.assignApplication(actor, context.tenantId, first.applicationId, adminId);
await repository.addFollowUp(actor, context.tenantId, first.applicationId, {
followUpType: 'CALL', note: '已完成首次电话沟通', status: 'CONTACTED', nextFollowUpAt: null
});
await assert.rejects(() => repository.addFollowUp(actor, context.tenantId, first.applicationId, {
followUpType: 'NOTE', note: '跳过资格确认', status: 'CONVERTED'
}), (error) => error instanceof FranchiseError && error.code === 'FRANCHISE_STATUS_TRANSITION_INVALID');
const detail = await repository.getApplication(context.tenantId, first.applicationId);
assert.equal(detail.application.status, 'CONTACTED');
assert.equal(detail.followUps.length, 2);
const [auditRows] = await pool.query(
`SELECT CAST(metadata AS CHAR) AS metadata FROM qipai_audit_logs
WHERE tenant_id = ? AND resource_type = 'FRANCHISE_APPLICATION'`, [context.tenantId]
);
assert.equal(auditRows.some((row) => row.metadata.includes('13800138000')), false);
}
async function assertDeviceTopology(pool, context) {
const [adminRows] = await pool.query(
`SELECT u.id FROM qipai_users u
@@ -2026,7 +2073,8 @@ try {
{ version: '2026062218', name: 'm05d_profit_sharing' },
{ version: '2026062219', name: 'm06b_device_topology' },
{ version: '2026062220', name: 'm06c_iot_messages' },
{ version: '2026081002', name: 'm08d_content_asset_scope' }
{ version: '2026081002', name: 'm08d_content_asset_scope' },
{ version: '2026081003', name: 'm08d_franchise_leads' }
]);
await assertTaskDurability(pool);
const loginContext = await assertPlatformTenantIsolation(pool);
@@ -2034,6 +2082,7 @@ try {
await assertUserManagement(pool, loginContext);
await assertStoreRoomDomain(pool, loginContext);
await assertContentManagement(pool, loginContext);
await assertFranchiseManagement(pool, loginContext);
await assertStoreDiscovery(pool, loginContext);
await assertSceneAndWifiAccess(pool, loginContext);
await assertPricingAndReservations(pool, loginContext);
@@ -2077,7 +2126,8 @@ try {
{ version: '2026062218', name: 'm05d_profit_sharing' },
{ version: '2026062219', name: 'm06b_device_topology' },
{ version: '2026062220', name: 'm06c_iot_messages' },
{ version: '2026081002', name: 'm08d_content_asset_scope' }
{ version: '2026081002', name: 'm08d_content_asset_scope' },
{ version: '2026081003', name: 'm08d_franchise_leads' }
]);
await assertLegacyCompatibility(pool);
console.log('PASS: second up and verify restored the schema.');