295 lines
11 KiB
TypeScript
295 lines
11 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { randomUUID } from 'node:crypto';
|
|
import type { AddressInfo } from 'node:net';
|
|
import { Pool } from 'pg';
|
|
import type { Database } from '@atlas/database';
|
|
import type { NestExpressApplication } from '@nestjs/platform-express';
|
|
import { afterAll, beforeAll, describe, it } from 'vitest';
|
|
import { DATABASE } from '../src/database/database.module.js';
|
|
import { createApplication } from '../src/main.js';
|
|
|
|
let app: NestExpressApplication;
|
|
let db: Database;
|
|
let migrator: Pool;
|
|
let base = '',
|
|
token = '',
|
|
user = '',
|
|
workspace = '',
|
|
card = '',
|
|
platform = '';
|
|
const request = async (
|
|
path: string,
|
|
method = 'GET',
|
|
body?: unknown,
|
|
key?: string,
|
|
) => {
|
|
const response = await fetch(`${base}/api${path}`, {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
...(key ? { 'Idempotency-Key': key } : {}),
|
|
},
|
|
body: body === undefined ? undefined : JSON.stringify(body),
|
|
});
|
|
return { status: response.status, body: (await response.json()) as any };
|
|
};
|
|
|
|
describe('系统物流费率审核', () => {
|
|
beforeAll(async () => {
|
|
Object.assign(process.env, {
|
|
NODE_ENV: 'test',
|
|
SWAGGER_ENABLED: 'false',
|
|
AUTH_REGISTER_IP_LIMIT: '100',
|
|
AUTH_REGISTER_EMAIL_LIMIT: '100',
|
|
});
|
|
app = (await createApplication()).app;
|
|
db = app.get(DATABASE);
|
|
migrator = new Pool({
|
|
connectionString: process.env.MIGRATOR_DATABASE_URL,
|
|
max: 1,
|
|
});
|
|
await app.listen(0, '127.0.0.1');
|
|
base = `http://127.0.0.1:${(app.getHttpServer().address() as AddressInfo).port}`;
|
|
const registered = await request('/auth/register', 'POST', {
|
|
email: `pricing-review-${randomUUID()}@example.test`,
|
|
password: 'pricing-review-test-password-123',
|
|
password_confirmation: 'pricing-review-test-password-123',
|
|
});
|
|
assert.equal(registered.status, 201, JSON.stringify(registered.body));
|
|
token = registered.body.data.access_token;
|
|
user = registered.body.data.user.id;
|
|
workspace = registered.body.data.workspace.id;
|
|
const scope = await db.query<{
|
|
platform_id: string;
|
|
provider_id: string;
|
|
}>(
|
|
`SELECT platform.id AS platform_id,provider.id AS provider_id FROM atlas.platforms platform CROSS JOIN atlas.logistics_providers provider WHERE upper(platform.code::text)='OZON' AND upper(provider.code::text)='CEL' LIMIT 1`,
|
|
);
|
|
platform = scope.rows[0].platform_id;
|
|
const provider = scope.rows[0].provider_id;
|
|
card = (
|
|
await db.query<{ id: string }>(
|
|
`INSERT INTO atlas.logistics_rate_cards(provider_id,platform_id,fulfillment,market_code,version,source_name,status,effective_from)
|
|
VALUES($1,$2,'rfbs','RU',$3,'自动化审核测试,未来生效','draft',now()) RETURNING id`,
|
|
[provider, platform, `qa-${randomUUID()}`],
|
|
)
|
|
).rows[0].id;
|
|
const methods: string[] = [];
|
|
for (const code of ['express', 'standard', 'economy']) {
|
|
methods.push(
|
|
(
|
|
await db.query<{ id: string }>(
|
|
`INSERT INTO atlas.logistics_transport_methods(rate_card_id,code,name) VALUES($1,$2::text,$2::text) RETURNING id`,
|
|
[card, code],
|
|
)
|
|
).rows[0].id,
|
|
);
|
|
}
|
|
for (const [index, code] of [
|
|
'ExtraSmall',
|
|
'Budget',
|
|
'Small',
|
|
'Big',
|
|
'PremiumSmall',
|
|
'PremiumBig',
|
|
].entries()) {
|
|
const type = (
|
|
await db.query<{ id: string }>(
|
|
`INSERT INTO atlas.logistics_product_types(rate_card_id,code,name,merchandise_currency_code,min_merchandise_value,max_merchandise_value,min_actual_weight_kg,max_actual_weight_kg,billing_basis)
|
|
VALUES($1,$2::text,$2::text,'CNY',0,10000,0.001,30,'actual_only') RETURNING id`,
|
|
[card, code],
|
|
)
|
|
).rows[0].id;
|
|
for (const method of methods.slice(0, [3, 3, 3, 2, 3, 2][index]))
|
|
await db.query(
|
|
`INSERT INTO atlas.logistics_rate_rules(product_type_id,transport_method_id,fixed_fee_cny,per_billable_kg_fee_cny,status)
|
|
VALUES($1,$2,3.12,10,'draft')`,
|
|
[type, method],
|
|
);
|
|
}
|
|
});
|
|
|
|
afterAll(async () => {
|
|
if (migrator) {
|
|
if (card)
|
|
await migrator.query(
|
|
`DELETE FROM atlas.logistics_rate_card_reviews WHERE rate_card_id=$1`,
|
|
[card],
|
|
);
|
|
if (user)
|
|
await migrator.query(
|
|
`DELETE FROM atlas.system_admin_grants WHERE user_id=$1`,
|
|
[user],
|
|
);
|
|
}
|
|
if (db) {
|
|
if (card)
|
|
await db.query(
|
|
`DELETE FROM atlas.logistics_rate_cards WHERE id=$1`,
|
|
[card],
|
|
);
|
|
if (user) {
|
|
await db.query(
|
|
`DELETE FROM atlas.workspace_memberships WHERE user_id=$1`,
|
|
[user],
|
|
);
|
|
await db.query(`DELETE FROM atlas.workspaces WHERE id=$1`, [
|
|
workspace,
|
|
]);
|
|
await db.query(`DELETE FROM atlas.users WHERE id=$1`, [user]);
|
|
}
|
|
}
|
|
if (migrator) await migrator.end();
|
|
if (app) await app.close();
|
|
});
|
|
|
|
it('仅系统管理员可读取和启用;六类账单复算、重放与过期版本保护', async () => {
|
|
assert.equal(
|
|
(
|
|
await request(
|
|
`/system/pricing/rate-cards?platform_id=${platform}`,
|
|
)
|
|
).status,
|
|
403,
|
|
);
|
|
await migrator.query(
|
|
`INSERT INTO atlas.system_admin_grants(user_id) VALUES($1)`,
|
|
[user],
|
|
);
|
|
const listed = await request(
|
|
`/system/pricing/rate-cards?platform_id=${platform}`,
|
|
);
|
|
assert.equal(listed.status, 200, JSON.stringify(listed.body));
|
|
const item = listed.body.data.find((row: any) => row.id === card);
|
|
assert.equal(item.status, 'draft');
|
|
assert.equal(item.types.length, 6);
|
|
assert.equal(item.rules.length, 16);
|
|
const input = {
|
|
rules_hash: item.rules_hash,
|
|
source_reference: 'CEL 真实账单 QA 批次-2026-09',
|
|
review_note:
|
|
'逐类型核对实际包装、货值、计费重量与物流账单费用;本测试版本仅在未来生效。',
|
|
effective_from: new Date(Date.now() + 86400000).toISOString(),
|
|
samples: item.types.map((type: any, index: number) => ({
|
|
rule_id: item.rules.find(
|
|
(rule: any) => rule.product_type_id === type.id,
|
|
).id,
|
|
invoice_reference: `QA-INVOICE-${index + 1}`,
|
|
merchandise_value_cny: '100',
|
|
shipping_weight_g: '1000',
|
|
length_mm: '100',
|
|
width_mm: '100',
|
|
height_mm: '100',
|
|
billed_logistics_cny: '13.12',
|
|
})),
|
|
};
|
|
const key = randomUUID();
|
|
const mismatch = structuredClone(input);
|
|
mismatch.samples[0].billed_logistics_cny = '12.00';
|
|
const rejected = await request(
|
|
`/system/pricing/rate-cards/${card}/reviews`,
|
|
'POST',
|
|
mismatch,
|
|
randomUUID(),
|
|
);
|
|
assert.equal(rejected.status, 422, JSON.stringify(rejected.body));
|
|
const repeatedInvoice = structuredClone(input);
|
|
repeatedInvoice.samples[1].invoice_reference =
|
|
repeatedInvoice.samples[0].invoice_reference;
|
|
assert.equal(
|
|
(
|
|
await request(
|
|
`/system/pricing/rate-cards/${card}/reviews`,
|
|
'POST',
|
|
repeatedInvoice,
|
|
randomUUID(),
|
|
)
|
|
).status,
|
|
422,
|
|
);
|
|
assert.equal(
|
|
(await request(`/system/pricing/rate-cards/${card}`)).body.data
|
|
.status,
|
|
'draft',
|
|
);
|
|
const [accepted, concurrentReplay] = await Promise.all([
|
|
request(
|
|
`/system/pricing/rate-cards/${card}/reviews`,
|
|
'POST',
|
|
input,
|
|
key,
|
|
),
|
|
request(
|
|
`/system/pricing/rate-cards/${card}/reviews`,
|
|
'POST',
|
|
input,
|
|
key,
|
|
),
|
|
]);
|
|
assert.equal(accepted.status, 201, JSON.stringify(accepted.body));
|
|
assert.equal(
|
|
concurrentReplay.status,
|
|
201,
|
|
JSON.stringify(concurrentReplay.body),
|
|
);
|
|
assert.equal(accepted.body.data.status, 'active');
|
|
assert.equal(accepted.body.data.review.samples.length, 6);
|
|
const replay = await request(
|
|
`/system/pricing/rate-cards/${card}/reviews`,
|
|
'POST',
|
|
input,
|
|
key,
|
|
);
|
|
assert.equal(replay.status, 201, JSON.stringify(replay.body));
|
|
assert.equal(
|
|
(
|
|
await request(
|
|
`/system/pricing/rate-cards/${card}/reviews`,
|
|
'POST',
|
|
mismatch,
|
|
key,
|
|
)
|
|
).status,
|
|
409,
|
|
);
|
|
assert.equal(
|
|
(
|
|
await request(
|
|
`/system/pricing/rate-cards/${card}/reviews`,
|
|
'POST',
|
|
input,
|
|
randomUUID(),
|
|
)
|
|
).status,
|
|
409,
|
|
);
|
|
const activeRuleCount = await db.query<{ count: string }>(
|
|
`SELECT count(*) FROM atlas.logistics_rate_rules r JOIN atlas.logistics_product_types t ON t.id=r.product_type_id WHERE t.rate_card_id=$1 AND r.status='active'`,
|
|
[card],
|
|
);
|
|
assert.equal(Number(activeRuleCount.rows[0].count), 16);
|
|
await assert.rejects(() =>
|
|
db.query(
|
|
`UPDATE atlas.logistics_rate_rules SET fixed_fee_cny=4 WHERE id=$1`,
|
|
[item.rules[0].id],
|
|
),
|
|
);
|
|
await assert.rejects(() =>
|
|
db.query(
|
|
`UPDATE atlas.logistics_rate_cards SET source_name='错误改写' WHERE id=$1`,
|
|
[card],
|
|
),
|
|
);
|
|
const draftCard = await db.query<{ id: string }>(
|
|
`SELECT id FROM atlas.logistics_rate_cards WHERE version='cel-rfbs-review-2026-09' LIMIT 1`,
|
|
);
|
|
await assert.rejects(() =>
|
|
db.query(
|
|
`UPDATE atlas.logistics_product_types SET rate_card_id=$1 WHERE id=$2`,
|
|
[draftCard.rows[0].id, item.types[0].id],
|
|
),
|
|
);
|
|
});
|
|
});
|