优化
This commit is contained in:
@@ -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
@@ -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',
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user