653 lines
22 KiB
JavaScript
653 lines
22 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { buildApp } from '../dist/app.js';
|
|
import { signAccessToken } from '../dist/auth/jwt.js';
|
|
|
|
const secret = 'test-only-cleaning-route-secret';
|
|
const token = signAccessToken({
|
|
sub: '31', sid: '9c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
|
|
tid: '7', aid: '9', rv: 1
|
|
}, secret, 900);
|
|
const forbiddenToken = signAccessToken({
|
|
sub: '32', sid: '8c4d3af8-c63c-4edb-bf95-b84127bb3f6e',
|
|
tid: '7', aid: '9', rv: 1
|
|
}, secret, 900);
|
|
|
|
const calls = [];
|
|
const app = await buildApp({
|
|
cleaning: {
|
|
jwtSecret: secret,
|
|
authRepository: {
|
|
async validateSession(sessionId) {
|
|
return {
|
|
id: sessionId,
|
|
tenantId: '7',
|
|
platformAppId: '9',
|
|
expiresAt: new Date(Date.now() + 60000),
|
|
user: {
|
|
id: sessionId.startsWith('8') ? '32' : '31',
|
|
tenantId: '7',
|
|
userType: 'CUSTOMER',
|
|
status: 'ACTIVE',
|
|
roleVersion: 1,
|
|
nickname: '',
|
|
avatarUrl: '',
|
|
phone: ''
|
|
}
|
|
};
|
|
}
|
|
},
|
|
accessControl: {
|
|
async getAccessProfile(tenantId, userId) {
|
|
return userId === '31'
|
|
? {
|
|
roles: ['CLEANER'],
|
|
capabilities: [
|
|
'cleaning.task.read', 'cleaning.task.write', 'cleaning.statistics.read',
|
|
'cleaning.settlement.read', 'cleaning.settlement.write'
|
|
],
|
|
storeIds: ['11']
|
|
}
|
|
: { roles: ['CUSTOMER'], capabilities: ['profile.read'], storeIds: [] };
|
|
}
|
|
},
|
|
repository: {
|
|
async listHall(input) {
|
|
calls.push(['listHall', input]);
|
|
return { items: [task('WAITING')], total: 1, page: input.page, pageSize: input.pageSize };
|
|
},
|
|
async listMine(input) {
|
|
calls.push(['listMine', input]);
|
|
return { items: [task('CLAIMED')], total: 1, page: input.page, pageSize: input.pageSize };
|
|
},
|
|
async listManage(input) {
|
|
calls.push(['listManage', input]);
|
|
return { items: [task(input.status || 'SUBMITTED')], total: 1, page: input.page, pageSize: input.pageSize };
|
|
},
|
|
async claim(input) {
|
|
calls.push(['claim', input]);
|
|
return task('CLAIMED');
|
|
},
|
|
async start(input) {
|
|
calls.push(['start', input]);
|
|
return task('STARTED');
|
|
},
|
|
async rework(input) {
|
|
calls.push(['rework', input]);
|
|
return task('STARTED');
|
|
},
|
|
async assertCanUploadPhoto(input) {
|
|
calls.push(['assertCanUploadPhoto', input]);
|
|
},
|
|
async submit(input) {
|
|
calls.push(['submit', input]);
|
|
return { ...task('SUBMITTED'), photoUrls: input.photoUrls };
|
|
},
|
|
async assign(input) {
|
|
calls.push(['assign', input]);
|
|
return { ...task('CLAIMED'), cleanerUserId: input.cleanerUserId };
|
|
},
|
|
async complete(input) {
|
|
calls.push(['complete', input]);
|
|
return task('COMPLETED');
|
|
},
|
|
async reject(input) {
|
|
calls.push(['reject', input]);
|
|
return { ...task('REJECTED'), rejectReason: input.reason };
|
|
},
|
|
async listMembers(input) {
|
|
calls.push(['listMembers', input]);
|
|
return [member('LEAD', '31', 500), member('ASSIST', '33', 100)];
|
|
},
|
|
async addMember(input) {
|
|
calls.push(['addMember', input]);
|
|
return [member('LEAD', '31', 500), member('ASSIST', input.cleanerUserId, input.rewardCents)];
|
|
},
|
|
async removeMember(input) {
|
|
calls.push(['removeMember', input]);
|
|
return [member('LEAD', '31', 600)];
|
|
},
|
|
async settlementCandidates(input) {
|
|
calls.push(['settlementCandidates', input]);
|
|
return { items: [task('COMPLETED')], total: 1, page: input.page, pageSize: input.pageSize };
|
|
},
|
|
async listSettlements(input) {
|
|
calls.push(['listSettlements', input]);
|
|
return { items: [settlementResponse(input.status || 'DRAFT')], total: 1, page: input.page, pageSize: input.pageSize };
|
|
},
|
|
async getSettlementDetail(input) {
|
|
calls.push(['getSettlementDetail', input]);
|
|
return {
|
|
settlement: settlementResponse('DRAFT'),
|
|
items: [settlementItemResponse()]
|
|
};
|
|
},
|
|
async generateSettlement(input) {
|
|
calls.push(['generateSettlement', input]);
|
|
return settlementResponse('DRAFT');
|
|
},
|
|
async confirmSettlement(input) {
|
|
calls.push(['confirmSettlement', input]);
|
|
return settlementResponse('CONFIRMED');
|
|
},
|
|
async markSettlementPaid(input) {
|
|
calls.push(['markSettlementPaid', input]);
|
|
return {
|
|
...settlementResponse('PAID'),
|
|
paidBy: input.userId,
|
|
paidAt: new Date(),
|
|
payoutChannel: input.payoutChannel,
|
|
payoutReference: input.payoutReference,
|
|
payoutError: ''
|
|
};
|
|
},
|
|
async recordSettlementPayoutFailure(input) {
|
|
calls.push(['recordSettlementPayoutFailure', input]);
|
|
return {
|
|
...settlementResponse('CONFIRMED'),
|
|
payoutChannel: input.payoutChannel || '',
|
|
payoutReference: input.payoutReference || '',
|
|
payoutError: input.error
|
|
};
|
|
},
|
|
async reclaimTimeouts(input) {
|
|
calls.push(['reclaimTimeouts', input]);
|
|
return { reclaimed: 2, taskIds: ['101', '102'] };
|
|
},
|
|
async stats(input) {
|
|
calls.push(['stats', input]);
|
|
return { byStatus: { SUBMITTED: 2 }, pendingSettlementCents: 1200 };
|
|
},
|
|
async managerStatistics(input) {
|
|
calls.push(['managerStatistics', input]);
|
|
return {
|
|
summary: {
|
|
taskTotal: 5,
|
|
pendingReview: 2,
|
|
active: 1,
|
|
rejected: 1,
|
|
completed: 1,
|
|
rewardCents: 3000,
|
|
pendingSettlementCents: 1200,
|
|
confirmedSettlementCents: 800,
|
|
paidSettlementCents: 600
|
|
},
|
|
byStatus: [{ status: 'SUBMITTED', total: 2, rewardCents: 1200 }],
|
|
byStore: [{
|
|
storeId: '11',
|
|
storeName: 'Test Store',
|
|
taskTotal: 5,
|
|
pendingReview: 2,
|
|
completed: 1,
|
|
rejected: 1,
|
|
rewardCents: 3000
|
|
}],
|
|
settlements: [{ status: 'CONFIRMED', total: 1, rewardCents: 800 }],
|
|
members: [{
|
|
cleanerUserId: '31',
|
|
cleanerName: 'Cleaner',
|
|
taskCount: 3,
|
|
pendingSettlementCents: 1200,
|
|
settledRewardCents: 600
|
|
}],
|
|
trend: [{
|
|
date: '2026-06-30',
|
|
taskTotal: 5,
|
|
pendingReview: 2,
|
|
completed: 1,
|
|
rejected: 1,
|
|
rewardCents: 3000,
|
|
paidSettlementCents: 600
|
|
}]
|
|
};
|
|
}
|
|
},
|
|
payoutService: {
|
|
async preflightWechatTransfer(input) {
|
|
calls.push(['preflightWechatTransfer', input]);
|
|
return {
|
|
ready: true,
|
|
settlement: { id: input.settlementId, settlementNo: 'CLS-501', status: 'CONFIRMED' },
|
|
account: { configured: true, merchantIdMasked: '19***0109' },
|
|
credential: { configured: true, transferSceneId: '1000', reportInfoCount: 1 },
|
|
cleaner: { openidConfigured: true },
|
|
checks: [{ key: 'transfer_scene_id', status: 'PASS', message: 'ok' }]
|
|
};
|
|
},
|
|
async executeWechatTransfer(input) {
|
|
calls.push(['executeWechatTransfer', input]);
|
|
return {
|
|
settlement: settlementResponse(input.mode === 'MOCK' ? 'PAID' : 'CONFIRMED'),
|
|
transferState: input.mode === 'MOCK' ? 'SUCCESS' : 'WAIT_USER_CONFIRM',
|
|
idempotent: false
|
|
};
|
|
},
|
|
async syncWechatTransfer(input) {
|
|
calls.push(['syncWechatTransfer', input]);
|
|
return {
|
|
settlement: {
|
|
...settlementResponse('PAID'),
|
|
payoutChannel: 'WECHAT_TRANSFER',
|
|
payoutReference: 'wx-transfer-sync-501'
|
|
},
|
|
transferState: 'SUCCESS',
|
|
idempotent: false
|
|
};
|
|
},
|
|
async processWechatTransferNotification(headers, rawBody, traceId) {
|
|
calls.push(['processWechatTransferNotification', { headers, rawBody, traceId }]);
|
|
return {
|
|
settlement: {
|
|
...settlementResponse('PAID'),
|
|
payoutChannel: 'WECHAT_TRANSFER',
|
|
payoutReference: 'wx-transfer-notify-501'
|
|
},
|
|
transferState: 'SUCCESS',
|
|
idempotent: false
|
|
};
|
|
}
|
|
},
|
|
mediaStorage: {
|
|
async storeImage(input) {
|
|
calls.push(['storeImage', input]);
|
|
return {
|
|
storagePath: 'tenants/7/shared/cleaning.webp',
|
|
publicUrl: 'https://api.txyundm.cn/uploads/tenants/7/shared/cleaning.webp',
|
|
mimeType: 'image/webp',
|
|
byteSize: input.body.length,
|
|
width: 640,
|
|
height: 480,
|
|
checksumSha256: 'abc'
|
|
};
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const hall = await app.inject({
|
|
method: 'GET',
|
|
url: '/app-api/cleaning/tasks/hall?page=1&pageSize=10',
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
assert.equal(hall.statusCode, 200);
|
|
assert.equal(hall.json().data.items[0].status, 'WAITING');
|
|
assert.equal(calls.at(-1)[1].tenantId, '7');
|
|
assert.equal(calls.at(-1)[1].userId, '31');
|
|
|
|
const mine = await app.inject({
|
|
method: 'GET',
|
|
url: '/app-api/cleaning/tasks/mine?status=CLAIMED',
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
assert.equal(mine.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'listMine');
|
|
assert.equal(calls.at(-1)[1].status, 'CLAIMED');
|
|
|
|
const manageList = await app.inject({
|
|
method: 'GET',
|
|
url: '/admin-api/cleaning/tasks?status=SUBMITTED&storeId=11&cleanerUserId=31',
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
assert.equal(manageList.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'listManage');
|
|
assert.equal(calls.at(-1)[1].status, 'SUBMITTED');
|
|
assert.equal(calls.at(-1)[1].storeId, '11');
|
|
assert.equal(calls.at(-1)[1].cleanerUserId, '31');
|
|
|
|
const managerStats = await app.inject({
|
|
method: 'GET',
|
|
url: '/admin-api/cleaning/statistics?from=2026-06-01&to=2026-07-01&storeId=11&cleanerUserId=31',
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
assert.equal(managerStats.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'managerStatistics');
|
|
assert.equal(calls.at(-1)[1].storeId, '11');
|
|
assert.equal(calls.at(-1)[1].cleanerUserId, '31');
|
|
assert.equal(managerStats.json().data.summary.pendingSettlementCents, 1200);
|
|
assert.equal(managerStats.json().data.trend[0].paidSettlementCents, 600);
|
|
|
|
const claim = await app.inject({
|
|
method: 'POST',
|
|
url: '/app-api/cleaning/tasks/101/claim',
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
assert.equal(claim.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'claim');
|
|
assert.equal(calls.at(-1)[1].taskId, '101');
|
|
|
|
const start = await app.inject({
|
|
method: 'POST',
|
|
url: '/app-api/cleaning/tasks/101/start',
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
assert.equal(start.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'start');
|
|
|
|
const rework = await app.inject({
|
|
method: 'POST',
|
|
url: '/app-api/cleaning/tasks/101/rework',
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
assert.equal(rework.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'rework');
|
|
|
|
const photo = await app.inject({
|
|
method: 'POST',
|
|
url: '/app-api/cleaning/tasks/101/photos',
|
|
headers: {
|
|
authorization: `Bearer ${token}`,
|
|
'content-type': 'application/octet-stream',
|
|
'x-file-name': 'cleaning.jpg',
|
|
'x-image-content-type': 'image/jpeg'
|
|
},
|
|
payload: Buffer.from('fake-image')
|
|
});
|
|
assert.equal(photo.statusCode, 201);
|
|
assert.equal(photo.json().data.publicUrl, 'https://api.txyundm.cn/uploads/tenants/7/shared/cleaning.webp');
|
|
assert.equal(calls.at(-2)[0], 'assertCanUploadPhoto');
|
|
assert.equal(calls.at(-1)[0], 'storeImage');
|
|
|
|
const submit = await app.inject({
|
|
method: 'POST',
|
|
url: '/app-api/cleaning/tasks/101/submit',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: { photoUrls: ['https://api.txyundm.cn/uploads/cleaning/101.jpg'], note: 'ok' }
|
|
});
|
|
assert.equal(submit.statusCode, 200);
|
|
assert.deepEqual(calls.at(-1)[1].photoUrls, ['https://api.txyundm.cn/uploads/cleaning/101.jpg']);
|
|
|
|
const assign = await app.inject({
|
|
method: 'POST',
|
|
url: '/admin-api/cleaning/tasks/101/assign',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: { cleanerUserId: '33', note: 'manual dispatch' }
|
|
});
|
|
assert.equal(assign.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'assign');
|
|
assert.equal(calls.at(-1)[1].cleanerUserId, '33');
|
|
|
|
const complete = await app.inject({
|
|
method: 'POST',
|
|
url: '/admin-api/cleaning/tasks/101/complete',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: { note: 'ok' }
|
|
});
|
|
assert.equal(complete.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'complete');
|
|
|
|
const reject = await app.inject({
|
|
method: 'POST',
|
|
url: '/admin-api/cleaning/tasks/101/reject',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: { reason: 'photo is unclear' }
|
|
});
|
|
assert.equal(reject.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'reject');
|
|
assert.equal(calls.at(-1)[1].reason, 'photo is unclear');
|
|
|
|
const members = await app.inject({
|
|
method: 'GET',
|
|
url: '/admin-api/cleaning/tasks/101/members',
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
assert.equal(members.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'listMembers');
|
|
assert.equal(members.json().data.length, 2);
|
|
|
|
const addedMember = await app.inject({
|
|
method: 'POST',
|
|
url: '/admin-api/cleaning/tasks/101/members',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: { cleanerUserId: '33', rewardCents: 100, note: 'support' }
|
|
});
|
|
assert.equal(addedMember.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'addMember');
|
|
assert.equal(calls.at(-1)[1].cleanerUserId, '33');
|
|
assert.equal(calls.at(-1)[1].rewardCents, 100);
|
|
|
|
const removedMember = await app.inject({
|
|
method: 'POST',
|
|
url: '/admin-api/cleaning/tasks/101/members/33/remove',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: { note: 'done' }
|
|
});
|
|
assert.equal(removedMember.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'removeMember');
|
|
assert.equal(calls.at(-1)[1].cleanerUserId, '33');
|
|
|
|
const settlement = await app.inject({
|
|
method: 'GET',
|
|
url: '/admin-api/cleaning/settlement-candidates?page=1&pageSize=10&storeId=11&cleanerUserId=31',
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
assert.equal(settlement.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'settlementCandidates');
|
|
assert.equal(calls.at(-1)[1].storeId, '11');
|
|
assert.equal(calls.at(-1)[1].cleanerUserId, '31');
|
|
|
|
const settlementList = await app.inject({
|
|
method: 'GET',
|
|
url: '/admin-api/cleaning/settlements?status=DRAFT&storeId=11&cleanerUserId=31',
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
assert.equal(settlementList.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'listSettlements');
|
|
assert.equal(calls.at(-1)[1].status, 'DRAFT');
|
|
assert.equal(calls.at(-1)[1].storeId, '11');
|
|
assert.equal(calls.at(-1)[1].cleanerUserId, '31');
|
|
|
|
const settlementDetail = await app.inject({
|
|
method: 'GET',
|
|
url: '/admin-api/cleaning/settlements/501',
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
assert.equal(settlementDetail.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'getSettlementDetail');
|
|
assert.equal(settlementDetail.json().data.items[0].taskNo, 'CLN-20260625-0001');
|
|
|
|
const generatedSettlement = await app.inject({
|
|
method: 'POST',
|
|
url: '/admin-api/cleaning/settlements',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: { cleanerUserId: '31', storeId: '11', note: 'weekly settlement' }
|
|
});
|
|
assert.equal(generatedSettlement.statusCode, 201);
|
|
assert.equal(calls.at(-1)[0], 'generateSettlement');
|
|
assert.equal(calls.at(-1)[1].cleanerUserId, '31');
|
|
|
|
const confirmedSettlement = await app.inject({
|
|
method: 'POST',
|
|
url: '/admin-api/cleaning/settlements/501/confirm',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: { note: 'confirmed' }
|
|
});
|
|
assert.equal(confirmedSettlement.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'confirmSettlement');
|
|
assert.equal(calls.at(-1)[1].settlementId, '501');
|
|
|
|
const failedPayout = await app.inject({
|
|
method: 'POST',
|
|
url: '/admin-api/cleaning/settlements/501/payout-failure',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: {
|
|
payoutChannel: 'WECHAT_TRANSFER',
|
|
payoutReference: 'wx-failed-001',
|
|
error: 'insufficient merchant balance',
|
|
note: 'retry later'
|
|
}
|
|
});
|
|
assert.equal(failedPayout.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'recordSettlementPayoutFailure');
|
|
assert.equal(calls.at(-1)[1].settlementId, '501');
|
|
assert.equal(calls.at(-1)[1].error, 'insufficient merchant balance');
|
|
|
|
const paidSettlement = await app.inject({
|
|
method: 'POST',
|
|
url: '/admin-api/cleaning/settlements/501/paid',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: {
|
|
payoutChannel: 'WECHAT_TRANSFER',
|
|
payoutReference: 'wx-paid-001',
|
|
note: 'paid'
|
|
}
|
|
});
|
|
assert.equal(paidSettlement.statusCode, 200);
|
|
assert.equal(paidSettlement.json().data.status, 'PAID');
|
|
assert.equal(paidSettlement.json().data.payoutReference, 'wx-paid-001');
|
|
assert.equal(calls.at(-1)[0], 'markSettlementPaid');
|
|
|
|
const wechatTransfer = await app.inject({
|
|
method: 'POST',
|
|
url: '/admin-api/cleaning/settlements/501/wechat-transfer',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: { mode: 'API', note: 'wechat transfer' }
|
|
});
|
|
assert.equal(wechatTransfer.statusCode, 200);
|
|
assert.equal(wechatTransfer.json().data.transferState, 'WAIT_USER_CONFIRM');
|
|
assert.equal(calls.at(-1)[0], 'executeWechatTransfer');
|
|
assert.equal(calls.at(-1)[1].settlementId, '501');
|
|
assert.equal(calls.at(-1)[1].mode, 'API');
|
|
|
|
const wechatTransferPreflight = await app.inject({
|
|
method: 'GET',
|
|
url: '/admin-api/cleaning/settlements/501/wechat-transfer/preflight',
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
assert.equal(wechatTransferPreflight.statusCode, 200);
|
|
assert.equal(wechatTransferPreflight.json().data.ready, true);
|
|
assert.equal(wechatTransferPreflight.json().data.account.merchantIdMasked, '19***0109');
|
|
assert.equal(calls.at(-1)[0], 'preflightWechatTransfer');
|
|
assert.equal(calls.at(-1)[1].settlementId, '501');
|
|
|
|
const syncedWechatTransfer = await app.inject({
|
|
method: 'POST',
|
|
url: '/admin-api/cleaning/settlements/501/wechat-transfer/sync',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: { note: 'poll wechat transfer' }
|
|
});
|
|
assert.equal(syncedWechatTransfer.statusCode, 200);
|
|
assert.equal(syncedWechatTransfer.json().data.transferState, 'SUCCESS');
|
|
assert.equal(syncedWechatTransfer.json().data.settlement.payoutReference, 'wx-transfer-sync-501');
|
|
assert.equal(calls.at(-1)[0], 'syncWechatTransfer');
|
|
assert.equal(calls.at(-1)[1].settlementId, '501');
|
|
|
|
const wechatTransferNotify = await app.inject({
|
|
method: 'POST',
|
|
url: '/app-api/cleaning/wechat-transfer/notify',
|
|
headers: {
|
|
'content-type': 'application/json',
|
|
'wechatpay-timestamp': '1782700001',
|
|
'wechatpay-nonce': 'notify-nonce',
|
|
'wechatpay-serial': 'PLATFORM-SERIAL',
|
|
'wechatpay-signature': 'signature'
|
|
},
|
|
payload: '{"resource":"encrypted"}'
|
|
});
|
|
assert.equal(wechatTransferNotify.statusCode, 200);
|
|
assert.equal(wechatTransferNotify.json().code, 'SUCCESS');
|
|
assert.equal(calls.at(-1)[0], 'processWechatTransferNotification');
|
|
assert.equal(calls.at(-1)[1].headers.serial, 'PLATFORM-SERIAL');
|
|
assert.equal(calls.at(-1)[1].rawBody, '{"resource":"encrypted"}');
|
|
|
|
const reclaimed = await app.inject({
|
|
method: 'POST',
|
|
url: '/admin-api/cleaning/reclaim-timeouts',
|
|
headers: { authorization: `Bearer ${token}` },
|
|
payload: { olderThanMinutes: 30, limit: 10 }
|
|
});
|
|
assert.equal(reclaimed.statusCode, 200);
|
|
assert.equal(calls.at(-1)[0], 'reclaimTimeouts');
|
|
assert.equal(calls.at(-1)[1].olderThanMinutes, 30);
|
|
|
|
const stats = await app.inject({
|
|
method: 'GET',
|
|
url: '/app-api/cleaning/stats',
|
|
headers: { authorization: `Bearer ${token}` }
|
|
});
|
|
assert.equal(stats.statusCode, 200);
|
|
assert.equal(stats.json().data.pendingSettlementCents, 1200);
|
|
|
|
const unauthorized = await app.inject({ method: 'GET', url: '/app-api/cleaning/tasks/hall' });
|
|
assert.equal(unauthorized.statusCode, 401);
|
|
|
|
const forbidden = await app.inject({
|
|
method: 'GET',
|
|
url: '/app-api/cleaning/tasks/hall',
|
|
headers: { authorization: `Bearer ${forbiddenToken}` }
|
|
});
|
|
assert.equal(forbidden.statusCode, 403);
|
|
|
|
await app.close();
|
|
console.log('PASS: M08-B cleaning routes authenticate and forward cleaner task workflows.');
|
|
|
|
function task(status) {
|
|
return {
|
|
id: '101',
|
|
taskNo: 'CLN-20260625-0001',
|
|
storeId: '11',
|
|
storeName: 'Test Store',
|
|
roomId: '21',
|
|
roomName: 'A Room',
|
|
roomNo: 'A01',
|
|
orderId: '301',
|
|
orderNo: 'O301',
|
|
status,
|
|
rewardCents: 600,
|
|
photoUrls: []
|
|
};
|
|
}
|
|
|
|
function settlementResponse(status) {
|
|
return {
|
|
id: '501',
|
|
settlementNo: 'CLS-20260626-0001',
|
|
cleanerUserId: '31',
|
|
cleanerName: 'Cleaner',
|
|
storeId: '11',
|
|
storeName: 'Test Store',
|
|
status,
|
|
taskCount: 2,
|
|
totalRewardCents: 1200,
|
|
periodStart: new Date(),
|
|
periodEnd: new Date(),
|
|
paidBy: status === 'PAID' ? '31' : null,
|
|
confirmedAt: status === 'CONFIRMED' ? new Date() : null,
|
|
paidAt: status === 'PAID' ? new Date() : null,
|
|
payoutChannel: '',
|
|
payoutReference: '',
|
|
payoutError: '',
|
|
note: ''
|
|
};
|
|
}
|
|
|
|
function settlementItemResponse() {
|
|
return {
|
|
id: '601',
|
|
settlementId: '501',
|
|
taskId: '101',
|
|
taskNo: 'CLN-20260625-0001',
|
|
orderNo: 'O301',
|
|
storeName: 'Test Store',
|
|
roomName: 'A Room',
|
|
roomNo: 'A01',
|
|
cleanerUserId: '31',
|
|
cleanerName: 'Cleaner',
|
|
rewardCents: 600,
|
|
completedAt: new Date(),
|
|
createdAt: new Date()
|
|
};
|
|
}
|
|
|
|
function member(memberRole, userId, rewardCents) {
|
|
return {
|
|
id: `${userId}01`,
|
|
taskId: '101',
|
|
userId,
|
|
nickname: `Cleaner ${userId}`,
|
|
memberRole,
|
|
rewardCents,
|
|
joinedAt: new Date(),
|
|
removedAt: null,
|
|
settledAt: null
|
|
};
|
|
}
|