Files
Public/apps/api/src/bidding/bid-workflow.spec.ts
T
daiyongkang 76f266645d Initial commit: 风影 OA 全栈源码(API/Web/Flutter/IM)
含 Phase 1.1 IM seq 排序、断线重连、多端已读同步与微信式语音转文字 UI。
排除 node_modules、构建产物、安装包与 .env 密钥。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-02 10:04:03 +00:00

162 lines
7.1 KiB
TypeScript

import {
ASSIGN_ROLE,
canAccessBidModule,
canAssignFetcher,
canCreateBid,
canCreateSpecialBid,
canFetchBid,
canFetchThisBid,
canManageDraft,
canManageThisDraft,
canSeeAllBids,
canSeeTerminated,
canTerminateBid,
canTerminateFrom,
currentHandlerNames,
isCommercialStage,
isSpecialBid,
isSpecialCoordinatorUser,
stageForStatus,
previousBidStatus,
inferLegacyRestoreStatus,
} from './bid-workflow';
describe('bid-workflow', () => {
it('maps status to review stage', () => {
expect(stageForStatus('TECH_INITIAL')).toBe('TECH_INITIAL');
expect(stageForStatus('TECH_FINAL')).toBe('TECH_FINAL');
expect(stageForStatus('REVIEW1')).toBe('REVIEW1');
expect(stageForStatus('REVIEW2')).toBe('REVIEW2');
expect(stageForStatus('FETCH')).toBeNull();
expect(stageForStatus('DRAFTING')).toBeNull();
});
it('treats commercial reject as bounce, not terminate', () => {
expect(isCommercialStage('REVIEW1')).toBe(true);
expect(isCommercialStage('REVIEW2')).toBe(true);
expect(isCommercialStage('TECH_INITIAL')).toBe(false);
});
it('lets every director see all bids', () => {
expect(canSeeAllBids(['rd_director'])).toBe(true);
expect(canSeeAllBids(['3d_director'])).toBe(true);
expect(canSeeAllBids(['video_director'])).toBe(true);
expect(canSeeAllBids(['material_director'])).toBe(true);
expect(canSeeAllBids(['biz_director'])).toBe(true);
expect(canSeeAllBids(['tech_director'])).toBe(true);
expect(canSeeAllBids(['owner'])).toBe(true);
expect(canSeeAllBids(['admin'])).toBe(true);
expect(canSeeAllBids(['biz_staff'])).toBe(true);
expect(canSeeAllBids(['employee'])).toBe(false);
expect(canSeeAllBids(['pm'])).toBe(false);
});
it('hides terminated bids from ordinary employees', () => {
expect(canSeeTerminated(['employee'], ['office'])).toBe(false);
expect(canSeeTerminated(['hr'], ['office', 'hr'])).toBe(false);
expect(canSeeTerminated(['finance'], ['bid:result'])).toBe(false);
expect(canSeeTerminated(['biz_staff'], ['bid:screening'])).toBe(false);
expect(canSeeTerminated(['biz_director'], ['bid:terminated'])).toBe(true);
expect(canSeeTerminated(['tech_director'], ['bid'])).toBe(true);
expect(canSeeTerminated(['owner'], [])).toBe(true);
expect(canSeeTerminated(['admin'], [])).toBe(true);
});
it('restricts create / fetch / terminate by role', () => {
expect(canCreateBid(['owner'])).toBe(true);
expect(canCreateBid(['tech_director'])).toBe(false);
expect(canCreateBid(['employee'], ['bid:screening'])).toBe(true);
expect(canFetchBid(['biz_staff'])).toBe(true);
expect(canFetchBid(['tech_director'])).toBe(false);
expect(canTerminateBid(['biz_director'])).toBe(true);
expect(canTerminateBid(['biz_staff'])).toBe(false);
expect(canManageDraft(['biz_director'])).toBe(true);
expect(canManageDraft(['biz_staff'])).toBe(false);
});
it('keeps special screening on named coordinators', () => {
expect(isSpecialBid('SPECIAL')).toBe(true);
expect(isSpecialBid('FLOW')).toBe(false);
expect(canCreateSpecialBid(['owner'])).toBe(true);
expect(canCreateSpecialBid(['biz_director'])).toBe(false);
expect(isSpecialCoordinatorUser({ username: 'liuyunze', displayName: '刘运泽' })).toBe(true);
expect(isSpecialCoordinatorUser({ username: 'chencheng', displayName: '陈铖' })).toBe(true);
expect(isSpecialCoordinatorUser({ username: 'other', displayName: '其他人' })).toBe(false);
const coordinators = [{ userId: 'liu', role: ASSIGN_ROLE.COORDINATOR }];
const fetchers = [{ userId: 'staff', role: ASSIGN_ROLE.FETCHER }];
expect(canAssignFetcher(['3d_director'], 'SPECIAL', coordinators, 'liu', 'FETCH')).toBe(true);
expect(canAssignFetcher(['3d_director'], 'SPECIAL', coordinators, 'other', 'FETCH')).toBe(false);
expect(canAssignFetcher(['biz_director'], 'FLOW', [], 'x', 'FETCH')).toBe(true);
expect(canAssignFetcher(['biz_staff'], 'FLOW', [], 'x', 'FETCH')).toBe(false);
expect(canFetchThisBid(['employee'], 'SPECIAL', fetchers, 'staff')).toBe(true);
expect(canFetchThisBid(['biz_staff'], 'SPECIAL', fetchers, 'other')).toBe(false);
expect(canFetchThisBid(['biz_staff'], 'FLOW', [], 'other')).toBe(false);
expect(canFetchThisBid(['employee'], 'FLOW', fetchers, 'staff')).toBe(true);
expect(canManageThisDraft(['3d_director'], 'SPECIAL', coordinators, 'liu')).toBe(true);
expect(canManageThisDraft(['biz_director'], 'SPECIAL', coordinators, 'biz')).toBe(false);
expect(canManageThisDraft(['biz_director'], 'FLOW', [], 'biz')).toBe(true);
});
it('allows terminate until execution, not after open', () => {
expect(canTerminateFrom('TECH_INITIAL')).toBe(true);
expect(canTerminateFrom('DRAFTING')).toBe(true);
expect(canTerminateFrom('LIST')).toBe(true);
expect(canTerminateFrom('PENDING')).toBe(false);
expect(canTerminateFrom('WON')).toBe(false);
});
it('shows the person who should act on the current step', () => {
expect(
currentHandlerNames({
status: 'TECH_INITIAL',
reviewCycle: 1,
createdBy: { displayName: '潘兴辉' },
reviewers: [
{ stage: 'TECH_INITIAL', result: 'PENDING', cycle: 1, user: { displayName: '戴永康' } },
],
}),
).toBe('戴永康');
expect(
currentHandlerNames({
status: 'FETCH',
assignees: [{ role: 'FETCHER', user: { displayName: '何烨' } }],
createdBy: { displayName: '潘兴辉' },
}),
).toBe('何烨');
expect(
currentHandlerNames({
status: 'DRAFTING',
assignees: [{ role: 'DRAFTER', user: { displayName: '张婧妍' } }],
}),
).toBe('张婧妍');
expect(currentHandlerNames({ status: 'PRINT', createdBy: { displayName: '潘兴辉' } })).toBe('商务部');
expect(currentHandlerNames({ status: 'FETCH', createdBy: { displayName: '潘兴辉' } })).toBe('待派取标人');
});
it('rolls back one process step, except special fetch', () => {
expect(previousBidStatus('FETCH', 'FLOW')).toBe('TECH_INITIAL');
expect(previousBidStatus('FETCH', 'SPECIAL')).toBeNull();
expect(previousBidStatus('TECH_FINAL')).toBe('FETCH');
expect(previousBidStatus('DRAFTING')).toBe('TECH_FINAL');
expect(previousBidStatus('REVIEW1')).toBe('DRAFTING');
expect(previousBidStatus('REVIEW2')).toBe('REVIEW1');
expect(previousBidStatus('PRINT')).toBe('REVIEW2');
expect(previousBidStatus('LIST')).toBe('PRINT');
expect(previousBidStatus('PENDING')).toBe('LIST');
expect(previousBidStatus('TECH_INITIAL')).toBeNull();
expect(previousBidStatus('WON')).toBeNull();
});
it('infers the restore step for legacy terminated records without an action log', () => {
expect(inferLegacyRestoreStatus([], [])).toBe('DRAFTING');
expect(
inferLegacyRestoreStatus(
[{ stage: 'TECH_INITIAL', result: 'APPROVED' }],
[{ role: 'FETCHER' }, { role: 'DRAFTER' }],
),
).toBe('DRAFTING');
expect(inferLegacyRestoreStatus([], [{ role: 'BIDDER' }])).toBe('PENDING');
expect(inferLegacyRestoreStatus([{ stage: 'REVIEW2', result: 'PENDING' }], [])).toBe('REVIEW2');
});
});