Files
atlas-seller-kit/apps/api/src/pricing/commission-import-parser.ts
T

107 lines
3.7 KiB
TypeScript

import { z } from 'zod';
import type { CommissionImportCategory } from '@atlas/contracts';
import { ApiError } from '../common/api-error.js';
import { D } from './pricing-engine.js';
const sourceId = z
.union([
z.number().int().positive().max(Number.MAX_SAFE_INTEGER),
z.string().regex(/^[1-9]\d{0,17}$/),
])
.transform(String);
const leaf = z
.object({
id: z.union([z.string(), z.number()]),
label: z.string().max(200),
value: z.string().max(50),
})
.strict();
const category = z
.object({
id: z.union([z.string(), z.number()]),
label: z.string().trim().min(1).max(200),
value: z.string().max(200),
cate_id: sourceId,
children: z.array(leaf).length(3),
})
.strict();
const root = category
.omit({ children: true })
.extend({ children: z.array(category).min(1).max(2000) });
const documentSchema = z
.object({
code: z.literal(1),
msg: z.string(),
data: z.array(root).min(1).max(100),
})
.strict();
const fail = (message: string): never => {
throw new ApiError(422, 'COMMISSION_IMPORT_INVALID', message);
};
/** 仅在导入时解析来源文字;报价读取结构化区间,不解析展示标签。 */
export function parseCommissionDocument(
document: unknown,
): CommissionImportCategory[] {
if (Buffer.byteLength(JSON.stringify(document) ?? '') > 2 * 1024 * 1024)
fail('JSON 不能超过 2 MiB');
const parsed = documentSchema.safeParse(document);
if (!parsed.success)
return fail(
'JSON 格式不正确:应包含一级类目、二级类目和每类完整的三个价格档位',
);
const ids = new Set<string>();
const result: CommissionImportCategory[] = [];
for (const group of parsed.data.data) {
if (ids.has(group.cate_id)) fail(`类目 ID 重复:${group.cate_id}`);
ids.add(group.cate_id);
for (const item of group.children) {
if (ids.has(item.cate_id)) fail(`类目 ID 重复:${item.cate_id}`);
ids.add(item.cate_id);
const bands = item.children.map((tier, index) => {
const value = /^([123]),(\d{1,2}(?:\.\d{1,2})?)$/.exec(
tier.value,
);
const prefix = [
'售价 ≤ 1500₽',
'1500₽ < 售价 ≤ 5000₽',
'售价 > 5000₽',
][index];
const label = /^(.*?)\s*\((\d{1,2}(?:\.\d{1,2})?)%\)$/.exec(
tier.label,
);
if (
!value ||
!label ||
value[1] !== String(index + 1) ||
label[1].trim() !== prefix ||
!D(value[2]).eq(label[2])
)
return fail(
`${item.label}:档位或标签百分比与 value 不一致`,
);
return {
min_price: ['0', '1500', '5000'][index],
max_price: ['1500', '5000', null][index],
min_inclusive: false,
max_inclusive: index < 2,
commission_rate: D(value[2]).div(100).toFixed(6),
};
});
result.push({
source_category_id: item.cate_id,
parent_source_id: group.cate_id,
name: item.label,
parent_name: group.label,
bands,
status: 'eligible',
issues: [],
change: 'added',
mapped_types: 0,
});
}
}
if (result.length > 2000) fail('二级类目不能超过 2,000 个');
return result;
}