From 3e8fa3c5d7fdbd2dbb15cb72a01ffe5acf679bf1 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Mon, 21 Sep 2026 10:44:14 +0800 Subject: [PATCH] fix: allow manual FBA quote when rate is missing --- src/modules/inbounds/fee/api.test.ts | 12 +++++ src/modules/inbounds/fee/api.ts | 12 ++++- src/modules/inbounds/fee/types.ts | 6 +-- src/modules/inbounds/index.ts | 1 + .../pages/UnifiedOrdersView.test.ts | 5 ++ .../pages/UnifiedOrdersView.vue | 48 ++++++++++++++----- 6 files changed, 67 insertions(+), 17 deletions(-) diff --git a/src/modules/inbounds/fee/api.test.ts b/src/modules/inbounds/fee/api.test.ts index 95f7aab9..0725148d 100644 --- a/src/modules/inbounds/fee/api.test.ts +++ b/src/modules/inbounds/fee/api.test.ts @@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it, vi } from 'vitest' import { ApiResponseError, http } from '@/shared/api/http' import { adjustPublishedInboundFee, + createManualInboundQuote, confirmInboundFee, createFeeActionIdempotencyKey, createFeeAdjustmentIdempotencyKey, @@ -69,6 +70,17 @@ describe('inbound fee API', () => { }) }) + it('creates a manual FBA quote through the dedicated fallback endpoint', async () => { + vi.mocked(http.post).mockResolvedValue({ code: '200', message: 'success', data: { ...snapshot, rateCode: 'MANUAL_QUOTE' } }) + const payload = { selectedWeightKg: 62.5, selectedVolumeCbm: 0.384, unitPrice: 12.2, reason: 'FBA价格未维护' } + + await expect(createManualInboundQuote('10001', '88', payload, 'fee-manual-1')) + .resolves.toMatchObject({ rateCode: 'MANUAL_QUOTE' }) + expect(http.post).toHaveBeenCalledWith('/platform/v1/inbounds/88/fees/manual-quote', payload, { + params: { tenantId: '10001' }, headers: { 'Idempotency-Key': 'fee-manual-1' }, + }) + }) + it('preserves backend conflict metadata for stale-version recovery', async () => { vi.mocked(http.put).mockResolvedValue({ code: 'A0409', message: '费用已被其他操作员修改,请刷新后重试', data: null, diff --git a/src/modules/inbounds/fee/api.ts b/src/modules/inbounds/fee/api.ts index 27993cfb..cebe6bf9 100644 --- a/src/modules/inbounds/fee/api.ts +++ b/src/modules/inbounds/fee/api.ts @@ -23,6 +23,16 @@ export async function calculateInboundFee(tenantId: string, inboundId: string, p return unwrapApiResponse(response) } +export async function createManualInboundQuote(tenantId: string, inboundId: string, payload: { + selectedWeightKg: number; selectedVolumeCbm: number; unitPrice: number; reason: string +}, idempotencyKey: string) { + const response = await http.post>( + `${feeBasePath(inboundId)}/manual-quote`, payload, + { params: { tenantId }, headers: { 'Idempotency-Key': idempotencyKey } }, + ) + return unwrapApiResponse(response) +} + async function transitionInboundFee( tenantId: string, inboundId: string, @@ -63,7 +73,7 @@ export function createFeeAdjustmentIdempotencyKey() { return createFeeActionIdempotencyKey('adjust') } -export function createFeeActionIdempotencyKey(action: 'adjust' | 'calculate' | 'confirm' | 'send') { +export function createFeeActionIdempotencyKey(action: 'adjust' | 'calculate' | 'manual' | 'confirm' | 'send') { const uuid = globalThis.crypto?.randomUUID?.() if (uuid) return `fee-${action}-${uuid}` return `fee-${action}-${Date.now()}-${Math.random().toString(36).slice(2, 12)}` diff --git a/src/modules/inbounds/fee/types.ts b/src/modules/inbounds/fee/types.ts index 1e3b814b..f658cd07 100644 --- a/src/modules/inbounds/fee/types.ts +++ b/src/modules/inbounds/fee/types.ts @@ -13,9 +13,9 @@ export interface InboundFeeSnapshot { chargeRuleId: string ruleCode: string chargeRuleVersion: number - carrierRateId: string + carrierRateId: string | null rateCode: string - carrierRateVersion: number + carrierRateVersion: number | null customerId: string customerName: string originWarehouseId: string @@ -45,7 +45,7 @@ export interface InboundFeeSnapshot { additionalAmount: number totalAmount: number currency: string - rateEffectiveFrom: string + rateEffectiveFrom: string | null rateEffectiveTo: string | null calculatedAt: string confirmedAt: string | null diff --git a/src/modules/inbounds/index.ts b/src/modules/inbounds/index.ts index 1aadb9cf..8772c58b 100644 --- a/src/modules/inbounds/index.ts +++ b/src/modules/inbounds/index.ts @@ -8,6 +8,7 @@ export { default as InboundFeePanel } from './fee/InboundFeePanel.vue' export { adjustPublishedInboundFee, calculateInboundFee, + createManualInboundQuote, confirmInboundFee, createFeeAdjustmentIdempotencyKey, createFeeActionIdempotencyKey, diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts index 3c8db07c..09f8a540 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.test.ts @@ -34,6 +34,11 @@ describe('UnifiedOrdersView contract', () => { expect(sendFeeSource).toContain('adjustPublishedInboundFee') expect(source).toContain('价格表参考单价') expect(source).toContain('本次对客单价') + expect(openDialogSource).toContain("feeConfirmPriceMissing.value = true") + expect(sendFeeSource).toContain('createManualInboundQuote') + expect(source).toContain("feeConfirmPriceMissing ? '未命中'") + expect(source).toContain("feeConfirmPriceMissing ? '人工定价原因'") + expect(source).not.toContain('
固定附加费
') expect(sendFeeSource).toContain("ElMessage.error(getApiErrorDetails(error, '确认费用发送失败').message)") }) diff --git a/src/modules/unified-orders/pages/UnifiedOrdersView.vue b/src/modules/unified-orders/pages/UnifiedOrdersView.vue index e6fffb40..69f9514f 100644 --- a/src/modules/unified-orders/pages/UnifiedOrdersView.vue +++ b/src/modules/unified-orders/pages/UnifiedOrdersView.vue @@ -66,6 +66,7 @@ import { adjustPublishedInboundFee, downloadWeightImage, calculateInboundFee, + createManualInboundQuote, confirmInboundFee, createFeeAdjustmentIdempotencyKey, createFeeActionIdempotencyKey, @@ -258,24 +259,27 @@ const feeConfirmWeight = ref(null) const feeConfirmVolume = ref(null) const feeConfirmReferenceLoading = ref(false) const feeConfirmReference = ref(null) +const feeConfirmPriceMissing = ref(false) const feeConfirmUnitPrice = ref(null) const feeConfirmPriceReason = ref('') type FeeConfirmAttempt = { fingerprint: string calculateKey: string + manualKey: string adjustKey: string confirmKey: string sendKey: string } let feeConfirmAttempt: FeeConfirmAttempt | null = null const feeConfirmPriceChanged = computed(() => { + if (feeConfirmPriceMissing.value) return true const reference = Number(feeConfirmReference.value?.unitPrice) const current = Number(feeConfirmUnitPrice.value) return reference >= 0 && current >= 0 && Math.abs(reference - current) > 0.000001 }) const feeConfirmPriceUnit = computed(() => { const snapshot = feeConfirmReference.value - return snapshot ? `${snapshot.currency} / ${snapshot.chargeUnit}` : '—' + return snapshot ? `${snapshot.currency} / ${snapshot.chargeUnit}` : 'CNY / 计费单位' }) const feeConfirmEstimatedTotal = computed(() => { const snapshot = feeConfirmReference.value @@ -1669,6 +1673,7 @@ async function openFeeConfirmDialog() { : measuredGrossWeight > 0 ? measuredGrossWeight : measuredNetWeight > 0 ? measuredNetWeight : null feeConfirmVolume.value = Number(actualTotals.volume) > 0 ? Number(actualTotals.volume) : null feeConfirmReference.value = null + feeConfirmPriceMissing.value = false feeConfirmUnitPrice.value = null feeConfirmPriceReason.value = '' feeConfirmAttempt = null @@ -1693,8 +1698,14 @@ async function openFeeConfirmDialog() { feeConfirmReference.value = snapshot feeConfirmUnitPrice.value = Number(snapshot.unitPrice) } catch (error) { - ElMessage.error(getApiErrorDetails(error, '价格表参考单价加载失败,暂不能推送费用').message) - feeConfirmOpen.value = false + const details = getApiErrorDetails(error, '价格表参考单价加载失败') + if (details.code === 'A0404') { + feeConfirmPriceMissing.value = true + ElMessage.warning('当前FBA价格未命中,请填写本次对客单价和人工定价原因') + } else { + ElMessage.error(details.message) + feeConfirmOpen.value = false + } } finally { feeConfirmReferenceLoading.value = false } @@ -1715,6 +1726,7 @@ function currentFeeConfirmAttempt( feeConfirmAttempt = { fingerprint, calculateKey: createFeeActionIdempotencyKey('calculate'), + manualKey: createFeeActionIdempotencyKey('manual'), adjustKey: createFeeAdjustmentIdempotencyKey(), confirmKey: createFeeActionIdempotencyKey('confirm'), sendKey: createFeeActionIdempotencyKey('send'), @@ -1747,14 +1759,25 @@ async function executeConfirmAndSendFee() { try { const tenantId = String(summary.tenantId) const inboundId = String(summary.inboundId) - let snapshot = await calculateInboundFee(tenantId, inboundId, { - selectedWeightKg, - selectedVolumeCbm, - weightSource: 'ACTUAL', - volumeSource: 'ACTUAL', - }, attempt.calculateKey) + let snapshot: InboundFeeSnapshot + try { + snapshot = await calculateInboundFee(tenantId, inboundId, { + selectedWeightKg, selectedVolumeCbm, weightSource: 'ACTUAL', volumeSource: 'ACTUAL', + }, attempt.calculateKey) + } catch (error) { + if (getApiErrorDetails(error, '费用试算失败').code !== 'A0404') throw error + feeConfirmPriceMissing.value = true + if (feeConfirmPriceReason.value.trim().length < 2) { + ElMessage.warning('价格未命中,请填写至少 2 个字符的人工定价原因') + return + } + snapshot = await createManualInboundQuote(tenantId, inboundId, { + selectedWeightKg, selectedVolumeCbm, unitPrice: selectedUnitPrice, + reason: feeConfirmPriceReason.value.trim(), + }, attempt.manualKey) + } const calculatedUnitPrice = Number(snapshot.unitPrice) - if (Math.abs(selectedUnitPrice - calculatedUnitPrice) > 0.000001) { + if (snapshot.rateCode !== 'MANUAL_QUOTE' && Math.abs(selectedUnitPrice - calculatedUnitPrice) > 0.000001) { snapshot = await adjustPublishedInboundFee(tenantId, inboundId, { expectedFeeVersion: snapshot.feeVersion, chargeableQuantity: Number(snapshot.chargeableQuantity), @@ -2311,13 +2334,12 @@ onBeforeUnmount(() => {
kg
m³
-
{{ feeConfirmPriceUnit }}
+
{{ feeConfirmPriceUnit }}
{{ feeConfirmPriceUnit }}
- +
参考计费量
{{ formatNumber(Number(feeConfirmReference.chargeableQuantity), feeConfirmReference.roundingScale) }} {{ feeConfirmReference.chargeUnit }}
-
固定附加费
{{ feeConfirmMoney(Number(feeConfirmReference.additionalAmount || 0), feeConfirmReference.currency) }}
按当前单价预计合计
{{ feeConfirmEstimatedTotal == null ? '—' : feeConfirmMoney(feeConfirmEstimatedTotal, feeConfirmReference.currency) }}

参考数据