feat(pricing): 开发定价方案

This commit is contained in:
R524809
2026-09-28 17:34:33 +08:00
parent a29dd92676
commit 84b230e69a
54 changed files with 6264 additions and 236 deletions
+294
View File
@@ -0,0 +1,294 @@
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],
),
);
});
});