This commit is contained in:
2026-08-07 14:47:11 +08:00
parent e7cc0cf18b
commit 7ac173fac4
21 changed files with 1714 additions and 791 deletions
@@ -1,110 +0,0 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
get: vi.fn(),
post: vi.fn(),
delete: vi.fn(),
}))
vi.mock('@/shared/api/http', () => ({
http: mocks,
unwrapApiResponse: <T>(response: { data: T }) => response.data,
}))
import {
createPlatformCarrierRateVersion,
getPlatformCarrierRate,
listPlatformCarrierRates,
retirePlatformCarrierRate,
} from './platformCarrierRates'
const payload = {
id: '9007199254740995',
tenantId: '0',
rateCode: 'RATE-US-WEST',
rateVersion: 2,
originWarehouseId: '20001',
originWarehouseName: '义乌收货仓',
businessType: 'FBA_SEA',
channelCode: 'US-WEST-SEA',
carrierId: '30001',
carrierName: '示例船司',
destinationCode: 'ONT8',
chargeUnit: 'KG' as const,
unitPrice: 12.5,
currency: 'CNY',
additionalFee: 80,
effectiveFrom: '2026-08-01T00:00:00',
effectiveTo: null,
status: 'ACTIVE' as const,
remark: null,
createdBy: '10',
createdAt: '2026-08-01T10:00:00',
updatedBy: null,
updatedAt: null,
version: 1,
}
describe('platformCarrierRates api', () => {
beforeEach(() => vi.clearAllMocks())
it('queries the platform-wide carrier-rate path and keeps every long id as a string', async () => {
mocks.get.mockResolvedValue({ data: [payload] })
const result = await listPlatformCarrierRates({ rateCode: ' RATE-US ' })
expect(mocks.get).toHaveBeenCalledWith('/platform/v1/fba-pricing/carrier-rates', {
params: { rateCode: 'RATE-US' },
})
expect(result[0]).toMatchObject({
id: '9007199254740995',
tenantId: '0',
originWarehouseId: '20001',
carrierId: '30001',
})
})
it('omits the tenant parameter for the platform-wide list query', async () => {
mocks.get.mockResolvedValue({ data: [payload] })
await listPlatformCarrierRates()
await listPlatformCarrierRates({ rateCode: ' ' })
expect(mocks.get).toHaveBeenNthCalledWith(1, '/platform/v1/fba-pricing/carrier-rates', { params: {} })
expect(mocks.get).toHaveBeenNthCalledWith(2, '/platform/v1/fba-pricing/carrier-rates', { params: {} })
})
it('loads a platform-wide version detail without tenant scope', async () => {
mocks.get.mockResolvedValue({ data: payload })
await getPlatformCarrierRate('9001')
expect(mocks.get).toHaveBeenCalledWith('/platform/v1/fba-pricing/carrier-rates/9001')
})
it('creates immutable versions and retires an active version', async () => {
mocks.post.mockResolvedValue({ data: payload })
mocks.delete.mockResolvedValue({ data: null })
const request = {
rateCode: 'RATE-US-WEST',
originWarehouseId: '20001',
originWarehouseName: '义乌收货仓',
businessType: 'FBA_SEA',
channelCode: 'US-WEST-SEA',
carrierId: '30001',
carrierName: '示例船司',
destinationCode: 'ONT8',
chargeUnit: 'KG' as const,
unitPrice: 12.5,
currency: 'CNY',
additionalFee: 80,
effectiveFrom: '2026-08-01T00:00:00',
effectiveTo: null,
remark: null,
}
await createPlatformCarrierRateVersion(request)
await retirePlatformCarrierRate('9001')
expect(mocks.post).toHaveBeenCalledWith('/platform/v1/fba-pricing/carrier-rates/versions', request)
expect(mocks.delete).toHaveBeenCalledWith('/platform/v1/fba-pricing/carrier-rates/9001')
})
})
@@ -1,117 +0,0 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export type CarrierRateChargeUnit = 'KG' | 'CBM'
export type CarrierRateStatus = 'ACTIVE' | 'INACTIVE'
export interface PlatformCarrierRateResource {
id: string
tenantId: string
rateCode: string
rateVersion: number
originWarehouseId: string
originWarehouseName: string
businessType: string
channelCode: string
carrierId: string
carrierName: string
destinationCode: string
chargeUnit: CarrierRateChargeUnit
unitPrice: number
currency: string
additionalFee: number
effectiveFrom: string
effectiveTo: string | null
status: CarrierRateStatus
remark: string | null
createdBy: string | null
createdAt: string | null
updatedBy: string | null
updatedAt: string | null
version: number | null
}
interface PlatformCarrierRatePayload extends Omit<PlatformCarrierRateResource,
'id' | 'tenantId' | 'originWarehouseId' | 'carrierId' | 'createdBy' | 'updatedBy'> {
id: string | number
tenantId: string | number
originWarehouseId: string | number
carrierId: string | number
createdBy?: string | number | null
updatedBy?: string | number | null
}
export interface CreatePlatformCarrierRateVersionRequest {
rateCode: string
originWarehouseId: string
originWarehouseName: string
businessType: string
channelCode: string
carrierId: string
carrierName: string
destinationCode: string
chargeUnit: CarrierRateChargeUnit
unitPrice: number
currency: string
additionalFee: number
effectiveFrom: string
effectiveTo: string | null
remark: string | null
}
export interface PlatformCarrierRateQuery {
rateCode?: string
}
const basePath = '/platform/v1/fba-pricing/carrier-rates'
function nullableId(value: string | number | null | undefined) {
return value == null ? null : String(value)
}
function normalizeCarrierRate(value: PlatformCarrierRatePayload): PlatformCarrierRateResource {
return {
...value,
id: String(value.id),
tenantId: String(value.tenantId),
originWarehouseId: String(value.originWarehouseId),
carrierId: String(value.carrierId),
createdBy: nullableId(value.createdBy),
updatedBy: nullableId(value.updatedBy),
effectiveTo: value.effectiveTo ?? null,
remark: value.remark ?? null,
createdAt: value.createdAt ?? null,
updatedAt: value.updatedAt ?? null,
version: value.version ?? null,
}
}
export async function listPlatformCarrierRates(query: PlatformCarrierRateQuery = {}) {
const rateCode = query.rateCode?.trim()
const response = await http.get<ApiEnvelope<PlatformCarrierRatePayload[]>, ApiEnvelope<PlatformCarrierRatePayload[]>>(
basePath,
{ params: rateCode ? { rateCode } : {} },
)
return unwrapApiResponse(response).map(normalizeCarrierRate)
}
export async function getPlatformCarrierRate(id: string) {
const response = await http.get<ApiEnvelope<PlatformCarrierRatePayload>, ApiEnvelope<PlatformCarrierRatePayload>>(
`${basePath}/${id}`,
)
return normalizeCarrierRate(unwrapApiResponse(response))
}
export async function createPlatformCarrierRateVersion(
payload: CreatePlatformCarrierRateVersionRequest,
) {
const response = await http.post<ApiEnvelope<PlatformCarrierRatePayload>, ApiEnvelope<PlatformCarrierRatePayload>>(
`${basePath}/versions`,
payload,
)
return normalizeCarrierRate(unwrapApiResponse(response))
}
export async function retirePlatformCarrierRate(id: string) {
const response = await http.delete<ApiEnvelope<null>, ApiEnvelope<null>>(`${basePath}/${id}`)
return unwrapApiResponse(response)
}
@@ -0,0 +1,151 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
const http = vi.hoisted(() => ({
get: vi.fn(),
post: vi.fn(),
}))
vi.mock('@/shared/api/http', () => ({
http,
unwrapApiResponse: <T>(response: { data: T }) => response.data,
}))
import {
downloadPlatformPriceBookSource,
getPlatformPriceBook,
listPlatformPriceBooks,
publishPlatformPriceBook,
uploadPlatformPriceBook,
} from './platformPriceBooks'
const version = {
id: '9007199254740995',
versionNo: '2',
originalFileName: '2026-08-price-book.xlsx',
sourceFileId: '9007199254740996',
sourceSha256: 'a'.repeat(64),
status: 'PENDING_CONFIRMATION' as const,
channelCount: '15',
rateCount: '42',
blockingErrorCount: '2',
warningCount: 1,
parseIssuesJson: null,
createdBy: '10001',
createdAt: '2026-08-07T10:00:00',
publishedBy: null,
publishedAt: null,
supersededAt: null,
}
const detail = {
version,
channels: [{
channelCode: 'MASON_TRUCK',
channelName: '美森正班海卡',
priceModel: 'FBA_WAREHOUSE_RATE' as const,
sourceSheetName: '美森正班海卡',
estimatedDepartureDate: '2026-08-12',
estimatedDepartureText: '8月12日',
latestWarehouseEntry: null,
vesselName: null,
voyageNo: null,
referenceTransit: null,
surchargeDescription: null,
remark: null,
}],
rates: [{
id: '9007199254740997',
channelCode: 'MASON_TRUCK',
priceModel: 'FBA_WAREHOUSE_RATE' as const,
originWarehouseId: null,
originWarehouseCode: 'CN-YIWU-01',
originWarehouseName: '义乌仓',
destinationType: 'OVERSEAS' as const,
destinationCode: 'US-LAX-01',
destinationWarehouseId: null,
destinationName: '洛杉矶仓',
regionCode: null,
pricingTaxMode: 'TAX_INCLUDED' as const,
chargeUnit: 'KG' as const,
weightFromKg: null,
unitPrice: '12.5',
currency: 'CNY',
sourceSheetName: '美森正班海卡',
sourceRowNumber: '8',
sourceDestinationText: '洛杉矶仓',
remark: null,
}],
issues: [{
severity: 'BLOCKING',
issueCode: 'ORIGIN_MAPPING_REQUIRED',
message: '起运仓需要映射到平台国内仓',
sourceSheetName: '美森正班海卡',
sourceRowNumber: '8',
sourceCell: null,
}],
}
describe('platform price-book API', () => {
beforeEach(() => vi.clearAllMocks())
it('uses the platform price-book list endpoint without customer scope and preserves long ids', async () => {
http.get.mockResolvedValue({ data: [version] })
const result = await listPlatformPriceBooks({ status: ' PENDING_CONFIRMATION ' })
expect(http.get).toHaveBeenCalledWith('/platform/v1/fba-pricing/carrier-rates/price-books', {
params: { status: 'PENDING_CONFIRMATION' },
})
expect(result[0]).toMatchObject({
id: '9007199254740995',
sourceFileId: '9007199254740996',
createdBy: '10001',
versionNo: 2,
rateCount: 42,
})
})
it('omits an empty status filter for the complete version history', async () => {
http.get.mockResolvedValue({ data: [] })
await listPlatformPriceBooks({ status: ' ' })
expect(http.get).toHaveBeenCalledWith('/platform/v1/fba-pricing/carrier-rates/price-books', { params: {} })
})
it('uploads one workbook, loads its parsed detail, and normalizes parsed rows', async () => {
http.post.mockResolvedValue({ data: detail })
http.get.mockResolvedValue({ data: detail })
const file = new File(['book'], 'price-book.xlsx', { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' })
const uploadResult = await uploadPlatformPriceBook(file)
const detailResult = await getPlatformPriceBook('9007199254740995')
expect(http.post).toHaveBeenCalledWith('/platform/v1/fba-pricing/carrier-rates/price-books', expect.any(FormData))
const form = http.post.mock.calls[0][1] as FormData
expect(form.get('file')).toBe(file)
expect(http.get).toHaveBeenCalledWith('/platform/v1/fba-pricing/carrier-rates/price-books/9007199254740995')
expect(uploadResult.rates[0]).toMatchObject({ id: '9007199254740997', unitPrice: 12.5, sourceRowNumber: 8 })
expect(detailResult.issues[0].sourceRowNumber).toBe(8)
})
it('publishes only confirmed warehouse mappings and downloads the original workbook', async () => {
http.post.mockResolvedValue({ data: detail })
const request = {
originMappings: [{ originWarehouseCode: 'CN-YIWU-01', warehouseId: '20001' }],
overseasMappings: [{ sourceDestinationName: '洛杉矶仓', warehouseId: '20011' }],
}
await publishPlatformPriceBook('9007199254740995', request)
await downloadPlatformPriceBookSource('9007199254740995')
expect(http.post).toHaveBeenCalledWith(
'/platform/v1/fba-pricing/carrier-rates/price-books/9007199254740995/publish',
request,
)
expect(http.get).toHaveBeenCalledWith(
'/platform/v1/fba-pricing/carrier-rates/price-books/9007199254740995/source',
{ responseType: 'blob' },
)
})
})
@@ -0,0 +1,269 @@
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export type PriceBookStatus = 'PENDING_CONFIRMATION' | 'ACTIVE' | 'SUPERSEDED' | 'FAILED'
export type PriceModel = 'FBA_WAREHOUSE_RATE' | 'REGION_WEIGHT_TIER'
export type PricingTaxMode = 'TAX_INCLUDED' | 'TAX_EXCLUDED'
export type PricingDestinationType = 'FBA' | 'OVERSEAS'
export type PriceBookChargeUnit = 'KG' | 'CBM'
type ApiId = string | number
type ApiNumber = string | number
export interface PlatformPriceBookVersion {
id: string
versionNo: number
originalFileName: string
sourceFileId: string | null
sourceSha256: string | null
status: PriceBookStatus
channelCount: number
rateCount: number
blockingErrorCount: number
warningCount: number
parseIssuesJson: string | null
createdBy: string | null
createdAt: string | null
publishedBy: string | null
publishedAt: string | null
supersededAt: string | null
}
/** Read-only channel data parsed from the uploaded workbook. */
export interface PlatformPriceBookParsedChannel {
channelCode: string
channelName: string
priceModel: PriceModel
sourceSheetName: string
estimatedDepartureDate: string | null
estimatedDepartureText: string | null
latestWarehouseEntry: string | null
vesselName: string | null
voyageNo: string | null
referenceTransit: string | null
surchargeDescription: string | null
remark: string | null
}
/** Read-only price row parsed from the uploaded workbook. */
export interface PlatformPriceBookParsedRate {
id: string | null
channelCode: string
priceModel: PriceModel
originWarehouseId: string | null
originWarehouseCode: string
originWarehouseName: string
destinationType: PricingDestinationType | null
destinationCode: string | null
destinationWarehouseId: string | null
destinationName: string | null
regionCode: string | null
pricingTaxMode: PricingTaxMode | null
chargeUnit: PriceBookChargeUnit
weightFromKg: number | null
unitPrice: number
currency: string
sourceSheetName: string
sourceRowNumber: number
sourceDestinationText: string | null
remark: string | null
}
export interface PlatformPriceBookParseIssue {
severity: 'BLOCKING' | 'WARNING' | string
issueCode: string
message: string
sourceSheetName: string | null
sourceRowNumber: number | null
sourceCell: string | null
}
export interface PlatformPriceBookDetail {
version: PlatformPriceBookVersion
channels: PlatformPriceBookParsedChannel[]
rates: PlatformPriceBookParsedRate[]
issues: PlatformPriceBookParseIssue[]
}
export interface PriceBookConfirmationRequest {
originMappings: Array<{
originWarehouseCode: string
warehouseId: string
}>
overseasMappings: Array<{
sourceDestinationName: string
warehouseId: string
}>
}
export interface PlatformPriceBookQuery {
status?: PriceBookStatus | string
}
interface PriceBookVersionPayload extends Omit<PlatformPriceBookVersion,
'id' | 'versionNo' | 'sourceFileId' | 'channelCount' | 'rateCount' | 'blockingErrorCount' | 'warningCount' | 'createdBy' | 'publishedBy'> {
id: ApiId
versionNo: ApiNumber
sourceFileId?: ApiId | null
channelCount?: ApiNumber | null
rateCount?: ApiNumber | null
blockingErrorCount?: ApiNumber | null
warningCount?: ApiNumber | null
createdBy?: ApiId | null
publishedBy?: ApiId | null
}
interface PriceBookParsedChannelPayload extends Omit<PlatformPriceBookParsedChannel,
'estimatedDepartureDate' | 'estimatedDepartureText' | 'latestWarehouseEntry' | 'vesselName' | 'voyageNo'
| 'referenceTransit' | 'surchargeDescription' | 'remark'> {
estimatedDepartureDate?: string | null
estimatedDepartureText?: string | null
latestWarehouseEntry?: string | null
vesselName?: string | null
voyageNo?: string | null
referenceTransit?: string | null
surchargeDescription?: string | null
remark?: string | null
}
interface PriceBookParsedRatePayload extends Omit<PlatformPriceBookParsedRate,
'id' | 'originWarehouseId' | 'destinationWarehouseId' | 'weightFromKg' | 'unitPrice' | 'sourceRowNumber'> {
id?: ApiId | null
originWarehouseId?: ApiId | null
destinationWarehouseId?: ApiId | null
weightFromKg?: ApiNumber | null
unitPrice: ApiNumber
sourceRowNumber: ApiNumber
}
interface PriceBookParseIssuePayload extends Omit<PlatformPriceBookParseIssue, 'sourceRowNumber'> {
sourceRowNumber?: ApiNumber | null
}
interface PriceBookDetailPayload {
version: PriceBookVersionPayload
channels?: PriceBookParsedChannelPayload[] | null
rates?: PriceBookParsedRatePayload[] | null
issues?: PriceBookParseIssuePayload[] | null
}
const basePath = '/platform/v1/fba-pricing/carrier-rates/price-books'
function nullableId(value: ApiId | null | undefined) {
return value == null ? null : String(value)
}
function nullableText(value: string | null | undefined) {
return value ?? null
}
function numberValue(value: ApiNumber | null | undefined) {
const number = Number(value ?? 0)
return Number.isFinite(number) ? number : 0
}
function normalizeVersion(value: PriceBookVersionPayload): PlatformPriceBookVersion {
return {
...value,
id: String(value.id),
versionNo: numberValue(value.versionNo),
sourceFileId: nullableId(value.sourceFileId),
channelCount: numberValue(value.channelCount),
rateCount: numberValue(value.rateCount),
blockingErrorCount: numberValue(value.blockingErrorCount),
warningCount: numberValue(value.warningCount),
parseIssuesJson: nullableText(value.parseIssuesJson),
createdBy: nullableId(value.createdBy),
createdAt: nullableText(value.createdAt),
publishedBy: nullableId(value.publishedBy),
publishedAt: nullableText(value.publishedAt),
supersededAt: nullableText(value.supersededAt),
}
}
function normalizeChannel(value: PriceBookParsedChannelPayload): PlatformPriceBookParsedChannel {
return {
...value,
estimatedDepartureDate: nullableText(value.estimatedDepartureDate),
estimatedDepartureText: nullableText(value.estimatedDepartureText),
latestWarehouseEntry: nullableText(value.latestWarehouseEntry),
vesselName: nullableText(value.vesselName),
voyageNo: nullableText(value.voyageNo),
referenceTransit: nullableText(value.referenceTransit),
surchargeDescription: nullableText(value.surchargeDescription),
remark: nullableText(value.remark),
}
}
function normalizeRate(value: PriceBookParsedRatePayload): PlatformPriceBookParsedRate {
return {
...value,
id: nullableId(value.id),
originWarehouseId: nullableId(value.originWarehouseId),
destinationWarehouseId: nullableId(value.destinationWarehouseId),
destinationType: value.destinationType ?? null,
destinationCode: nullableText(value.destinationCode),
destinationName: nullableText(value.destinationName),
regionCode: nullableText(value.regionCode),
pricingTaxMode: value.pricingTaxMode ?? null,
weightFromKg: value.weightFromKg == null ? null : numberValue(value.weightFromKg),
unitPrice: numberValue(value.unitPrice),
sourceRowNumber: numberValue(value.sourceRowNumber),
sourceDestinationText: nullableText(value.sourceDestinationText),
remark: nullableText(value.remark),
}
}
function normalizeIssue(value: PriceBookParseIssuePayload): PlatformPriceBookParseIssue {
return {
...value,
sourceSheetName: nullableText(value.sourceSheetName),
sourceRowNumber: value.sourceRowNumber == null ? null : numberValue(value.sourceRowNumber),
sourceCell: nullableText(value.sourceCell),
}
}
function normalizeDetail(value: PriceBookDetailPayload): PlatformPriceBookDetail {
return {
version: normalizeVersion(value.version),
channels: (value.channels || []).map(normalizeChannel),
rates: (value.rates || []).map(normalizeRate),
issues: (value.issues || []).map(normalizeIssue),
}
}
export async function listPlatformPriceBooks(query: PlatformPriceBookQuery = {}): Promise<PlatformPriceBookVersion[]> {
const status = query.status?.trim()
const response = await http.get<ApiEnvelope<PriceBookVersionPayload[]>, ApiEnvelope<PriceBookVersionPayload[]>>(basePath, {
params: status ? { status } : {},
})
return unwrapApiResponse(response).map(normalizeVersion)
}
export async function getPlatformPriceBook(versionId: string): Promise<PlatformPriceBookDetail> {
const response = await http.get<ApiEnvelope<PriceBookDetailPayload>, ApiEnvelope<PriceBookDetailPayload>>(
`${basePath}/${encodeURIComponent(versionId)}`,
)
return normalizeDetail(unwrapApiResponse(response))
}
export async function uploadPlatformPriceBook(file: File): Promise<PlatformPriceBookDetail> {
const form = new FormData()
form.append('file', file)
const response = await http.post<ApiEnvelope<PriceBookDetailPayload>, ApiEnvelope<PriceBookDetailPayload>>(basePath, form)
return normalizeDetail(unwrapApiResponse(response))
}
export async function publishPlatformPriceBook(
versionId: string,
request: PriceBookConfirmationRequest,
): Promise<PlatformPriceBookDetail> {
const response = await http.post<ApiEnvelope<PriceBookDetailPayload>, ApiEnvelope<PriceBookDetailPayload>>(
`${basePath}/${encodeURIComponent(versionId)}/publish`,
request,
)
return normalizeDetail(unwrapApiResponse(response))
}
export function downloadPlatformPriceBookSource(versionId: string) {
return http.get<Blob, Blob>(`${basePath}/${encodeURIComponent(versionId)}/source`, { responseType: 'blob' })
}
@@ -5,102 +5,81 @@ import { describe, expect, it } from 'vitest'
const source = readFileSync(join(process.cwd(), 'src/modules/fba-pricing/carrier-rates/pages/CarrierRatesView.vue'), 'utf8')
const queryToolbarSource = readFileSync(join(process.cwd(), 'src/shared/components/QueryToolbar.vue'), 'utf8')
describe('CarrierRatesView source contract', () => {
it('keeps operation column first and covers every required business field', () => {
const scopeColumn = "key: 'scope', label: '适用范围'"
const destinationColumn = "key: 'destinationCode', prop: 'destinationCode'"
expect(source.indexOf("key: 'actions'")).toBeLessThan(source.indexOf(scopeColumn))
expect(source.indexOf(scopeColumn)).toBeLessThan(source.indexOf(destinationColumn))
expect(source.indexOf(destinationColumn)).toBeLessThan(source.indexOf("key: 'rate'"))
for (const label of ['起运仓', '业务类型', '渠道编码', '船司', 'FBA 目的仓', '计费单位', '单价', '币种', '固定附加费', '有效期', '备注', '生效时间', '结束时间']) {
describe('CarrierRatesView price-book source contract', () => {
it('keeps the operation column first and presents version-level history', () => {
expect(source.indexOf("key: 'actions'")).toBeLessThan(source.indexOf("key: 'version'"))
for (const label of ['价格表版本', '原始文件', '解析结果', '校验结果', '上传时间', '实际生效区间']) {
expect(source).toContain(label)
}
expect(source).toContain("placeholder: 'FBA 目的仓编码'")
expect(source).toContain("placeholder: '渠道编码'")
expect(source).toContain('formatEffectivePeriod')
expect(source).toContain('PENDING_CONFIRMATION')
expect(source).toContain('SUPERSEDED')
})
it('implements immutable version actions with exact backend permissions', () => {
expect(source).toContain("firstleg:fba-pricing:rate:edit")
expect(source).toContain('创建版本')
expect(source).toContain('新版本')
expect(source).toContain('停用')
expect(source).not.toContain('编辑价格')
it('uses only the parsed price-book workflow instead of legacy carrier-rate editing', () => {
for (const capability of [
'listPlatformPriceBooks',
'getPlatformPriceBook',
'uploadPlatformPriceBook',
'publishPlatformPriceBook',
'downloadPlatformPriceBookSource',
]) expect(source).toContain(capability)
for (const legacyCapability of [
'createPlatformCarrierRateVersion',
'listPlatformCarrierRates',
'retirePlatformCarrierRate',
'新增价格版本',
'编辑价格',
'有效期',
'生效日期',
]) expect(source).not.toContain(legacyCapability)
})
it('treats carrier rates as platform-wide data without a customer selector', () => {
expect(source).toContain("key: 'scope', label: '适用范围'")
expect(source).toContain('全部客户')
expect(source).not.toContain("key: 'customerId'")
expect(source).not.toContain('listPlatformTenants')
expect(source).not.toContain('PlatformTenantSelector')
expect(source).not.toContain('selectedTenantId')
expect(source).not.toContain('租户')
it('keeps uploaded prices immutable and makes confirmation the only edit action', () => {
expect(source).toContain('上传并解析')
expect(source).toContain('查看并确认')
expect(source).toContain('确认并立即生效')
expect(source).toContain('价格行不可在页面内修改')
expect(source).toContain('confirmPriceBook')
expect(source).toContain('publishPlatformPriceBook')
expect(source).not.toContain('草稿')
})
it('uses semantic compact filter widths without a customer field', () => {
for (const field of [
"key: 'rateCode', type: 'input', width: 220",
"key: 'destinationCode', type: 'input', width: 220",
"key: 'channelCode', type: 'input', width: 220",
"key: 'chargeUnit', type: 'select', width: 180",
"key: 'status', type: 'select', width: 160",
]) expect(source).toContain(field)
expect(source).not.toContain(':search-on-reset="false"')
expect(source).not.toContain('请选择所属企业并查询价格数据')
expect(source).not.toContain('customers.value[0]')
it('maps only active platform public domestic and overseas warehouses before confirmation', () => {
expect(source).toContain('listAllPlatformWarehouses()')
expect(source).toContain("warehouse.warehouseType === 'DOMESTIC'")
expect(source).toContain("warehouse.warehouseType === 'OVERSEAS'")
expect(source).toContain('起运仓映射')
expect(source).toContain('海外仓映射')
expect(source).toContain('originWarehouseCode')
expect(source).toContain('sourceDestinationName')
expect(source).not.toContain('tenantId')
expect(source).not.toContain('客户选择')
})
it('loads the platform-wide list and keeps row actions independent of customer scope', () => {
expect(source).toContain('void loadRateData()')
expect(source).toContain('listPlatformCarrierRates({ rateCode: filters.value.rateCode })')
expect(source).toContain('getPlatformCarrierRate(row.id)')
expect(source).toContain('retirePlatformCarrierRate(row.id)')
expect(source).toContain('<dt>适用范围</dt><dd>全部客户</dd>')
it('shows parsing blockers and a channel-tabbed, read-only preview', () => {
expect(source).toContain('nonMappingBlockingIssues')
expect(source).toContain('解析与校验')
expect(source).toContain('按渠道预览')
expect(source).toContain('<el-tabs v-model="selectedChannelCode"')
expect(source).toContain('sourceLocation')
expect(source).toContain('pricingTaxModeLabel')
})
it('loads shared carrier master data under the platform public scope', () => {
expect(source).toContain('listPlatformPartners({ page: 1, size: 200, type: \'CARRIER\', status: \'ACTIVE\' })')
expect(source).toContain('平台仓库或船司资料加载失败')
expect(source).not.toContain('listPlatformPartners(customerId,')
expect(source).not.toContain('所属企业的仓库或船司资料加载失败')
expect(source).toContain('async function loadPlatformMasterData()')
expect(source).not.toContain('loadCustomerMasterData')
it('accepts only xlsx uploads and retains original-file download', () => {
expect(source).toContain('accept=".xlsx,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"')
expect(source).toContain("endsWith('.xlsx')")
expect(source).toContain('10 * 1024 * 1024')
expect(source).toContain('downloadSource')
expect(source).toContain('saveBlob')
})
it('keeps public rate queries independent from edit-only master data', () => {
expect(source).toContain('await loadRateData()')
expect(source).not.toContain('loadPlatformMasterData(customerId)')
})
it('inherits QueryToolbar wrapping and full-width mobile controls', () => {
expect(queryToolbarSource).toContain('flex-wrap: wrap')
expect(queryToolbarSource).toContain('@media (max-width: 720px)')
expect(queryToolbarSource).toContain('width: 100%')
expect(source).not.toContain('--query-field-width')
})
it('guards unsaved work, failures and narrow screens', () => {
expect(source).toContain('useUnsavedWorkRegistration')
it('uses the shared query and table primitives with mobile layout handling', () => {
expect(source).toContain("import DataTable, { type DataTableColumn } from '@/shared/components/DataTable.vue'")
expect(source).toContain("import QueryToolbar, { type QueryField, type QueryModel } from '@/shared/components/QueryToolbar.vue'")
expect(source).toContain('LoadErrorState')
expect(source).toContain('ApiErrorMeta')
expect(queryToolbarSource).toContain('flex-wrap: wrap')
expect(source).toContain('@media (max-width: 720px)')
})
it('reuses the first-leg order list primitives for status, empty state and pagination', () => {
expect(source).toContain("import StatusBadge from '@/shared/components/StatusBadge.vue'")
expect(source).toContain('<StatusBadge :status="statusLabel(')
expect(source).toContain('<template #empty><el-empty')
expect(source).toContain('<div class="business-page">')
expect(source).not.toContain('<QueryToolbar v-model="filters" class="surface-panel"')
expect(source).not.toContain('<template #pagination-left>')
expect(source).not.toContain('<el-tag')
})
it('does not create route or navigation ownership in the page task', () => {
expect(source).not.toContain('createRouter')
expect(source).not.toContain('adminNavigation')
for (const unsupportedCapability of ['导入', '导出', '全部船期', '参考时效', '最晚进仓时间']) {
expect(source).not.toContain(unsupportedCapability)
}
})
})
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -7,7 +7,7 @@ export const fbaPricingRoutes = [
component: () => import('./carrier-rates/pages/CarrierRatesView.vue'),
meta: {
title: '船期价格表',
description: '维护起运仓、渠道、船司和 FBA 目的仓价格版本',
description: '上传、确认并查询平台公共船期价格表版本',
permission: 'firstleg:fba-pricing:rate:view',
},
},