备份:修复前完整项目快照 2026-04-19

This commit is contained in:
root
2026-04-19 19:15:01 +08:00
parent 5266d7732b
commit d00f41a120
449 changed files with 89577 additions and 23251 deletions
@@ -0,0 +1,104 @@
const db = require('../db-sqlite');
describe('预支核销状态管理测试', () => {
let advanceId;
let verificationId;
beforeAll(async () => {
// 清空测试数据
await db.query('DELETE FROM verifications WHERE 1=1');
await db.query('DELETE FROM advances WHERE 1=1');
});
afterAll(async () => {
// 清理测试数据
await db.query('DELETE FROM verifications WHERE 1=1');
await db.query('DELETE FROM advances WHERE 1=1');
});
// 测试1: 创建预支单
test('创建预支单', async () => {
const result = await db.query(
'INSERT INTO advances (user_id, project_id, amount, currency, reason, advance_date, advance_code, status, applicant) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)',
[1, 1, 1000, 'CNY', '测试预支', '2026-03-25', 'ADV-TEST-001', 'pending', '隆林']
);
advanceId = result.lastID;
expect(advanceId).toBeDefined();
});
// 测试2: 审批预支单
test('审批预支单', async () => {
await db.query('UPDATE advances SET status = ? WHERE id = ?', ['approved', advanceId]);
const advance = await db.query('SELECT * FROM advances WHERE id = ?', [advanceId]);
expect(advance.rows[0].status).toBe('approved');
});
// 测试3: 执行预支单
test('执行预支单', async () => {
await db.query('UPDATE advances SET status = ? WHERE id = ?', ['executed', advanceId]);
const advance = await db.query('SELECT * FROM advances WHERE id = ?', [advanceId]);
expect(advance.rows[0].status).toBe('executed');
});
// 测试4: 创建部分核销单
test('创建部分核销单', async () => {
const result = await db.query(
'INSERT INTO verifications (advance_id, amount, currency, reason, verification_date, verification_code, status, applicant, advance_code, advance_amount, settlement) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[advanceId, 300, 'CNY', '部分核销', '2026-03-25', 'VER-TEST-001', 'pending', '隆林', 'ADV-TEST-001', 1000, 0]
);
verificationId = result.lastID;
expect(verificationId).toBeDefined();
});
// 测试5: 审批核销单
test('审批核销单', async () => {
await db.query('UPDATE verifications SET status = ? WHERE id = ?', ['approved', verificationId]);
const verification = await db.query('SELECT * FROM verifications WHERE id = ?', [verificationId]);
expect(verification.rows[0].status).toBe('approved');
});
// 测试6: 执行核销单
test('执行核销单', async () => {
await db.query('UPDATE verifications SET status = ? WHERE id = ?', ['executed', verificationId]);
// 更新预支单状态为部分核销
await db.query('UPDATE advances SET status = ? WHERE id = ?', ['partial_verification', advanceId]);
// 更新预支单已核销金额
await db.query('UPDATE advances SET total_reimbursed = ? WHERE id = ?', [300, advanceId]);
const verification = await db.query('SELECT * FROM verifications WHERE id = ?', [verificationId]);
expect(verification.rows[0].status).toBe('executed');
});
// 测试7: 检查预支单状态
test('检查预支单状态', async () => {
const advance = await db.query('SELECT * FROM advances WHERE id = ?', [advanceId]);
expect(advance.rows[0].total_reimbursed).toBe(300);
expect(advance.rows[0].status).toBe('partial_verification');
});
// 测试8: 创建结算核销单(退款)
test('创建结算核销单(退款)', async () => {
await db.query(
'INSERT INTO verifications (advance_id, amount, currency, reason, verification_date, verification_code, status, applicant, advance_code, advance_amount, settlement, settlement_amount) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[advanceId, 600, 'CNY', '结算核销(退款)', '2026-03-25', 'VER-TEST-002', 'executed', '隆林', 'ADV-TEST-001', 1000, 1, 100]
);
// 更新预支单状态为已完成
await db.query('UPDATE advances SET status = ? WHERE id = ?', ['completed', advanceId]);
// 更新预支单已核销金额
await db.query('UPDATE advances SET total_reimbursed = ? WHERE id = ?', [900, advanceId]);
});
// 测试9: 检查预支单是否已完结
test('检查预支单是否已完结', async () => {
const advance = await db.query('SELECT * FROM advances WHERE id = ?', [advanceId]);
expect(advance.rows[0].total_reimbursed).toBe(900);
expect(advance.rows[0].status).toBe('completed');
});
// 测试10: 获取预支核销状态列表
test('获取预支核销状态列表', async () => {
const result = await db.query('SELECT * FROM advances');
expect(Array.isArray(result.rows)).toBe(true);
expect(result.rows.length).toBeGreaterThan(0);
});
});
+321
View File
@@ -0,0 +1,321 @@
/**
* 分类管理API测试
* TDD: 先写测试,再实现功能
*/
const request = require('supertest');
const express = require('express');
// 模拟Express应用
const app = express();
app.use(express.json());
// 测试数据
const testCategories = [
{ name: '测试一级分类1', parent_id: null, level: 1 },
{ name: '测试一级分类2', parent_id: null, level: 1 },
];
describe('分类管理API测试', () => {
// 测试1: 获取分类树
describe('GET /api/categories/tree', () => {
test('应该返回分类树结构', async () => {
const response = await request(app)
.get('/api/categories/tree')
.expect(200);
expect(response.body.success).toBe(true);
expect(Array.isArray(response.body.data)).toBe(true);
// 每个一级分类应该有children属性
if (response.body.data.length > 0) {
expect(response.body.data[0]).toHaveProperty('children');
}
});
test('应该支持按层级筛选', async () => {
const response = await request(app)
.get('/api/categories/tree?level=1')
.expect(200);
expect(response.body.success).toBe(true);
// 返回的都是一级分类
response.body.data.forEach(cat => {
expect(cat.level).toBe(1);
});
});
});
// 测试2: 创建分类
describe('POST /api/categories', () => {
test('应该能创建一级分类', async () => {
const newCategory = {
name: '新材料分类',
parent_id: null,
level: 1,
description: '测试描述'
};
const response = await request(app)
.post('/api/categories')
.send(newCategory)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('id');
expect(response.body.data.name).toBe(newCategory.name);
});
test('应该能在指定父分类下创建二级分类', async () => {
// 先创建父分类
const parentResponse = await request(app)
.post('/api/categories')
.send({ name: '父分类', parent_id: null, level: 1 });
const parentId = parentResponse.body.data.id;
// 创建子分类
const childCategory = {
name: '子分类',
parent_id: parentId,
level: 2
};
const response = await request(app)
.post('/api/categories')
.send(childCategory)
.expect(200);
expect(response.body.success).toBe(true);
expect(response.body.data.parent_id).toBe(parentId);
expect(response.body.data.level).toBe(2);
});
test('分类名称不能为空', async () => {
const response = await request(app)
.post('/api/categories')
.send({ name: '', parent_id: null, level: 1 })
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.message).toContain('名称');
});
test('同一父分类下不能有重名子分类', async () => {
// 创建父分类
const parent = await request(app)
.post('/api/categories')
.send({ name: '唯一父分类', parent_id: null, level: 1 });
// 创建第一个子分类
await request(app)
.post('/api/categories')
.send({ name: '同名子分类', parent_id: parent.body.data.id, level: 2 });
// 尝试创建同名子分类
const response = await request(app)
.post('/api/categories')
.send({ name: '同名子分类', parent_id: parent.body.data.id, level: 2 })
.expect(400);
expect(response.body.success).toBe(false);
});
});
// 测试3: 更新分类
describe('PUT /api/categories/:id', () => {
test('应该能更新分类名称', async () => {
// 先创建分类
const createResponse = await request(app)
.post('/api/categories')
.send({ name: '原名称', parent_id: null, level: 1 });
const categoryId = createResponse.body.data.id;
// 更新分类
const response = await request(app)
.put(`/api/categories/${categoryId}`)
.send({ name: '新名称' })
.expect(200);
expect(response.body.success).toBe(true);
// 验证更新
const getResponse = await request(app)
.get(`/api/categories/${categoryId}`)
.expect(200);
expect(getResponse.body.data.name).toBe('新名称');
});
test('不能将分类设置为自己的子分类', async () => {
// 创建父分类
const parent = await request(app)
.post('/api/categories')
.send({ name: '父', parent_id: null, level: 1 });
// 创建子分类
const child = await request(app)
.post('/api/categories')
.send({ name: '子', parent_id: parent.body.data.id, level: 2 });
// 尝试将父分类的parent_id设为子分类(循环引用)
const response = await request(app)
.put(`/api/categories/${parent.body.data.id}`)
.send({ parent_id: child.body.data.id })
.expect(400);
expect(response.body.success).toBe(false);
});
});
// 测试4: 删除分类
describe('DELETE /api/categories/:id', () => {
test('应该能删除空分类', async () => {
// 创建分类
const createResponse = await request(app)
.post('/api/categories')
.send({ name: '待删除', parent_id: null, level: 1 });
const categoryId = createResponse.body.data.id;
// 删除分类
const response = await request(app)
.delete(`/api/categories/${categoryId}`)
.expect(200);
expect(response.body.success).toBe(true);
// 验证已删除
const getResponse = await request(app)
.get(`/api/categories/${categoryId}`)
.expect(404);
});
test('删除一级分类应该级联删除二级分类', async () => {
// 创建父分类
const parent = await request(app)
.post('/api/categories')
.send({ name: '父', parent_id: null, level: 1 });
// 创建子分类
const child = await request(app)
.post('/api/categories')
.send({ name: '子', parent_id: parent.body.data.id, level: 2 });
// 删除父分类
await request(app)
.delete(`/api/categories/${parent.body.data.id}`)
.expect(200);
// 验证子分类也被删除
await request(app)
.get(`/api/categories/${child.body.data.id}`)
.expect(404);
});
test('有商品的分类不应该被删除', async () => {
// 创建分类
const category = await request(app)
.post('/api/categories')
.send({ name: '有商品的分类', parent_id: null, level: 1 });
// 在该分类下创建商品(模拟)
// ... 创建商品逻辑
// 尝试删除分类
const response = await request(app)
.delete(`/api/categories/${category.body.data.id}`)
.expect(400);
expect(response.body.success).toBe(false);
expect(response.body.message).toContain('商品');
});
});
// 测试5: 批量导入时自动创建分类
describe('批量导入分类处理', () => {
test('导入时应该自动创建不存在的一级分类', async () => {
const importData = {
products: [
{
name: '测试商品',
category_level1: '新一级分类',
category_level2: '新二级分类'
}
]
};
const response = await request(app)
.post('/api/products/batch-import')
.send(importData)
.expect(200);
expect(response.body.success).toBe(true);
// 验证分类已创建
const categories = await request(app)
.get('/api/categories/tree')
.expect(200);
const level1Exists = categories.body.data.some(
cat => cat.name === '新一级分类'
);
expect(level1Exists).toBe(true);
});
test('导入时应该自动创建不存在的二级分类', async () => {
// 先创建一级分类
await request(app)
.post('/api/categories')
.send({ name: '已有的一级', parent_id: null, level: 1 });
// 导入带有新二级分类的商品
const importData = {
products: [
{
name: '测试商品2',
category_level1: '已有的一级',
category_level2: '新的二级'
}
]
};
await request(app)
.post('/api/products/batch-import')
.send(importData)
.expect(200);
// 验证二级分类已创建
const categories = await request(app)
.get('/api/categories/tree')
.expect(200);
const parent = categories.body.data.find(
cat => cat.name === '已有的一级'
);
expect(parent).toBeTruthy();
expect(parent.children).toBeDefined();
const level2Exists = parent.children.some(
child => child.name === '新的二级'
);
expect(level2Exists).toBe(true);
});
});
});
// 运行测试
if (require.main === module) {
const { execSync } = require('child_process');
try {
execSync('npx jest test-category-api.spec.js --verbose', {
cwd: __dirname,
stdio: 'inherit'
});
} catch (e) {
process.exit(1);
}
}
module.exports = { app };
+365
View File
@@ -0,0 +1,365 @@
/**
* 分类树结构测试
* TDD: 先写测试,再实现功能
*/
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
// 使用测试数据库
const TEST_DB_PATH = path.join(__dirname, '../test_company_finance.db');
// 测试数据库连接
function createTestDb() {
return new sqlite3.Database(TEST_DB_PATH);
}
// 初始化测试数据库
async function initTestDb(db) {
return new Promise((resolve, reject) => {
// 读取迁移脚本
const fs = require('fs');
const migrationScript = fs.readFileSync(
path.join(__dirname, '../migrations/001_create_category_tree.sql'),
'utf8'
);
db.exec(migrationScript, (err) => {
if (err) reject(err);
else resolve();
});
});
}
// 测试套件
describe('分类树结构测试', () => {
let db;
beforeAll(async () => {
db = createTestDb();
await initTestDb(db);
});
afterAll((done) => {
db.close(() => {
// 清理测试数据库
const fs = require('fs');
try {
fs.unlinkSync(TEST_DB_PATH);
} catch (e) {
// 忽略删除错误
}
done();
});
});
// 测试1: 分类表应该存在
test('category_tree 表应该存在', async () => {
const result = await new Promise((resolve, reject) => {
db.get(
"SELECT name FROM sqlite_master WHERE type='table' AND name='category_tree'",
(err, row) => {
if (err) reject(err);
else resolve(row);
}
);
});
expect(result).toBeTruthy();
expect(result.name).toBe('category_tree');
});
// 测试2: 应该能创建一级分类
test('应该能创建一级分类', async () => {
const result = await new Promise((resolve, reject) => {
db.run(
'INSERT INTO category_tree (name, parent_id, level, sort_order) VALUES (?, ?, ?, ?)',
['测试一级分类', null, 1, 1],
function(err) {
if (err) reject(err);
else resolve({ id: this.lastID });
}
);
});
expect(result.id).toBeGreaterThan(0);
});
// 测试3: 应该能在一级分类下创建二级分类
test('应该能创建二级分类', async () => {
// 先创建一级分类
const parentResult = await new Promise((resolve, reject) => {
db.run(
'INSERT INTO category_tree (name, parent_id, level, sort_order) VALUES (?, ?, ?, ?)',
['父分类', null, 1, 1],
function(err) {
if (err) reject(err);
else resolve({ id: this.lastID });
}
);
});
// 再创建二级分类
const childResult = await new Promise((resolve, reject) => {
db.run(
'INSERT INTO category_tree (name, parent_id, level, sort_order) VALUES (?, ?, ?, ?)',
['子分类', parentResult.id, 2, 1],
function(err) {
if (err) reject(err);
else resolve({ id: this.lastID });
}
);
});
expect(childResult.id).toBeGreaterThan(0);
// 验证父子关系
const childCategory = await new Promise((resolve, reject) => {
db.get(
'SELECT * FROM category_tree WHERE id = ?',
[childResult.id],
(err, row) => {
if (err) reject(err);
else resolve(row);
}
);
});
expect(childCategory.parent_id).toBe(parentResult.id);
expect(childCategory.level).toBe(2);
});
// 测试4: 应该能获取分类树
test('应该能获取完整的分类树', async () => {
const categories = await new Promise((resolve, reject) => {
db.all(
'SELECT * FROM category_tree ORDER BY level, sort_order',
(err, rows) => {
if (err) reject(err);
else resolve(rows);
}
);
});
expect(Array.isArray(categories)).toBe(true);
expect(categories.length).toBeGreaterThan(0);
// 验证有默认分类数据
const defaultCategories = categories.filter(c =>
['电杆横担', '电缆电线', '变压器'].includes(c.name)
);
expect(defaultCategories.length).toBeGreaterThan(0);
});
// 测试5: 删除一级分类应该级联删除二级分类
test('删除一级分类应该级联删除二级分类', async () => {
// 创建测试数据
const parent = await new Promise((resolve, reject) => {
db.run(
'INSERT INTO category_tree (name, parent_id, level) VALUES (?, ?, ?)',
['待删除父分类', null, 1],
function(err) {
if (err) reject(err);
else resolve({ id: this.lastID });
}
);
});
const child = await new Promise((resolve, reject) => {
db.run(
'INSERT INTO category_tree (name, parent_id, level) VALUES (?, ?, ?)',
['待删除子分类', parent.id, 2],
function(err) {
if (err) reject(err);
else resolve({ id: this.lastID });
}
);
});
// 删除父分类
await new Promise((resolve, reject) => {
db.run('DELETE FROM category_tree WHERE id = ?', [parent.id], function(err) {
if (err) reject(err);
else resolve();
});
});
// 验证子分类也被删除
const remainingChild = await new Promise((resolve, reject) => {
db.get('SELECT * FROM category_tree WHERE id = ?', [child.id], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
expect(remainingChild).toBeUndefined();
});
// 测试6: 应该能更新分类
test('应该能更新分类', async () => {
// 创建分类
const category = await new Promise((resolve, reject) => {
db.run(
'INSERT INTO category_tree (name, parent_id, level) VALUES (?, ?, ?)',
['原名称', null, 1],
function(err) {
if (err) reject(err);
else resolve({ id: this.lastID });
}
);
});
// 更新名称
await new Promise((resolve, reject) => {
db.run(
'UPDATE category_tree SET name = ? WHERE id = ?',
['新名称', category.id],
function(err) {
if (err) reject(err);
else resolve();
}
);
});
// 验证更新
const updated = await new Promise((resolve, reject) => {
db.get('SELECT * FROM category_tree WHERE id = ?', [category.id], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
expect(updated.name).toBe('新名称');
});
});
// 商品表测试
describe('商品表测试', () => {
let db;
beforeAll(async () => {
db = createTestDb();
await initTestDb(db);
});
afterAll((done) => {
db.close(done);
});
// 测试1: 商品表应该存在
test('products 表应该存在', async () => {
const result = await new Promise((resolve, reject) => {
db.get(
"SELECT name FROM sqlite_master WHERE type='table' AND name='products'",
(err, row) => {
if (err) reject(err);
else resolve(row);
}
);
});
expect(result).toBeTruthy();
expect(result.name).toBe('products');
});
// 测试2: 应该能创建商品
test('应该能创建商品', async () => {
// 先获取一个分类ID
const category = await new Promise((resolve, reject) => {
db.get('SELECT id FROM category_tree LIMIT 1', (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
const result = await new Promise((resolve, reject) => {
db.run(
`INSERT INTO products (
name, model, category_id, category_name, unit,
cost_price, price, brand, specification, source, remark
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
'测试商品', 'Model-001', category.id, '测试分类', '件',
100.00, 150.00, '测试品牌', '规格参数', '中国', '测试备注'
],
function(err) {
if (err) reject(err);
else resolve({ id: this.lastID });
}
);
});
expect(result.id).toBeGreaterThan(0);
});
// 测试3: 商品应该有默认值
test('商品字段应该有正确的默认值', async () => {
const category = await new Promise((resolve, reject) => {
db.get('SELECT id FROM category_tree LIMIT 1', (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
const result = await new Promise((resolve, reject) => {
db.run(
'INSERT INTO products (name, category_id) VALUES (?, ?)',
['最小化商品', category.id],
function(err) {
if (err) reject(err);
else resolve({ id: this.lastID });
}
);
});
const product = await new Promise((resolve, reject) => {
db.get('SELECT * FROM products WHERE id = ?', [result.id], (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
expect(product.unit).toBe('件');
expect(product.source).toBe('老挝');
expect(product.status).toBe('active');
expect(product.stock_quantity).toBe(0);
});
// 测试4: cost_price 可以为空
test('cost_price 应该可以为空', async () => {
const category = await new Promise((resolve, reject) => {
db.get('SELECT id FROM category_tree LIMIT 1', (err, row) => {
if (err) reject(err);
else resolve(row);
});
});
const result = await new Promise((resolve, reject) => {
db.run(
'INSERT INTO products (name, category_id, cost_price) VALUES (?, ?, ?)',
['无成本商品', category.id, null],
function(err) {
if (err) reject(err);
else resolve({ id: this.lastID });
}
);
});
expect(result.id).toBeGreaterThan(0);
});
});
// 运行测试
if (require.main === module) {
const { execSync } = require('child_process');
try {
execSync('npx jest test-category-tree.spec.js --verbose', {
cwd: __dirname,
stdio: 'inherit'
});
} catch (e) {
process.exit(1);
}
}
module.exports = { createTestDb, initTestDb };
+156
View File
@@ -0,0 +1,156 @@
const request = require('supertest');
const app = require('../final-backend');
describe('采购付款分离 - API层测试', () => {
describe('采购申请API', () => {
let testPurchaseRequestId;
test('POST /api/purchase-requests - 应该能创建采购申请', async () => {
const response = await request(app)
.post('/api/purchase-requests')
.send({
project_id: 1,
applicant: '测试申请人',
request_date: '2026-03-25',
expense_category: 'material',
total_amount: 15000,
items: [
{
product_name: '测试商品A',
quantity: 10,
unit_price: 1000,
total_price: 10000
},
{
product_name: '测试商品B',
quantity: 5,
unit_price: 1000,
total_price: 5000
}
]
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('id');
expect(response.body.data).toHaveProperty('request_code');
testPurchaseRequestId = response.body.data.id;
});
test('GET /api/purchase-requests - 应该能获取采购申请列表', async () => {
const response = await request(app).get('/api/purchase-requests');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(Array.isArray(response.body.data)).toBe(true);
});
test('GET /api/purchase-requests/:id - 应该能获取采购申请详情', async () => {
const response = await request(app).get(`/api/purchase-requests/${testPurchaseRequestId}`);
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('items');
expect(response.body.data.items.length).toBe(2);
});
test('POST /api/purchase-requests/:id/submit - 应该能提交采购申请', async () => {
const response = await request(app)
.post(`/api/purchase-requests/${testPurchaseRequestId}/submit`);
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
});
test('POST /api/purchase-requests/:id/approve - 应该能审批通过采购申请', async () => {
const response = await request(app)
.post(`/api/purchase-requests/${testPurchaseRequestId}/approve`);
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
});
test('PUT /api/purchase-requests/:id - 应该能更新采购申请', async () => {
const response = await request(app)
.put(`/api/purchase-requests/${testPurchaseRequestId}`)
.send({
project_id: 1,
applicant: '测试申请人',
request_date: '2026-03-25',
expense_category: 'material',
total_amount: 20000,
remark: '更新备注'
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
});
});
describe('库存管理API', () => {
test('GET /api/inventory - 应该能获取库存记录', async () => {
const response = await request(app).get('/api/inventory');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(Array.isArray(response.body.data)).toBe(true);
});
test('GET /api/inventory/summary - 应该能获取库存汇总', async () => {
const response = await request(app).get('/api/inventory/summary');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(Array.isArray(response.body.data)).toBe(true);
});
test('POST /api/inventory/out - 应该能创建出库记录', async () => {
const response = await request(app)
.post('/api/inventory/out')
.send({
project_id: 1,
product_id: 1,
quantity: 10,
operator: '测试操作员'
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
});
});
describe('项目成本统计API', () => {
test('GET /api/projects/:id/cost-summary - 应该能获取项目成本统计', async () => {
const response = await request(app).get('/api/projects/1/cost-summary');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('project_name');
expect(response.body.data).toHaveProperty('purchase_cost');
expect(response.body.data).toHaveProperty('payment_cost');
expect(response.body.data).toHaveProperty('total_cost');
expect(response.body.data).toHaveProperty('profit');
});
});
describe('采购申请删除API', () => {
let tempPurchaseRequestId;
beforeAll(async () => {
const response = await request(app)
.post('/api/purchase-requests')
.send({
project_id: 1,
applicant: '临时测试',
request_date: '2026-03-25',
expense_category: 'material',
total_amount: 1000
});
tempPurchaseRequestId = response.body.data.id;
});
test('DELETE /api/purchase-requests/:id - 应该能删除采购申请', async () => {
const response = await request(app)
.delete(`/api/purchase-requests/${tempPurchaseRequestId}`);
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
});
test('GET /api/purchase-requests/:id - 删除后应该返回404', async () => {
const response = await request(app)
.get(`/api/purchase-requests/${tempPurchaseRequestId}`);
expect(response.status).toBe(404);
});
});
});
@@ -0,0 +1,100 @@
const db = require('../db-sqlite');
const path = require('path');
describe('采购付款分离 - 数据库层测试', () => {
beforeAll(async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
});
describe('采购申请表 (purchase_requests)', () => {
test('应该能创建采购申请', async () => {
const result = await db.query(`
INSERT INTO purchase_requests
(request_code, project_id, applicant, request_date, expense_category, total_amount, status)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, ['PUR-TEST-001', 1, '测试申请人', '2026-03-25', 'material', 10000.00, 'pending']);
expect(result.changes).toBe(1);
});
test('采购申请编号应该唯一', async () => {
try {
await db.query(`
INSERT INTO purchase_requests
(request_code, project_id, applicant, request_date, expense_category, total_amount, status)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, ['PUR-TEST-001', 1, '测试申请人2', '2026-03-25', 'material', 20000.00, 'pending']);
fail('应该抛出唯一约束错误');
} catch (error) {
expect(error.message).toContain('UNIQUE constraint failed');
}
});
test('应该能查询采购申请', async () => {
const result = await db.query('SELECT * FROM purchase_requests WHERE request_code = ?', ['PUR-TEST-001']);
expect(result.length).toBeGreaterThan(0);
expect(result[0].request_code).toBe('PUR-TEST-001');
});
});
describe('采购明细表 (purchase_request_items)', () => {
let purchaseRequestId;
beforeAll(async () => {
const result = await db.query(`
INSERT INTO purchase_requests
(request_code, project_id, applicant, request_date, expense_category, total_amount, status)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, ['PUR-TEST-002', 1, '测试申请人', '2026-03-25', 'material', 5000.00, 'pending']);
purchaseRequestId = result.lastID;
});
test('应该能创建采购明细', async () => {
const result = await db.query(`
INSERT INTO purchase_request_items
(purchase_request_id, product_name, quantity, unit_price, total_price)
VALUES (?, ?, ?, ?, ?)
`, [purchaseRequestId, '测试商品', 10, 500.00, 5000.00]);
expect(result.changes).toBe(1);
});
test('应该能查询采购明细', async () => {
const result = await db.query('SELECT * FROM purchase_request_items WHERE purchase_request_id = ?', [purchaseRequestId]);
expect(result.length).toBeGreaterThan(0);
expect(result[0].product_name).toBe('测试商品');
});
});
describe('库存记录表 (inventory_records)', () => {
test('应该能创建库存入库记录', async () => {
const result = await db.query(`
INSERT INTO inventory_records
(record_type, product_id, quantity, unit_price, total_amount, record_date, operator)
VALUES (?, ?, ?, ?, ?, ?, ?)
`, ['in', 1, 100, 10.00, 1000.00, '2026-03-25', '测试操作员']);
expect(result.changes).toBe(1);
});
test('应该能创建库存出库记录', async () => {
const result = await db.query(`
INSERT INTO inventory_records
(record_type, project_id, product_id, quantity, unit_price, total_amount, record_date, operator)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`, ['out', 1, 1, 50, 10.00, 500.00, '2026-03-25', '测试操作员']);
expect(result.changes).toBe(1);
});
});
describe('付款申请表修改', () => {
test('付款申请表应该有purchase_request_id字段', async () => {
const result = await db.query("PRAGMA table_info(payment_requests)");
const hasPurchaseRequestId = result.some(col => col.name === 'purchase_request_id');
expect(hasPurchaseRequestId).toBe(true);
});
test('付款申请表应该有payment_type字段', async () => {
const result = await db.query("PRAGMA table_info(payment_requests)");
const hasPaymentType = result.some(col => col.name === 'payment_type');
expect(hasPaymentType).toBe(true);
});
});
});
@@ -0,0 +1,183 @@
const request = require('supertest');
const app = require('../final-backend');
const db = require('../db-sqlite');
describe('核销申请流程测试', () => {
let server;
let advanceId;
let verificationId1;
let verificationId2;
beforeAll(async () => {
server = app.listen(3006);
// 清理测试数据
await db.query('DELETE FROM verifications WHERE 1=1');
await db.query('DELETE FROM advances WHERE 1=1');
// 创建测试预支申请
const advanceResult = await db.query(
'INSERT INTO advances (user_id, project_id, amount, currency, reason, advance_date, advance_code, status, applicant, attachments) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[1, null, 10000, 'CNY', '测试预支', '2026-03-25', 'ADV-TEST', 'executed', '测试用户', '[]']
);
advanceId = advanceResult.lastID;
// 更新预支单状态为待核销
await db.query('UPDATE advances SET status = ? WHERE id = ?', ['pending_verification', advanceId]);
});
afterAll(async () => {
server.close();
});
describe('预支单状态管理', () => {
test('预支单初始状态应该是待核销', async () => {
const result = await db.query('SELECT status FROM advances WHERE id = ?', [advanceId]);
expect(result.rows[0].status).toBe('pending_verification');
});
});
describe('核销申请创建', () => {
test('创建第一张核销申请(非结算)', async () => {
const response = await request(server)
.post('/api/verifications')
.send({
verification_date: '2026-03-25',
advance_id: advanceId,
advance_code: 'ADV-TEST',
advance_amount: 10000,
currency: 'CNY',
reason: '测试核销1',
detail_items: [{ description: '测试费用1', amount: 6000, category: 'accommodation' }],
attachments: [],
applicant: '测试用户',
expense_type: 'company',
project_id: null,
settlement: false
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
verificationId1 = response.body.data.id;
// 检查预支单已核销金额是否更新
const advanceResult = await db.query('SELECT total_reimbursed FROM advances WHERE id = ?', [advanceId]);
expect(advanceResult.rows[0].total_reimbursed).toBe(6000);
});
test('创建第二张核销申请(结算)', async () => {
const response = await request(server)
.post('/api/verifications')
.send({
verification_date: '2026-03-25',
advance_id: advanceId,
advance_code: 'ADV-TEST',
advance_amount: 10000,
currency: 'CNY',
reason: '测试核销2',
detail_items: [{ description: '测试费用2', amount: 5000, category: 'food' }],
attachments: [],
applicant: '测试用户',
expense_type: 'company',
project_id: null,
settlement: true,
settlement_amount: -1000
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
verificationId2 = response.body.data.id;
// 检查预支单已核销金额是否更新
const advanceResult = await db.query('SELECT total_reimbursed FROM advances WHERE id = ?', [advanceId]);
expect(advanceResult.rows[0].total_reimbursed).toBe(11000);
});
});
describe('核销申请审批和执行', () => {
test('审批第一张核销申请', async () => {
const response = await request(server)
.post(`/api/verifications/${verificationId1}/approve`)
.send({ remark: '批准' });
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
});
test('执行第一张核销申请(非结算)', async () => {
const response = await request(server)
.post(`/api/verifications/${verificationId1}/execute`)
.send({ execute_method: '银行转账', voucher_no: 'VOUCHER-001' });
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
// 检查预支单状态应该变为部分核销
const advanceResult = await db.query('SELECT status FROM advances WHERE id = ?', [advanceId]);
expect(advanceResult.rows[0].status).toBe('partial_verification');
});
test('审批第二张核销申请', async () => {
const response = await request(server)
.post(`/api/verifications/${verificationId2}/approve`)
.send({ remark: '批准' });
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
});
test('执行第二张核销申请(结算)', async () => {
const response = await request(server)
.post(`/api/verifications/${verificationId2}/execute`)
.send({ execute_method: '银行转账', voucher_no: 'VOUCHER-002' });
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
// 检查预支单状态应该变为已完成
const advanceResult = await db.query('SELECT status FROM advances WHERE id = ?', [advanceId]);
expect(advanceResult.rows[0].status).toBe('completed');
});
});
describe('核销申请退回', () => {
test('创建测试核销申请', async () => {
const response = await request(server)
.post('/api/verifications')
.send({
verification_date: '2026-03-25',
advance_id: advanceId,
advance_code: 'ADV-TEST',
advance_amount: 10000,
currency: 'CNY',
reason: '测试退回',
detail_items: [{ description: '测试费用', amount: 2000, category: 'transportation' }],
attachments: [],
applicant: '测试用户',
expense_type: 'company',
project_id: null,
settlement: false
});
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
const testVerificationId = response.body.data.id;
// 检查预支单已核销金额是否更新
let advanceResult = await db.query('SELECT total_reimbursed FROM advances WHERE id = ?', [advanceId]);
expect(advanceResult.rows[0].total_reimbursed).toBe(13000);
// 退回核销申请
const rejectResponse = await request(server)
.post(`/api/verifications/${testVerificationId}/reject`)
.send({ remark: '退回' });
expect(rejectResponse.status).toBe(200);
expect(rejectResponse.body.success).toBe(true);
// 检查预支单已核销金额是否恢复
advanceResult = await db.query('SELECT total_reimbursed FROM advances WHERE id = ?', [advanceId]);
expect(advanceResult.rows[0].total_reimbursed).toBe(11000);
});
});
});
@@ -0,0 +1,97 @@
const request = require('supertest');
const app = require('../final-backend');
describe('质保金数据正确性测试 - TDD Red/Green', () => {
describe('项目详情API - 质保金数据', () => {
test('GET /api/projects/:id - 应该返回正确的质保金比例(从合同表读取)', async () => {
const response = await request(app).get('/api/projects/1');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('warranty_percent');
// 质保比例应该从 project_contracts 表的 warranty_deposit_percentage 字段读取
// 而不是硬编码为 5
const warrantyPercent = parseFloat(response.body.data.warranty_percent);
expect(warrantyPercent).toBeGreaterThan(0);
expect(warrantyPercent).toBeLessThanOrEqual(100);
});
test('GET /api/projects/:id - 质保金金额应该根据合同金额和比例正确计算', async () => {
const response = await request(app).get('/api/projects/1');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
const contractAmount = parseFloat(response.body.data.contract_amount || '0');
const warrantyPercent = parseFloat(response.body.data.warranty_percent || '0');
const warrantyAmount = parseFloat(response.body.data.warranty_amount || '0');
// 质保金金额 = 合同金额 * 质保比例 / 100
const expectedWarrantyAmount = Math.round(contractAmount * warrantyPercent / 100);
// 允许1元的四舍五入误差
expect(Math.abs(warrantyAmount - expectedWarrantyAmount)).toBeLessThanOrEqual(1);
});
test('GET /api/projects/:id - 质保期限应该从合同表读取', async () => {
const response = await request(app).get('/api/projects/1');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(response.body.data).toHaveProperty('warranty_months');
// 质保期限应该是数字且大于0
const warrantyMonths = parseInt(response.body.data.warranty_months);
expect(warrantyMonths).toBeGreaterThan(0);
});
});
describe('项目质保金列表API', () => {
test('GET /api/projects/:id/warranty-deposits - 应该返回质保金记录', async () => {
const response = await request(app).get('/api/projects/1/warranty-deposits');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
expect(Array.isArray(response.body.data)).toBe(true);
});
});
describe('质保金数据字段正确性测试', () => {
test('质保金数据应该包含所有必要字段', async () => {
const response = await request(app).get('/api/projects/1');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
const data = response.body.data;
// 检查所有质保金相关字段
expect(data).toHaveProperty('has_warranty');
expect(data).toHaveProperty('warranty_amount');
expect(data).toHaveProperty('warranty_percent');
expect(data).toHaveProperty('warranty_months');
expect(data).toHaveProperty('warranty_start_date');
expect(data).toHaveProperty('warranty_end_date');
expect(data).toHaveProperty('warranty_status');
});
test('质保比例和质保期限应该是不同的值', async () => {
const response = await request(app).get('/api/projects/1');
expect(response.status).toBe(200);
expect(response.body.success).toBe(true);
const warrantyPercent = parseFloat(response.body.data.warranty_percent);
const warrantyMonths = parseInt(response.body.data.warranty_months);
// 质保比例(百分比)和质保期限(月数)应该是不同的概念
// 比例通常是 0-100 之间的数,期限通常是 12、24 等月数
// 它们不应该相等(除非是极端情况,但测试中应该区分)
expect(typeof warrantyPercent).toBe('number');
expect(typeof warrantyMonths).toBe('number');
expect(warrantyPercent).not.toBeNaN();
expect(warrantyMonths).not.toBeNaN();
});
});
});