76f266645d
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。 排除 node_modules、构建产物、安装包与 .env 密钥。 Co-authored-by: Cursor <cursoragent@cursor.com>
1451 lines
60 KiB
TypeScript
1451 lines
60 KiB
TypeScript
import { INestApplication, ValidationPipe } from '@nestjs/common';
|
|
import { Test } from '@nestjs/testing';
|
|
import request from 'supertest';
|
|
import { AppModule } from '../src/app.module';
|
|
|
|
describe('API e2e', () => {
|
|
let app: INestApplication;
|
|
|
|
beforeAll(async () => {
|
|
const moduleRef = await Test.createTestingModule({
|
|
imports: [AppModule],
|
|
}).compile();
|
|
app = moduleRef.createNestApplication();
|
|
app.setGlobalPrefix('api/v1');
|
|
app.useGlobalPipes(new ValidationPipe({ whitelist: true, transform: true }));
|
|
await app.init();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
await app.close();
|
|
});
|
|
|
|
it('liveness', async () => {
|
|
const res = await request(app.getHttpServer()).get('/api/v1/health/live');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.code).toBe(0);
|
|
expect(res.body.data.status).toBe('ok');
|
|
});
|
|
|
|
it('readiness', async () => {
|
|
const res = await request(app.getHttpServer()).get('/api/v1/health/ready');
|
|
expect(res.status).toBe(200);
|
|
expect(res.body.data.checks.postgres).toBe('up');
|
|
});
|
|
|
|
it('login and menu', async () => {
|
|
const login = await request(app.getHttpServer())
|
|
.post('/api/v1/auth/login')
|
|
.send({ username: 'admin', password: 'Admin@123456', agreeTerms: true });
|
|
expect(login.status).toBe(201);
|
|
expect(login.body.data.accessToken).toBeDefined();
|
|
const token = login.body.data.accessToken;
|
|
const menus = await request(app.getHttpServer())
|
|
.get('/api/v1/system/menus')
|
|
.set('Authorization', `Bearer ${token}`);
|
|
expect(menus.status).toBe(200);
|
|
expect(menus.body.data.length).toBeGreaterThan(5);
|
|
});
|
|
|
|
it('rejects bad password', async () => {
|
|
const res = await request(app.getHttpServer())
|
|
.post('/api/v1/auth/login')
|
|
.send({ username: 'admin', password: 'wrong-password', agreeTerms: true });
|
|
expect(res.status).toBe(401);
|
|
expect(res.body.code).toBe(401);
|
|
});
|
|
|
|
it('rejects login without agreeing to terms', async () => {
|
|
const res = await request(app.getHttpServer())
|
|
.post('/api/v1/auth/login')
|
|
.send({ username: 'admin', password: 'Admin@123456' });
|
|
expect(res.status).toBe(400);
|
|
expect(String(res.body.message || '')).toContain('用户协议');
|
|
});
|
|
|
|
it('serves user agreement and privacy policy', async () => {
|
|
const agreement = await request(app.getHttpServer()).get('/api/v1/legal/user-agreement');
|
|
expect(agreement.status).toBe(200);
|
|
expect(agreement.body.data.title).toBe('用户协议');
|
|
expect(agreement.body.data.paragraphs.length).toBeGreaterThan(3);
|
|
const privacy = await request(app.getHttpServer()).get('/api/v1/legal/privacy');
|
|
expect(privacy.status).toBe(200);
|
|
expect(privacy.body.data.title).toBe('隐私政策');
|
|
});
|
|
|
|
async function login(username: string, password: string) {
|
|
const res = await request(app.getHttpServer())
|
|
.post('/api/v1/auth/login')
|
|
.send({ username, password, agreeTerms: true });
|
|
expect(res.status).toBe(201);
|
|
return res.body.data.accessToken as string;
|
|
}
|
|
|
|
it('runs screening to final review, and hides terminated from employees', async () => {
|
|
const owner = await login('owner', 'Demo@123456');
|
|
const directors = await request(app.getHttpServer())
|
|
.get('/api/v1/directors')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(directors.status).toBe(200);
|
|
const dirsoft = (directors.body.data as { id: string; name: string }[]).find(
|
|
(d) => d.name === '吴软件',
|
|
);
|
|
const dir3d = (directors.body.data as { id: string; name: string }[]).find(
|
|
(d) => d.name === '赵三维',
|
|
);
|
|
expect(dirsoft && dir3d).toBeTruthy();
|
|
|
|
const created = await request(app.getHttpServer())
|
|
.post('/api/v1/bid-cases')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({
|
|
name: '阶段1主链演示',
|
|
externalNo: 'E2E-P1-001',
|
|
projectType: '集成',
|
|
source: '招标公告',
|
|
qualificationNeed: '涉乙',
|
|
tenderMethod: '公开招标',
|
|
applyMethod: '网上申领',
|
|
openMethod: '线下开标',
|
|
openPlace: '交易中心',
|
|
reviewerIds: [dirsoft!.id, dir3d!.id],
|
|
});
|
|
expect(created.status).toBe(201);
|
|
const bidId = created.body.data.id as string;
|
|
expect(created.body.data.status).toBe('TECH_INITIAL');
|
|
|
|
const softToken = await login('dirsoft', 'Demo@123456');
|
|
const first = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${softToken}`)
|
|
.send({ result: 'APPROVED', comment: '软件可做' });
|
|
expect(first.status).toBe(201);
|
|
expect(first.body.data.status).toBe('TECH_INITIAL');
|
|
|
|
const d3Token = await login('dir3d', 'Demo@123456');
|
|
const second = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${d3Token}`)
|
|
.send({ result: 'APPROVED', comment: '三维可做' });
|
|
expect(second.status).toBe(201);
|
|
expect(second.body.data.status).toBe('FETCH');
|
|
|
|
const biz = await login('bizstaff', 'Demo@123456');
|
|
const fetched = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/fetch`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ nasPath: '\\\\nas\\bids\\E2E-P1-001', openPlace: '交易中心三楼' });
|
|
expect(fetched.status).toBe(201);
|
|
expect(fetched.body.data.status).toBe('TECH_FINAL');
|
|
|
|
const final1 = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${softToken}`)
|
|
.send({ result: 'APPROVED' });
|
|
expect(final1.body.data.status).toBe('TECH_FINAL');
|
|
const final2 = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${d3Token}`)
|
|
.send({ result: 'APPROVED' });
|
|
expect(final2.status).toBe(201);
|
|
expect(final2.body.data.status).toBe('DRAFTING');
|
|
|
|
const drafting = await request(app.getHttpServer())
|
|
.get('/api/v1/bid-cases?bucket=drafting')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(drafting.body.data.items.some((i: { id: string }) => i.id === bidId)).toBe(true);
|
|
|
|
const rejectCreate = await request(app.getHttpServer())
|
|
.post('/api/v1/bid-cases')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({
|
|
name: '阶段1驳回演示',
|
|
externalNo: 'E2E-P1-002',
|
|
projectType: '软件',
|
|
source: '招标公告',
|
|
qualificationNeed: '无需资质',
|
|
tenderMethod: '公开招标',
|
|
reviewerIds: [dirsoft!.id],
|
|
});
|
|
const rejectId = rejectCreate.body.data.id as string;
|
|
const rejected = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${rejectId}/reviews`)
|
|
.set('Authorization', `Bearer ${softToken}`)
|
|
.send({ result: 'REJECTED', comment: '工期不够' });
|
|
expect(rejected.body.data.status).toBe('TERMINATED');
|
|
|
|
const employee = await login('employee', 'Demo@123456');
|
|
const hidden = await request(app.getHttpServer())
|
|
.get('/api/v1/bid-cases?bucket=terminated')
|
|
.set('Authorization', `Bearer ${employee}`);
|
|
expect(hidden.status).toBe(403);
|
|
|
|
const visible = await request(app.getHttpServer())
|
|
.get('/api/v1/bid-cases?bucket=terminated')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(visible.status).toBe(200);
|
|
expect(visible.body.data.items.some((i: { id: string }) => i.id === rejectId)).toBe(true);
|
|
}, 60000);
|
|
|
|
it('runs drafting to open result, and bounces rejected drafts back', async () => {
|
|
const owner = await login('owner', 'Demo@123456');
|
|
const bizdir = await login('bizdir', 'Demo@123456');
|
|
const biz = await login('bizstaff', 'Demo@123456');
|
|
const directors = await request(app.getHttpServer())
|
|
.get('/api/v1/directors')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
const dirsoft = (directors.body.data as { id: string; name: string }[]).find(
|
|
(d) => d.name === '吴软件',
|
|
);
|
|
expect(dirsoft).toBeTruthy();
|
|
const staff = await request(app.getHttpServer())
|
|
.get('/api/v1/staff')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
const bizUser = (staff.body.data as { id: string; username: string }[]).find(
|
|
(s) => s.username === 'bizstaff',
|
|
);
|
|
const employee = (staff.body.data as { id: string; username: string }[]).find(
|
|
(s) => s.username === 'employee',
|
|
);
|
|
expect(bizUser && employee).toBeTruthy();
|
|
|
|
const created = await request(app.getHttpServer())
|
|
.post('/api/v1/bid-cases')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({
|
|
name: '阶段2主链演示',
|
|
externalNo: 'E2E-P2-001',
|
|
projectType: '软件',
|
|
source: '招标公告',
|
|
qualificationNeed: '无需资质',
|
|
tenderMethod: '公开招标',
|
|
reviewerIds: [dirsoft!.id],
|
|
});
|
|
const bidId = created.body.data.id as string;
|
|
const softToken = await login('dirsoft', 'Demo@123456');
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${softToken}`)
|
|
.send({ result: 'APPROVED' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/fetch`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ nasPath: '\\\\nas\\p2' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${softToken}`)
|
|
.send({ result: 'APPROVED' });
|
|
|
|
const assigned = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/assign-drafter`)
|
|
.set('Authorization', `Bearer ${bizdir}`)
|
|
.send({ drafterId: bizUser!.id, review1Id: bizUser!.id });
|
|
expect(assigned.body.data.status).toBe('DRAFTING');
|
|
|
|
const submitted = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/submit-draft`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ nasPath: '\\\\nas\\p2\\v1', note: '初稿' });
|
|
expect(submitted.body.data.status).toBe('REVIEW1');
|
|
|
|
const bounced = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ result: 'REJECTED', comment: '封面格式不对' });
|
|
expect(bounced.body.data.status).toBe('DRAFTING');
|
|
|
|
const resubmit = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/submit-draft`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ nasPath: '\\\\nas\\p2\\v2', note: '已改封面' });
|
|
expect(resubmit.body.data.status).toBe('REVIEW1');
|
|
expect(resubmit.body.data.versions).toHaveLength(2);
|
|
|
|
const r1 = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ result: 'APPROVED' });
|
|
expect(r1.body.data.status).toBe('REVIEW2');
|
|
|
|
const r2 = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${bizdir}`)
|
|
.send({ result: 'APPROVED' });
|
|
expect(r2.body.data.status).toBe('PRINT');
|
|
|
|
const printed = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/print`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ amount: 800, note: '印标店' });
|
|
expect(printed.body.data.status).toBe('LIST');
|
|
expect(printed.body.data.expenses.length).toBeGreaterThan(0);
|
|
|
|
const completed = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/complete`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({
|
|
bidderIds: [employee!.id],
|
|
bondMethod: '电汇',
|
|
bondAmount: 50000,
|
|
quoteAmount: '1280000',
|
|
});
|
|
expect(completed.body.data.status).toBe('PENDING');
|
|
|
|
const empToken = await login('employee', 'Demo@123456');
|
|
const opened = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/open-result`)
|
|
.set('Authorization', `Bearer ${empToken}`)
|
|
.send({ result: 'LOST', costDisposition: 'MARKETING', comment: '第二名' });
|
|
expect(opened.status).toBe(201);
|
|
expect(opened.body.data.status).toBe('LOST');
|
|
expect(opened.body.data.costDisposition).toBe('MARKETING');
|
|
|
|
const resultList = await request(app.getHttpServer())
|
|
.get('/api/v1/bid-cases?bucket=result')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(resultList.body.data.items.some((i: { id: string }) => i.id === bidId)).toBe(true);
|
|
}, 60000);
|
|
|
|
it('converts a won bid to contract, rejects then approves into a project', async () => {
|
|
const owner = await login('owner', 'Demo@123456');
|
|
const bizdir = await login('bizdir', 'Demo@123456');
|
|
const biz = await login('bizstaff', 'Demo@123456');
|
|
const empToken = await login('employee', 'Demo@123456');
|
|
const directors = await request(app.getHttpServer())
|
|
.get('/api/v1/directors')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
const dirsoft = (directors.body.data as { id: string; name: string }[]).find(
|
|
(d) => d.name === '吴软件',
|
|
);
|
|
const staff = await request(app.getHttpServer())
|
|
.get('/api/v1/staff')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
const bizUser = (staff.body.data as { id: string; username: string }[]).find(
|
|
(s) => s.username === 'bizstaff',
|
|
);
|
|
const employee = (staff.body.data as { id: string; username: string }[]).find(
|
|
(s) => s.username === 'employee',
|
|
);
|
|
expect(dirsoft && bizUser && employee).toBeTruthy();
|
|
|
|
const created = await request(app.getHttpServer())
|
|
.post('/api/v1/bid-cases')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({
|
|
name: '阶段3主链演示',
|
|
externalNo: 'E2E-P3-001',
|
|
projectType: '软件',
|
|
source: '招标公告',
|
|
qualificationNeed: '无需资质',
|
|
tenderMethod: '公开招标',
|
|
reviewerIds: [dirsoft!.id],
|
|
});
|
|
const bidId = created.body.data.id as string;
|
|
const softToken = await login('dirsoft', 'Demo@123456');
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${softToken}`)
|
|
.send({ result: 'APPROVED' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/fetch`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ nasPath: '\\\\nas\\p3' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${softToken}`)
|
|
.send({ result: 'APPROVED' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/assign-drafter`)
|
|
.set('Authorization', `Bearer ${bizdir}`)
|
|
.send({ drafterId: bizUser!.id, review1Id: bizUser!.id });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/submit-draft`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ nasPath: '\\\\nas\\p3\\v1' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ result: 'APPROVED' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${bizdir}`)
|
|
.send({ result: 'APPROVED' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/print`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ amount: 800, note: '印标店' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/complete`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ bidderIds: [employee!.id], bondMethod: '电汇', quoteAmount: '1280000' });
|
|
|
|
const opened = await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/open-result`)
|
|
.set('Authorization', `Bearer ${empToken}`)
|
|
.send({ result: 'WON', comment: '第一名' });
|
|
expect(opened.status).toBe(201);
|
|
expect(opened.body.data.status).toBe('WON');
|
|
expect(opened.body.data.contracts).toHaveLength(1);
|
|
expect(opened.body.data.contracts[0].status).toBe('DRAFT');
|
|
expect(opened.body.data.contracts[0].projects).toHaveLength(1);
|
|
const contractId = opened.body.data.contracts[0].id as string;
|
|
|
|
const asBiz = await request(app.getHttpServer())
|
|
.get(`/api/v1/bid-cases/${bidId}`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
expect(asBiz.body.data.myActions.canConvertToContract).toBe(false);
|
|
|
|
const forbidden = await request(app.getHttpServer())
|
|
.post('/api/v1/contracts')
|
|
.set('Authorization', `Bearer ${empToken}`)
|
|
.send({ bidCaseId: bidId, amount: 1280000 });
|
|
expect(forbidden.status).toBe(403);
|
|
|
|
const drafted = await request(app.getHttpServer())
|
|
.post('/api/v1/contracts')
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ bidCaseId: bidId, amount: 1280000 });
|
|
expect(drafted.status).toBe(400);
|
|
|
|
await request(app.getHttpServer())
|
|
.patch(`/api/v1/contracts/${contractId}`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ contractNo: `HT-E2E-${bidId.slice(0, 8)}` });
|
|
|
|
const noTerms = await request(app.getHttpServer())
|
|
.post(`/api/v1/contracts/${contractId}/submit`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
expect(noTerms.status).toBe(400);
|
|
|
|
const miles = await request(app.getHttpServer())
|
|
.put(`/api/v1/contracts/${contractId}/milestones`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({
|
|
items: [
|
|
{ name: '预付款', ratio: 30 },
|
|
{ name: '验收款', ratio: 70 },
|
|
],
|
|
});
|
|
expect(miles.status).toBe(200);
|
|
|
|
const sealed = await request(app.getHttpServer())
|
|
.post(`/api/v1/contracts/${contractId}/seal`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ sealType: '合同章', reason: '签订中标合同' });
|
|
expect(sealed.status).toBe(201);
|
|
|
|
const submitted = await request(app.getHttpServer())
|
|
.post(`/api/v1/contracts/${contractId}/submit`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
expect(submitted.status).toBe(201);
|
|
expect(submitted.body.data.status).toBe('PENDING');
|
|
|
|
const rejected = await request(app.getHttpServer())
|
|
.post(`/api/v1/contracts/${contractId}/reviews`)
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ result: 'REJECTED', comment: '收款节点要改' });
|
|
expect(rejected.status).toBe(201);
|
|
expect(rejected.body.data.status).toBe('DRAFT');
|
|
expect(rejected.body.data.projects).toHaveLength(1);
|
|
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/contracts/${contractId}/submit`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
|
|
const ownerOk = await request(app.getHttpServer())
|
|
.post(`/api/v1/contracts/${contractId}/reviews`)
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ result: 'APPROVED' });
|
|
expect(ownerOk.body.data.status).toBe('PENDING');
|
|
expect(ownerOk.body.data.projects).toHaveLength(1);
|
|
|
|
const dirOk = await request(app.getHttpServer())
|
|
.post(`/api/v1/contracts/${contractId}/reviews`)
|
|
.set('Authorization', `Bearer ${bizdir}`)
|
|
.send({ result: 'APPROVED' });
|
|
expect(dirOk.status).toBe(201);
|
|
expect(dirOk.body.data.status).toBe('APPROVED');
|
|
expect(dirOk.body.data.projects).toHaveLength(1);
|
|
expect(dirOk.body.data.projects[0].projectNo).toMatch(/^PRJ-\d{4}-\d{5}$/);
|
|
expect(Number(dirOk.body.data.projects[0].presalesCost)).toBe(800);
|
|
expect(dirOk.body.data.sealRequests[0].status).toBe('APPROVED');
|
|
|
|
const bid = await request(app.getHttpServer())
|
|
.get(`/api/v1/bid-cases/${bidId}`)
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(bid.body.data.expenses.every((e: { projectId: string | null }) => e.projectId)).toBe(true);
|
|
|
|
const projects = await request(app.getHttpServer())
|
|
.get('/api/v1/projects')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(projects.body.data.items.some((p: { contractId: string }) => p.contractId === contractId)).toBe(
|
|
true,
|
|
);
|
|
}, 60000);
|
|
|
|
it('requires cost object, offsets loans, and flags overdue bonds', async () => {
|
|
const owner = await login('owner', 'Demo@123456');
|
|
const biz = await login('bizstaff', 'Demo@123456');
|
|
const finance = await login('finance', 'Demo@123456');
|
|
const emp = await login('employee', 'Demo@123456');
|
|
const directors = await request(app.getHttpServer())
|
|
.get('/api/v1/directors')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
const dirsoft = (directors.body.data as { id: string; name: string }[]).find((d) => d.name === '吴软件');
|
|
const created = await request(app.getHttpServer())
|
|
.post('/api/v1/bid-cases')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({
|
|
name: '阶段4差旅借款',
|
|
externalNo: 'E2E-P4-001',
|
|
projectType: '软件',
|
|
source: '招标公告',
|
|
qualificationNeed: '无需资质',
|
|
tenderMethod: '公开招标',
|
|
reviewerIds: [dirsoft!.id],
|
|
});
|
|
const bidId = created.body.data.id as string;
|
|
|
|
const badCost = await request(app.getHttpServer())
|
|
.post('/api/v1/expenses')
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ amount: 800, costType: 'OTHER' });
|
|
expect(badCost.status).toBe(400);
|
|
|
|
const noBid = await request(app.getHttpServer())
|
|
.post('/api/v1/expenses')
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ amount: 800, costType: 'BID' });
|
|
expect(noBid.status).toBe(400);
|
|
|
|
const loan = await request(app.getHttpServer())
|
|
.post('/api/v1/loans')
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ costType: 'BID', bidCaseId: bidId, amount: 2000, purpose: '取标差旅' });
|
|
expect(loan.status).toBe(201);
|
|
const loanId = loan.body.data.id as string;
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/loans/${loanId}/submit`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
const loanOk = await request(app.getHttpServer())
|
|
.post(`/api/v1/loans/${loanId}/reviews`)
|
|
.set('Authorization', `Bearer ${finance}`)
|
|
.send({ result: 'APPROVED' });
|
|
expect(loanOk.body.data.status).toBe('OPEN');
|
|
|
|
const dup = await request(app.getHttpServer())
|
|
.post('/api/v1/expenses')
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ costType: 'BID', bidCaseId: bidId, amount: 800 });
|
|
expect(dup.status).toBe(400);
|
|
|
|
const exp = await request(app.getHttpServer())
|
|
.post('/api/v1/expenses')
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ costType: 'BID', bidCaseId: bidId, amount: 800, loanId });
|
|
expect(exp.status).toBe(201);
|
|
const expId = exp.body.data.id as string;
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/expenses/${expId}/submit`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
const bounced = await request(app.getHttpServer())
|
|
.post(`/api/v1/expenses/${expId}/reviews`)
|
|
.set('Authorization', `Bearer ${finance}`)
|
|
.send({ result: 'REJECTED', comment: '发票不齐' });
|
|
expect(bounced.body.data.status).toBe('REJECTED');
|
|
const stillOpen = await request(app.getHttpServer())
|
|
.get(`/api/v1/loans/${loanId}`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
expect(stillOpen.body.data.status).toBe('OPEN');
|
|
|
|
const categorized = await request(app.getHttpServer())
|
|
.patch(`/api/v1/expenses/${expId}`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ costType: 'BID', bidCaseId: bidId, amount: 800, loanId, expenseCategory: '交通' });
|
|
expect(categorized.status).toBe(200);
|
|
expect(categorized.body.data.expenseCategory).toBe('交通');
|
|
|
|
const uploaded = await request(app.getHttpServer())
|
|
.post('/api/v1/files')
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.attach('file', Buffer.from('%PDF-1.4 invoice'), {
|
|
filename: 'fapiao.pdf',
|
|
contentType: 'application/pdf',
|
|
})
|
|
.field('bizType', 'EXPENSE')
|
|
.field('bizId', expId);
|
|
expect(uploaded.status).toBe(201);
|
|
expect(uploaded.body.data.fileName).toBe('fapiao.pdf');
|
|
|
|
const withFile = await request(app.getHttpServer())
|
|
.get(`/api/v1/expenses/${expId}`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
expect(withFile.body.data.files).toHaveLength(1);
|
|
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/expenses/${expId}/submit`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
const expOk = await request(app.getHttpServer())
|
|
.post(`/api/v1/expenses/${expId}/reviews`)
|
|
.set('Authorization', `Bearer ${finance}`)
|
|
.send({ result: 'APPROVED' });
|
|
expect(expOk.body.data.status).toBe('APPROVED');
|
|
const offset = await request(app.getHttpServer())
|
|
.get(`/api/v1/loans/${loanId}`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
expect(offset.body.data.status).toBe('OPEN');
|
|
expect(Number(offset.body.data.remaining)).toBe(1200);
|
|
|
|
const rest = await request(app.getHttpServer())
|
|
.post('/api/v1/expenses')
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ costType: 'BID', bidCaseId: bidId, amount: 1200, loanId });
|
|
const restId = rest.body.data.id as string;
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/expenses/${restId}/submit`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/expenses/${restId}/reviews`)
|
|
.set('Authorization', `Bearer ${finance}`)
|
|
.send({ result: 'APPROVED' });
|
|
const done = await request(app.getHttpServer())
|
|
.get(`/api/v1/loans/${loanId}`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
expect(done.body.data.status).toBe('OFFSET');
|
|
expect(Number(done.body.data.remaining)).toBe(0);
|
|
|
|
const noBond = await request(app.getHttpServer())
|
|
.post('/api/v1/bonds')
|
|
.set('Authorization', `Bearer ${finance}`)
|
|
.send({ bondType: 'TENDER', amount: 50000 });
|
|
expect(noBond.status).toBe(400);
|
|
|
|
const empBond = await request(app.getHttpServer())
|
|
.get('/api/v1/bonds')
|
|
.set('Authorization', `Bearer ${emp}`);
|
|
expect(empBond.status).toBe(403);
|
|
|
|
const bond = await request(app.getHttpServer())
|
|
.post('/api/v1/bonds')
|
|
.set('Authorization', `Bearer ${finance}`)
|
|
.send({ bondType: 'TENDER', amount: 50000, bidCaseId: bidId });
|
|
expect(bond.status).toBe(201);
|
|
const yesterday = new Date(Date.now() - 86400000).toISOString();
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bonds/${bond.body.data.id}/pay`)
|
|
.set('Authorization', `Bearer ${finance}`)
|
|
.send({ dueBackAt: yesterday });
|
|
const overdue = await request(app.getHttpServer())
|
|
.get('/api/v1/bonds?bucket=overdue')
|
|
.set('Authorization', `Bearer ${finance}`);
|
|
expect(overdue.body.data.items.some((b: { id: string }) => b.id === bond.body.data.id)).toBe(true);
|
|
}, 60000);
|
|
|
|
it('invoices a contract milestone then records receipt', async () => {
|
|
const owner = await login('owner', 'Demo@123456');
|
|
const finance = await login('finance', 'Demo@123456');
|
|
const list = await request(app.getHttpServer())
|
|
.get('/api/v1/contracts')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
const contract = (list.body.data.items as {
|
|
id: string;
|
|
status: string;
|
|
milestones: { id: string }[];
|
|
}[]).find((c) => c.status === 'APPROVED' && c.milestones?.length);
|
|
expect(contract).toBeTruthy();
|
|
const mileId = contract!.milestones[0].id;
|
|
const created = await request(app.getHttpServer())
|
|
.post('/api/v1/invoices')
|
|
.set('Authorization', `Bearer ${finance}`)
|
|
.send({ contractId: contract!.id, milestoneId: mileId });
|
|
expect(created.status).toBe(201);
|
|
expect(created.body.data.status).toBe('TO_ISSUE');
|
|
expect(Number(created.body.data.amount)).toBeGreaterThan(0);
|
|
const dup = await request(app.getHttpServer())
|
|
.post('/api/v1/invoices')
|
|
.set('Authorization', `Bearer ${finance}`)
|
|
.send({ contractId: contract!.id, milestoneId: mileId });
|
|
expect(dup.status).toBe(400);
|
|
const issued = await request(app.getHttpServer())
|
|
.post(`/api/v1/invoices/${created.body.data.id}/issue`)
|
|
.set('Authorization', `Bearer ${finance}`)
|
|
.send({ invoiceNo: 'INV-P4-001' });
|
|
expect(issued.body.data.status).toBe('ISSUED');
|
|
const received = await request(app.getHttpServer())
|
|
.post(`/api/v1/invoices/${created.body.data.id}/receive`)
|
|
.set('Authorization', `Bearer ${finance}`);
|
|
expect(received.body.data.status).toBe('RECEIVED');
|
|
}, 30000);
|
|
|
|
it('breaks down WBS, fills timesheets, blocks over-budget expenses, and invoices on acceptance', async () => {
|
|
const owner = await login('owner', 'Demo@123456');
|
|
const bizdir = await login('bizdir', 'Demo@123456');
|
|
const biz = await login('bizstaff', 'Demo@123456');
|
|
const empToken = await login('employee', 'Demo@123456');
|
|
const hr = await login('hr', 'Demo@123456');
|
|
const directors = await request(app.getHttpServer())
|
|
.get('/api/v1/directors')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
const dirsoft = (directors.body.data as { id: string; name: string }[]).find((d) => d.name === '吴软件');
|
|
const staff = await request(app.getHttpServer())
|
|
.get('/api/v1/staff')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
const bizUser = (staff.body.data as { id: string; username: string }[]).find((s) => s.username === 'bizstaff');
|
|
const employee = (staff.body.data as { id: string; username: string }[]).find((s) => s.username === 'employee');
|
|
expect(dirsoft && bizUser && employee).toBeTruthy();
|
|
|
|
const created = await request(app.getHttpServer())
|
|
.post('/api/v1/bid-cases')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({
|
|
name: '阶段5主链演示',
|
|
externalNo: 'E2E-P5-001',
|
|
projectType: '软件',
|
|
source: '招标公告',
|
|
qualificationNeed: '无需资质',
|
|
tenderMethod: '公开招标',
|
|
reviewerIds: [dirsoft!.id],
|
|
});
|
|
const bidId = created.body.data.id as string;
|
|
const softToken = await login('dirsoft', 'Demo@123456');
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${softToken}`)
|
|
.send({ result: 'APPROVED' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/fetch`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ nasPath: '\\\\nas\\p5' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${softToken}`)
|
|
.send({ result: 'APPROVED' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/assign-drafter`)
|
|
.set('Authorization', `Bearer ${bizdir}`)
|
|
.send({ drafterId: bizUser!.id, review1Id: bizUser!.id });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/submit-draft`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ nasPath: '\\\\nas\\p5\\v1' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ result: 'APPROVED' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/reviews`)
|
|
.set('Authorization', `Bearer ${bizdir}`)
|
|
.send({ result: 'APPROVED' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/print`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ amount: 600, note: '印标' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/complete`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ bidderIds: [employee!.id], bondMethod: '电汇', quoteAmount: '880000' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/bid-cases/${bidId}/open-result`)
|
|
.set('Authorization', `Bearer ${empToken}`)
|
|
.send({ result: 'WON' });
|
|
const opened = await request(app.getHttpServer())
|
|
.get(`/api/v1/bid-cases/${bidId}`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
const contractId = opened.body.data.contracts[0].id as string;
|
|
await request(app.getHttpServer())
|
|
.patch(`/api/v1/contracts/${contractId}`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ contractNo: `HT-E2E5-${bidId.slice(0, 8)}` });
|
|
await request(app.getHttpServer())
|
|
.put(`/api/v1/contracts/${contractId}/milestones`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({
|
|
items: [
|
|
{ name: '预付款', ratio: 30 },
|
|
{ name: '验收款', ratio: 70 },
|
|
],
|
|
});
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/contracts/${contractId}/seal`)
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ sealType: '合同章', reason: '阶段5合同' });
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/contracts/${contractId}/submit`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
await request(app.getHttpServer())
|
|
.post(`/api/v1/contracts/${contractId}/reviews`)
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ result: 'APPROVED' });
|
|
const approved = await request(app.getHttpServer())
|
|
.post(`/api/v1/contracts/${contractId}/reviews`)
|
|
.set('Authorization', `Bearer ${bizdir}`)
|
|
.send({ result: 'APPROVED' });
|
|
expect(approved.body.data.projects).toHaveLength(1);
|
|
const projectId = approved.body.data.projects[0].id as string;
|
|
|
|
const hidden = await request(app.getHttpServer())
|
|
.get('/api/v1/projects')
|
|
.set('Authorization', `Bearer ${hr}`);
|
|
expect(hidden.status).toBe(403);
|
|
|
|
const depts = await request(app.getHttpServer())
|
|
.get('/api/v1/departments')
|
|
.set('Authorization', `Bearer ${softToken}`);
|
|
const software = (depts.body.data as { id: string; code: string }[]).find((d) => d.code === 'TECH_SOFTWARE');
|
|
const d3 = (depts.body.data as { id: string; code: string }[]).find((d) => d.code === 'TECH_3D');
|
|
expect(software && d3).toBeTruthy();
|
|
|
|
const t1 = await request(app.getHttpServer())
|
|
.post(`/api/v1/projects/${projectId}/tasks`)
|
|
.set('Authorization', `Bearer ${softToken}`)
|
|
.send({ name: '软件开发', departmentId: software!.id, assigneeId: employee!.id });
|
|
expect(t1.status).toBe(201);
|
|
const t2 = await request(app.getHttpServer())
|
|
.post(`/api/v1/projects/${projectId}/tasks`)
|
|
.set('Authorization', `Bearer ${softToken}`)
|
|
.send({ name: '三维制作', departmentId: d3!.id });
|
|
expect(t2.status).toBe(201);
|
|
|
|
const tasks = await request(app.getHttpServer())
|
|
.get('/api/v1/project-tasks')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(tasks.body.data.filter((t: { projectId: string }) => t.projectId === projectId)).toHaveLength(2);
|
|
|
|
const budgets = await request(app.getHttpServer())
|
|
.put(`/api/v1/projects/${projectId}/budgets`)
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({
|
|
items: [
|
|
{ category: 'LABOR', amount: 1000 },
|
|
{ category: 'TRAVEL', amount: 500 },
|
|
{ category: 'PURCHASE', amount: 8000 },
|
|
],
|
|
});
|
|
expect(budgets.status).toBe(200);
|
|
expect(Number(budgets.body.data.hourRate)).toBe(200);
|
|
|
|
const sheet = await request(app.getHttpServer())
|
|
.post('/api/v1/timesheets')
|
|
.set('Authorization', `Bearer ${empToken}`)
|
|
.send({
|
|
projectId,
|
|
taskId: t1.body.data.id,
|
|
workDate: '2026-08-29',
|
|
hours: 2,
|
|
note: '联调',
|
|
});
|
|
expect(sheet.status).toBe(201);
|
|
expect(Number(sheet.body.data.costAmount)).toBe(400);
|
|
|
|
const overLabor = await request(app.getHttpServer())
|
|
.post('/api/v1/timesheets')
|
|
.set('Authorization', `Bearer ${empToken}`)
|
|
.send({ projectId, workDate: '2026-08-30', hours: 4 });
|
|
expect(overLabor.status).toBe(400);
|
|
|
|
const noCat = await request(app.getHttpServer())
|
|
.post('/api/v1/expenses')
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ costType: 'PROJECT', projectId, amount: 100 });
|
|
expect(noCat.status).toBe(400);
|
|
|
|
const travel = await request(app.getHttpServer())
|
|
.post('/api/v1/expenses')
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ costType: 'PROJECT', projectId, amount: 400, budgetCategory: 'TRAVEL', remark: '现场' });
|
|
expect(travel.status).toBe(201);
|
|
|
|
const overTravel = await request(app.getHttpServer())
|
|
.post('/api/v1/expenses')
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ costType: 'PROJECT', projectId, amount: 200, budgetCategory: 'TRAVEL' });
|
|
expect(overTravel.status).toBe(400);
|
|
|
|
const tooSoon = await request(app.getHttpServer())
|
|
.post(`/api/v1/projects/${projectId}/accept`)
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ kind: 'CUSTOMER', result: 'APPROVED' });
|
|
expect(tooSoon.status).toBe(400);
|
|
|
|
const internal = await request(app.getHttpServer())
|
|
.post(`/api/v1/projects/${projectId}/accept`)
|
|
.set('Authorization', `Bearer ${softToken}`)
|
|
.send({ kind: 'INTERNAL', result: 'APPROVED' });
|
|
expect(internal.status).toBe(201);
|
|
expect(internal.body.data.status).toBe('INTERNAL_ACCEPTED');
|
|
|
|
const customer = await request(app.getHttpServer())
|
|
.post(`/api/v1/projects/${projectId}/accept`)
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ kind: 'CUSTOMER', result: 'APPROVED' });
|
|
expect(customer.status).toBe(201);
|
|
expect(customer.body.data.status).toBe('ACCEPTED');
|
|
expect(customer.body.data.invoicesCreated).toBe(2);
|
|
expect(
|
|
customer.body.data.contract.milestones.every((m: { invoice: { status: string } | null }) => m.invoice?.status),
|
|
).toBe(true);
|
|
|
|
const empProjects = await request(app.getHttpServer())
|
|
.get('/api/v1/projects')
|
|
.set('Authorization', `Bearer ${empToken}`);
|
|
expect(empProjects.status).toBe(200);
|
|
expect(empProjects.body.data.items.some((p: { id: string }) => p.id === projectId)).toBe(true);
|
|
}, 60000);
|
|
|
|
it('warns expiring qualifications, overdue borrows and take-out seals, and lists calendar dates', async () => {
|
|
const owner = await login('owner', 'Demo@123456');
|
|
const biz = await login('bizstaff', 'Demo@123456');
|
|
const emp = await login('employee', 'Demo@123456');
|
|
const hr = await login('hr', 'Demo@123456');
|
|
|
|
const forbidden = await request(app.getHttpServer())
|
|
.get('/api/v1/qualifications')
|
|
.set('Authorization', `Bearer ${hr}`);
|
|
expect(forbidden.status).toBe(403);
|
|
|
|
const yesterday = new Date(Date.now() - 86400000).toISOString();
|
|
const expired = await request(app.getHttpServer())
|
|
.post('/api/v1/qualifications')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ name: '阶段6过期资质', code: 'QUAL_P6_OLD', expiresAt: yesterday, keeper: '行政' });
|
|
expect(expired.status).toBe(201);
|
|
expect(expired.body.data.bucket).toBe('expired');
|
|
|
|
const expiringList = await request(app.getHttpServer())
|
|
.get('/api/v1/qualifications?bucket=expired')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(expiringList.body.data.items.some((q: { id: string }) => q.id === expired.body.data.id)).toBe(true);
|
|
|
|
const staff = await request(app.getHttpServer())
|
|
.get('/api/v1/staff')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
const employee = (staff.body.data as { id: string; username: string }[]).find((s) => s.username === 'employee');
|
|
expect(employee).toBeTruthy();
|
|
|
|
const borrow = await request(app.getHttpServer())
|
|
.post('/api/v1/credential-borrows')
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({
|
|
itemName: '涉乙证书原件',
|
|
borrowerId: employee!.id,
|
|
dueAt: yesterday,
|
|
});
|
|
expect(borrow.status).toBe(201);
|
|
expect(borrow.body.data.bucket).toBe('overdue');
|
|
|
|
const overdueBorrows = await request(app.getHttpServer())
|
|
.get('/api/v1/credential-borrows?bucket=overdue')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(overdueBorrows.body.data.items.some((b: { id: string }) => b.id === borrow.body.data.id)).toBe(true);
|
|
|
|
const directors = await request(app.getHttpServer())
|
|
.get('/api/v1/directors')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
const dirsoft = (directors.body.data as { id: string; name: string }[]).find((d) => d.name === '吴软件');
|
|
const bid = await request(app.getHttpServer())
|
|
.post('/api/v1/bid-cases')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({
|
|
name: '阶段6开标日程',
|
|
externalNo: 'E2E-P6-001',
|
|
projectType: '软件',
|
|
source: '招标公告',
|
|
qualificationNeed: '无需资质',
|
|
tenderMethod: '公开招标',
|
|
openAt: new Date(Date.now() + 3 * 86400000).toISOString(),
|
|
openPlace: '交易中心',
|
|
reviewerIds: [dirsoft!.id],
|
|
});
|
|
expect(bid.status).toBe(201);
|
|
const bidId = bid.body.data.id as string;
|
|
|
|
const noDue = await request(app.getHttpServer())
|
|
.post('/api/v1/seal-requests')
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({ sealType: '公章', reason: '开标带章', bidCaseId: bidId, takeOut: true });
|
|
expect(noDue.status).toBe(400);
|
|
|
|
const seal = await request(app.getHttpServer())
|
|
.post('/api/v1/seal-requests')
|
|
.set('Authorization', `Bearer ${biz}`)
|
|
.send({
|
|
sealType: '公章',
|
|
reason: '开标带章',
|
|
bidCaseId: bidId,
|
|
takeOut: true,
|
|
returnAt: yesterday,
|
|
});
|
|
expect(seal.status).toBe(201);
|
|
expect(seal.body.data.status).toBe('PENDING');
|
|
const sealId = seal.body.data.id as string;
|
|
|
|
const approved = await request(app.getHttpServer())
|
|
.post(`/api/v1/seal-requests/${sealId}/reviews`)
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ result: 'APPROVED' });
|
|
expect(approved.status).toBe(201);
|
|
expect(approved.body.data.status).toBe('OUT');
|
|
expect(approved.body.data.bucket).toBe('overdue');
|
|
|
|
const overview = await request(app.getHttpServer())
|
|
.get('/api/v1/office/overview')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(overview.body.data.expiringQualifications).toBeGreaterThan(0);
|
|
expect(overview.body.data.overdueBorrows).toBeGreaterThan(0);
|
|
expect(overview.body.data.overdueSeals).toBeGreaterThan(0);
|
|
expect(overview.body.data.upcomingOpens).toBeGreaterThan(0);
|
|
|
|
const calendar = await request(app.getHttpServer())
|
|
.get('/api/v1/office/calendar')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(calendar.body.data.some((i: { kind: string }) => i.kind === 'BORROW' && i.overdue)).toBe(true);
|
|
expect(calendar.body.data.some((i: { kind: string }) => i.kind === 'SEAL' && i.overdue)).toBe(true);
|
|
expect(calendar.body.data.some((i: { kind: string }) => i.kind === 'BID_OPEN')).toBe(false);
|
|
|
|
const returnedBorrow = await request(app.getHttpServer())
|
|
.post(`/api/v1/credential-borrows/${borrow.body.data.id}/return`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
expect(returnedBorrow.body.data.bucket).toBe('returned');
|
|
|
|
const returnedSeal = await request(app.getHttpServer())
|
|
.post(`/api/v1/seal-requests/${sealId}/return`)
|
|
.set('Authorization', `Bearer ${biz}`);
|
|
expect(returnedSeal.body.data.status).toBe('RETURNED');
|
|
|
|
const empCal = await request(app.getHttpServer())
|
|
.get('/api/v1/office/calendar')
|
|
.set('Authorization', `Bearer ${emp}`);
|
|
expect(empCal.status).toBe(200);
|
|
expect(empCal.body.data.some((i: { kind: string }) => i.kind === 'BID_OPEN')).toBe(false);
|
|
}, 30000);
|
|
|
|
it('maintains hire-transfer-leave, imports attendance, payroll without housing fund, and lets directors change performance ratio', async () => {
|
|
const hr = await login('hr', 'Demo@123456');
|
|
const empToken = await login('employee', 'Demo@123456');
|
|
const soft = await login('dirsoft', 'Demo@123456');
|
|
const film = await login('dirfilm', 'Demo@123456');
|
|
const suffix = String(Date.now()).slice(-8);
|
|
const payDate = new Date(2026, 8, 1);
|
|
payDate.setMonth(payDate.getMonth() + (Number(suffix) % 30));
|
|
const payMonth = `${payDate.getFullYear()}-${String(payDate.getMonth() + 1).padStart(2, '0')}`;
|
|
|
|
const forbiddenDept = await request(app.getHttpServer())
|
|
.post('/api/v1/departments')
|
|
.set('Authorization', `Bearer ${empToken}`)
|
|
.send({ name: '不可建', code: `NOPE_${suffix}`, deptType: 'TECH' });
|
|
expect(forbiddenDept.status).toBe(403);
|
|
|
|
const forbiddenPay = await request(app.getHttpServer())
|
|
.get('/api/v1/payroll?yearMonth=2026-07')
|
|
.set('Authorization', `Bearer ${empToken}`);
|
|
expect(forbiddenPay.status).toBe(403);
|
|
|
|
const depts = await request(app.getHttpServer())
|
|
.get('/api/v1/departments')
|
|
.set('Authorization', `Bearer ${hr}`);
|
|
expect(Array.isArray(depts.body.data)).toBe(true);
|
|
const software = (depts.body.data as { id: string; code: string }[]).find((d) => d.code === 'TECH_SOFTWARE');
|
|
const filmDept = (depts.body.data as { id: string; code: string }[]).find((d) => d.code === 'TECH_FILM');
|
|
expect(software && filmDept).toBeTruthy();
|
|
|
|
const createdDept = await request(app.getHttpServer())
|
|
.post('/api/v1/departments')
|
|
.set('Authorization', `Bearer ${hr}`)
|
|
.send({
|
|
name: '阶段7演示组',
|
|
code: `TECH_P7_${suffix}`,
|
|
deptType: 'TECH',
|
|
parentId: software!.id,
|
|
categoryCode: 'CAT_SOFTWARE',
|
|
});
|
|
expect(createdDept.status).toBe(201);
|
|
expect(createdDept.body.data.code).toBe(`TECH_P7_${suffix}`);
|
|
|
|
const roster = await request(app.getHttpServer())
|
|
.get('/api/v1/employees')
|
|
.set('Authorization', `Bearer ${hr}`);
|
|
const dirEmp = (roster.body.data.items as { id: string; employeeNo: string }[]).find((e) => e.employeeNo === 'E023');
|
|
expect(dirEmp).toBeTruthy();
|
|
|
|
const hired = await request(app.getHttpServer())
|
|
.post('/api/v1/employees')
|
|
.set('Authorization', `Bearer ${hr}`)
|
|
.send({
|
|
name: '阶段7实习生',
|
|
employeeNo: `E7${suffix}`,
|
|
departmentId: software!.id,
|
|
title: '实习工程师',
|
|
restSchedule: 'SINGLE',
|
|
employmentStatus: 'INTERN',
|
|
hiredAt: '2026-07-16',
|
|
managerId: dirEmp!.id,
|
|
baseSalary: 10000,
|
|
perfBase: 3000,
|
|
socialBase: 10000,
|
|
});
|
|
expect(hired.status).toBe(201);
|
|
expect(hired.body.data.employmentStatus).toBe('INTERN');
|
|
const employeeId = hired.body.data.id as string;
|
|
|
|
const intern = await request(app.getHttpServer())
|
|
.post('/api/v1/employment-events')
|
|
.set('Authorization', `Bearer ${hr}`)
|
|
.send({ employeeId, kind: 'PROBATION', effectiveAt: '2026-07-20', comment: '转试用' });
|
|
expect(intern.status).toBe(201);
|
|
expect(intern.body.data.employmentStatus).toBe('PROBATION');
|
|
|
|
const regular = await request(app.getHttpServer())
|
|
.post('/api/v1/employment-events')
|
|
.set('Authorization', `Bearer ${hr}`)
|
|
.send({ employeeId, kind: 'REGULARIZE', effectiveAt: '2026-07-25', comment: '转正' });
|
|
expect(regular.status).toBe(201);
|
|
expect(regular.body.data.employmentStatus).toBe('REGULAR');
|
|
|
|
const att = await request(app.getHttpServer())
|
|
.post('/api/v1/attendance')
|
|
.set('Authorization', `Bearer ${hr}`)
|
|
.send({
|
|
employeeId,
|
|
yearMonth: payMonth,
|
|
workDays: 12,
|
|
leaveDays: 1,
|
|
overtimeHours: 8,
|
|
remark: '加班转调休',
|
|
});
|
|
expect(att.status).toBe(201);
|
|
expect(att.body.data.overtimeHours).toBe(8);
|
|
expect(att.body.data.compLeaveHours).toBe(8);
|
|
|
|
const run = await request(app.getHttpServer())
|
|
.post('/api/v1/payroll/run')
|
|
.set('Authorization', `Bearer ${hr}`)
|
|
.send({ yearMonth: payMonth });
|
|
expect(run.status).toBe(201);
|
|
const item = (
|
|
run.body.data.items as {
|
|
id: string;
|
|
employeeId: string;
|
|
housingFund: number;
|
|
tax: number;
|
|
socialPersonal: number;
|
|
leaveDeduct: number;
|
|
grossPay: number;
|
|
performancePay: number;
|
|
overtimeHours: number;
|
|
netPay: number;
|
|
}[]
|
|
).find((i) => i.employeeId === employeeId);
|
|
expect(item).toBeTruthy();
|
|
expect(item!.housingFund).toBe(0);
|
|
expect(item!.socialPersonal).toBeGreaterThan(0);
|
|
expect(item!.tax).toBeGreaterThan(0);
|
|
expect(item!.leaveDeduct).toBeGreaterThan(0);
|
|
expect(item!.overtimeHours).toBe(8);
|
|
expect(item!.grossPay).toBeGreaterThan(item!.netPay);
|
|
const beforePerf = item!.performancePay;
|
|
const beforeGross = item!.grossPay;
|
|
|
|
const otherDept = await request(app.getHttpServer())
|
|
.patch(`/api/v1/payroll-items/${item!.id}`)
|
|
.set('Authorization', `Bearer ${film}`)
|
|
.send({ performanceRatio: 50 });
|
|
expect(otherDept.status).toBe(403);
|
|
|
|
const adjusted = await request(app.getHttpServer())
|
|
.patch(`/api/v1/payroll-items/${item!.id}`)
|
|
.set('Authorization', `Bearer ${soft}`)
|
|
.send({ performanceRatio: 80 });
|
|
expect(adjusted.status).toBe(200);
|
|
expect(adjusted.body.data.performanceRatio).toBe(80);
|
|
expect(adjusted.body.data.performancePay).toBeLessThan(beforePerf);
|
|
expect(adjusted.body.data.grossPay).toBeLessThan(beforeGross);
|
|
expect(adjusted.body.data.housingFund).toBe(0);
|
|
|
|
const transferred = await request(app.getHttpServer())
|
|
.post('/api/v1/employment-events')
|
|
.set('Authorization', `Bearer ${hr}`)
|
|
.send({
|
|
employeeId,
|
|
kind: 'TRANSFER',
|
|
toDepartmentId: filmDept!.id,
|
|
effectiveAt: '2026-07-28',
|
|
comment: '支援摄制',
|
|
});
|
|
expect(transferred.status).toBe(201);
|
|
expect(transferred.body.data.departmentId).toBe(filmDept!.id);
|
|
|
|
const left = await request(app.getHttpServer())
|
|
.post('/api/v1/employment-events')
|
|
.set('Authorization', `Bearer ${hr}`)
|
|
.send({ employeeId, kind: 'LEAVE', effectiveAt: '2026-07-31', comment: '交接完成' });
|
|
expect(left.status).toBe(201);
|
|
expect(left.body.data.employmentStatus).toBe('LEFT');
|
|
|
|
const confirmed = await request(app.getHttpServer())
|
|
.post(`/api/v1/payroll/${run.body.data.run.id}/confirm`)
|
|
.set('Authorization', `Bearer ${hr}`);
|
|
expect(confirmed.status).toBe(201);
|
|
expect(confirmed.body.data.status).toBe('CONFIRMED');
|
|
|
|
const reopened = await request(app.getHttpServer())
|
|
.patch(`/api/v1/payroll-items/${item!.id}`)
|
|
.set('Authorization', `Bearer ${soft}`)
|
|
.send({ performanceRatio: 60 });
|
|
expect(reopened.status).toBe(200);
|
|
expect(reopened.body.data.performanceRatio).toBe(60);
|
|
const after = await request(app.getHttpServer())
|
|
.get('/api/v1/payroll?yearMonth=2026-07')
|
|
.set('Authorization', `Bearer ${hr}`);
|
|
expect(after.status).toBe(200);
|
|
expect(after.body.data.run.status).toBe('DRAFT');
|
|
}, 30000);
|
|
|
|
it('shows funnel and spend, writes logs, and fills remaining ledgers', async () => {
|
|
const fail = await request(app.getHttpServer())
|
|
.post('/api/v1/auth/login')
|
|
.send({ username: 'admin', password: 'wrong-password', agreeTerms: true });
|
|
expect(fail.status).toBe(401);
|
|
|
|
const employee = await login('employee', 'Demo@123456');
|
|
const denied = await request(app.getHttpServer())
|
|
.get('/api/v1/reports/bid-conversion')
|
|
.set('Authorization', `Bearer ${employee}`);
|
|
expect(denied.status).toBe(403);
|
|
|
|
const report = await request(app.getHttpServer())
|
|
.post('/api/v1/office/reports')
|
|
.set('Authorization', `Bearer ${employee}`)
|
|
.send({ periodType: 'DAY', date: '2026-08-29', content: '完成接口联调,明天对方案。', submit: true });
|
|
expect(report.status).toBe(201);
|
|
expect(report.body.data.periodType).toBe('DAY');
|
|
expect(report.body.data.content).toContain('接口联调');
|
|
expect(report.body.data.status).toBe('SENT');
|
|
expect(report.body.data.toUser.displayName).toBe('软件项目经理');
|
|
expect(report.body.data.ccUser.displayName).toBe('吴软件');
|
|
|
|
const viewed = await request(app.getHttpServer())
|
|
.get(`/api/v1/office/reports/${report.body.data.id}`)
|
|
.set('Authorization', `Bearer ${employee}`);
|
|
expect(viewed.status).toBe(200);
|
|
expect(viewed.body.data.content).toContain('明天对方案');
|
|
|
|
const owner = await login('owner', 'Demo@123456');
|
|
const funnel = await request(app.getHttpServer())
|
|
.get('/api/v1/reports/bid-conversion')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(funnel.status).toBe(200);
|
|
expect(funnel.body.data[0].status).toBe('筛选');
|
|
expect(funnel.body.data.some((r: { id: string; presalesSpend: number }) => r.id === 'won')).toBe(true);
|
|
expect(typeof funnel.body.data[0].presalesSpend).toBe('number');
|
|
|
|
const profit = await request(app.getHttpServer())
|
|
.get('/api/v1/reports/project-profit')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(profit.status).toBe(200);
|
|
|
|
const depts = await request(app.getHttpServer())
|
|
.get('/api/v1/departments')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(Array.isArray(depts.body.data)).toBe(true);
|
|
|
|
const admin = await login('admin', 'Admin@123456');
|
|
const partyName = `E2E招标人-${Date.now()}`;
|
|
const party = await request(app.getHttpServer())
|
|
.post('/api/v1/parties')
|
|
.set('Authorization', `Bearer ${admin}`)
|
|
.send({
|
|
name: partyName,
|
|
creditNo: '91320000MA00E2E01X',
|
|
level: 'A',
|
|
contacts: [{ name: '李工', phone: '13900001111', title: '招标办' }],
|
|
});
|
|
expect(party.status).toBe(201);
|
|
expect(party.body.data.creditNo).toBe('91320000MA00E2E01X');
|
|
expect(party.body.data.contacts[0].name).toBe('李工');
|
|
const partyId = party.body.data.id as string;
|
|
const partyPatched = await request(app.getHttpServer())
|
|
.patch(`/api/v1/parties/${partyId}`)
|
|
.set('Authorization', `Bearer ${admin}`)
|
|
.send({ name: partyName, creditNo: '91320000MA00E2E02X', level: 'B', contacts: party.body.data.contacts });
|
|
expect(partyPatched.body.data.creditNo).toBe('91320000MA00E2E02X');
|
|
const partyView = await request(app.getHttpServer())
|
|
.get(`/api/v1/parties/${partyId}`)
|
|
.set('Authorization', `Bearer ${admin}`);
|
|
expect(partyView.status).toBe(200);
|
|
expect(partyView.body.data.contacts).toHaveLength(1);
|
|
|
|
const logs = await request(app.getHttpServer())
|
|
.get('/api/v1/system/logs?pageSize=100')
|
|
.set('Authorization', `Bearer ${admin}`);
|
|
expect(logs.status).toBe(200);
|
|
const actions = (logs.body.data.items as { action: string }[]).map((i) => i.action);
|
|
expect(actions).toEqual(expect.arrayContaining(['LOGIN', 'LOGIN_FAIL', 'FORBIDDEN']));
|
|
|
|
const roles = await request(app.getHttpServer())
|
|
.get('/api/v1/system/roles')
|
|
.set('Authorization', `Bearer ${admin}`);
|
|
const employeeRole = (roles.body.data.items as { id: string; code: string }[]).find((r) => r.code === 'employee');
|
|
expect(employeeRole).toBeTruthy();
|
|
const scoped = await request(app.getHttpServer())
|
|
.patch(`/api/v1/system/roles/${employeeRole!.id}`)
|
|
.set('Authorization', `Bearer ${admin}`)
|
|
.send({ dataScope: 'DEPT' });
|
|
expect(scoped.status).toBe(200);
|
|
expect(scoped.body.data.dataScope).toBe('DEPT');
|
|
|
|
const rules = await request(app.getHttpServer())
|
|
.get('/api/v1/system/number-rules')
|
|
.set('Authorization', `Bearer ${admin}`);
|
|
expect(rules.status).toBe(200);
|
|
expect((rules.body.data as { code: string }[]).map((r) => r.code).sort()).toEqual(['BID', 'HT', 'PRJ']);
|
|
|
|
const board = await request(app.getHttpServer())
|
|
.get('/api/v1/resource-board')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
expect(board.status).toBe(200);
|
|
expect(board.body.data.items.length).toBeGreaterThan(0);
|
|
expect(board.body.data.items[0].load).toMatch(/idle|full|over/);
|
|
|
|
const seal = await request(app.getHttpServer())
|
|
.post('/api/v1/seals')
|
|
.set('Authorization', `Bearer ${admin}`)
|
|
.send({ name: '项目章', keeper: '行政' });
|
|
expect(seal.status).toBe(201);
|
|
expect(seal.body.data.name).toBe('项目章');
|
|
|
|
const projects = await request(app.getHttpServer())
|
|
.get('/api/v1/projects?pageSize=50')
|
|
.set('Authorization', `Bearer ${owner}`);
|
|
const projectId = (projects.body.data.items as { id: string }[])[0]?.id;
|
|
expect(projectId).toBeTruthy();
|
|
|
|
const purchase = await request(app.getHttpServer())
|
|
.post('/api/v1/purchase-requests')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ title: '渲染节点硬盘', amount: 80, projectId });
|
|
expect(purchase.status).toBe(201);
|
|
expect(purchase.body.data.status).toBe('PENDING');
|
|
|
|
const change = await request(app.getHttpServer())
|
|
.post('/api/v1/project-changes')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ projectId, summary: '增加交底次数', amountDelta: 0 });
|
|
expect(change.status).toBe(201);
|
|
expect(change.body.data.status).toBe('PENDING');
|
|
|
|
const camera = await request(app.getHttpServer())
|
|
.post('/api/v1/registered-assets')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ kind: 'FIXED', name: `摄像机 ${Date.now()}`, code: `CAM-${Date.now()}` });
|
|
expect(camera.status).toBe(201);
|
|
const asset = await request(app.getHttpServer())
|
|
.post('/api/v1/assets')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ registeredAssetId: camera.body.data.id, projectId });
|
|
expect(asset.status).toBe(201);
|
|
expect(asset.body.data.status).toBe('IN_USE');
|
|
expect(asset.body.data.registeredAssetId || asset.body.data.registeredAsset?.id).toBe(camera.body.data.id);
|
|
|
|
const finance = await login('finance', 'Demo@123456');
|
|
const payment = await request(app.getHttpServer())
|
|
.post('/api/v1/payments')
|
|
.set('Authorization', `Bearer ${finance}`)
|
|
.send({ kind: 'EXPENSE', amount: 88, remark: '阶段8演示打款' });
|
|
expect(payment.status).toBe(201);
|
|
expect(payment.body.data.status).toBe('TODO');
|
|
const paid = await request(app.getHttpServer())
|
|
.post(`/api/v1/payments/${payment.body.data.id}/pay`)
|
|
.set('Authorization', `Bearer ${finance}`);
|
|
expect(paid.status).toBe(201);
|
|
expect(paid.body.data.status).toBe('PAID');
|
|
}, 60000);
|
|
|
|
it('occupies registry assets and office applies land on purchase occupancy calendar', async () => {
|
|
const owner = await login('owner', 'Demo@123456');
|
|
const employee = await login('employee', 'Demo@123456');
|
|
const stamp = Date.now();
|
|
|
|
const nameless = await request(app.getHttpServer())
|
|
.post('/api/v1/assets')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ assetName: '手填器材' });
|
|
expect(nameless.status).toBe(400);
|
|
|
|
const laptop = await request(app.getHttpServer())
|
|
.post('/api/v1/registered-assets')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ kind: 'DIGITAL', name: `渲染账号 ${stamp}`, code: `DIG-${stamp}` });
|
|
expect(laptop.status).toBe(201);
|
|
|
|
const useApply = await request(app.getHttpServer())
|
|
.post('/api/v1/office/applies')
|
|
.set('Authorization', `Bearer ${employee}`)
|
|
.send({
|
|
kind: 'ASSET_USE',
|
|
assetKind: 'DIGITAL',
|
|
registeredAssetId: laptop.body.data.id,
|
|
reason: '项目渲染',
|
|
endAt: '2026-09-15',
|
|
});
|
|
expect(useApply.status).toBe(201);
|
|
expect(useApply.body.data.status).toBe('PENDING');
|
|
|
|
const useOk = await request(app.getHttpServer())
|
|
.post(`/api/v1/office/applies/${useApply.body.data.id}/decide`)
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ result: 'APPROVED' });
|
|
expect(useOk.status).toBe(201);
|
|
expect(useOk.body.data.status).toBe('APPROVED');
|
|
expect(useOk.body.data.occupancy?.status).toBe('IN_USE');
|
|
|
|
const buy = await request(app.getHttpServer())
|
|
.post('/api/v1/office/applies')
|
|
.set('Authorization', `Bearer ${employee}`)
|
|
.send({
|
|
kind: 'ASSET_BUY',
|
|
assetKind: 'FIXED',
|
|
title: '摄影灯套装',
|
|
amount: 1280,
|
|
reason: '补充灯光',
|
|
});
|
|
expect(buy.status).toBe(201);
|
|
const buyOk = await request(app.getHttpServer())
|
|
.post(`/api/v1/office/applies/${buy.body.data.id}/decide`)
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ result: 'APPROVED' });
|
|
expect(buyOk.status).toBe(201);
|
|
expect(buyOk.body.data.purchase?.status).toBe('APPROVED');
|
|
|
|
const car = await request(app.getHttpServer())
|
|
.post('/api/v1/registered-assets')
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ kind: 'FIXED', name: `商务车 ${stamp}`, code: `CAR-${stamp}` });
|
|
expect(car.status).toBe(201);
|
|
const vehicle = await request(app.getHttpServer())
|
|
.post('/api/v1/office/applies')
|
|
.set('Authorization', `Bearer ${employee}`)
|
|
.send({
|
|
kind: 'VEHICLE',
|
|
registeredAssetId: car.body.data.id,
|
|
destination: '南京政务中心',
|
|
startAt: '2026-09-01',
|
|
endAt: '2026-09-02',
|
|
reason: '送标',
|
|
});
|
|
expect(vehicle.status).toBe(201);
|
|
const vehicleOk = await request(app.getHttpServer())
|
|
.post(`/api/v1/office/applies/${vehicle.body.data.id}/decide`)
|
|
.set('Authorization', `Bearer ${owner}`)
|
|
.send({ result: 'APPROVED' });
|
|
expect(vehicleOk.status).toBe(201);
|
|
expect(vehicleOk.body.data.occupancy?.status).toBe('IN_USE');
|
|
|
|
const cal = await request(app.getHttpServer())
|
|
.get('/api/v1/office/calendar')
|
|
.set('Authorization', `Bearer ${employee}`);
|
|
expect(cal.status).toBe(200);
|
|
const kinds = (cal.body.data as { kind: string; title: string }[]).map((i) => i.kind);
|
|
expect(kinds).toEqual(expect.arrayContaining(['VEHICLE']));
|
|
}, 30000);
|
|
});
|