From 72d77a7f70643e2c74856ac41c3d90d9157a7a31 Mon Sep 17 00:00:00 2001 From: liuyihe <2513762445@qq.com> Date: Thu, 3 Sep 2026 17:49:09 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../first-leg/api/inboundBilling.test.mjs | 14 +++- src/modules/first-leg/api/inboundBilling.ts | 26 +++++++ .../components/InboundBillingPanel.test.mjs | 8 ++- .../components/InboundBillingPanel.vue | 69 ++++++++++++++++--- src/modules/first-leg/types/inboundBilling.ts | 10 +++ .../first-leg/views/FirstLegOrdersView.vue | 3 + 6 files changed, 117 insertions(+), 13 deletions(-) diff --git a/src/modules/first-leg/api/inboundBilling.test.mjs b/src/modules/first-leg/api/inboundBilling.test.mjs index 2e8e05a..d0742c1 100644 --- a/src/modules/first-leg/api/inboundBilling.test.mjs +++ b/src/modules/first-leg/api/inboundBilling.test.mjs @@ -6,9 +6,17 @@ const source = readFileSync(new URL('./inboundBilling.ts', import.meta.url), 'ut const typesSource = readFileSync(new URL('../types/inboundBilling.ts', import.meta.url), 'utf8') test('queries the merchant order inbound-billing endpoint without tenant parameters', () => { - assert.match(source, /url: `\/merchant\/v1\/first-leg\/orders\/\$\{orderId\}\/inbound-billing`/) - assert.match(source, /method: 'GET'/) - assert.doesNotMatch(source, /tenantId|method: '(?:POST|PUT|PATCH|DELETE)'/) + const readSource = source.slice(source.indexOf('export async function getMerchantInboundBilling'), source.indexOf('function createFeeConfirmationKey')) + assert.match(readSource, /url: `\/merchant\/v1\/first-leg\/orders\/\$\{orderId\}\/inbound-billing`/) + assert.match(readSource, /method: 'GET'/) + assert.doesNotMatch(readSource, /tenantId|method: '(?:POST|PUT|PATCH|DELETE)'/) +}) + +test('confirms the latest customer fee version with an idempotency key', () => { + assert.match(source, /function confirmMerchantInboundFee/) + assert.match(source, /\/inbound-billing\/confirm/) + assert.match(source, /headers: \{ 'Idempotency-Key': idempotencyKey \}/) + assert.match(source, /data: \{ expectedFeeVersion \}/) }) test('sanitizes the platform-shaped response into a merchant whitelist', () => { diff --git a/src/modules/first-leg/api/inboundBilling.ts b/src/modules/first-leg/api/inboundBilling.ts index befba7b..59d9dc3 100644 --- a/src/modules/first-leg/api/inboundBilling.ts +++ b/src/modules/first-leg/api/inboundBilling.ts @@ -65,6 +65,8 @@ function sanitizeFee(fee: MerchantInboundFeeSnapshot): MerchantInboundFeeSnapsho feeStatus: fee.feeStatus, inboundId: fee.inboundId, inboundCode: fee.inboundCode, + measurementId: fee.measurementId == null ? null : String(fee.measurementId), + measurementSource: fee.measurementSource, chargeRuleId: fee.chargeRuleId, ruleCode: fee.ruleCode, chargeRuleVersion: fee.chargeRuleVersion, @@ -101,6 +103,10 @@ function sanitizeFee(fee: MerchantInboundFeeSnapshot): MerchantInboundFeeSnapsho confirmedAt: fee.confirmedAt, sentAt: fee.sentAt, nextProcessStatus: fee.nextProcessStatus, + customerConfirmationStatus: fee.customerConfirmationStatus, + customerConfirmedBy: fee.customerConfirmedBy == null ? null : String(fee.customerConfirmedBy), + customerConfirmedByName: fee.customerConfirmedByName, + customerConfirmedAt: fee.customerConfirmedAt, } } @@ -115,6 +121,7 @@ function sanitizeMeasurementSummary(item: MerchantInboundBilling['measurementSum actualVolumeCbm: item.actualVolumeCbm, volumeWeightKg: item.volumeWeightKg, actualGoodsName: item.actualGoodsName, + calculationSource: item.calculationSource, } } @@ -130,6 +137,9 @@ function sanitizeMeasurementItem(item: MerchantInboundBilling['measurementItems' unitVolumeCbm: item.unitVolumeCbm, totalVolumeCbm: item.totalVolumeCbm, volumeWeightKg: item.volumeWeightKg, + chargeRuleId: item.chargeRuleId == null ? null : String(item.chargeRuleId), + coefficientMode: item.coefficientMode, + calculationRuleVersion: item.calculationRuleVersion, } } @@ -154,3 +164,19 @@ export async function getMerchantInboundBilling(orderId: string): Promise({ + method: 'POST', + url: `/merchant/v1/first-leg/orders/${encodeURIComponent(orderId)}/inbound-billing/confirm`, + headers: { 'Idempotency-Key': idempotencyKey }, + data: { expectedFeeVersion }, + }) + return sanitizeFee(response) +} diff --git a/src/modules/first-leg/components/InboundBillingPanel.test.mjs b/src/modules/first-leg/components/InboundBillingPanel.test.mjs index 4669d84..197e707 100644 --- a/src/modules/first-leg/components/InboundBillingPanel.test.mjs +++ b/src/modules/first-leg/components/InboundBillingPanel.test.mjs @@ -10,17 +10,21 @@ test('renders loading, pending, isolated error, and published states', () => { assert.match(source, /role="alert"/) assert.match(source, /v-else-if="props\.unavailable \|\| !props\.data"/) assert.match(source, /role="status"/) - assert.match(source, /待分柜 · 费用已发布/) + assert.match(source, /费用已发布 · 待确认/) assert.match(source, /\$emit\('retry'\)/) }) -test('shows the confirmed merchant inbound, measurement details and customer confirmation entry', () => { +test('shows the published merchant inbound, measurement details and customer confirmation entry', () => { for (const label of ['入仓码', '平台收货仓', '实际毛重', '实测体积', '托盘与库位', '换算与精度', '最终计费量', '基础费用', '附加费用', '费用合计', '平台备注']) { assert.match(source, new RegExp(label)) } assert.match(source, /formatInboundMoney\(props\.data\.latestFee\.totalAmount/) assert.match(source, /formatConversionPrecision\(/) assert.match(source, /确认费用/) + assert.match(source, /二次确认费用/) + assert.match(source, /确认后该费用将锁定,平台不能再修改/) + assert.match(source, /customerConfirmationStatus === 'CONFIRMED'/) + assert.match(source, /confirmMerchantInboundFee/) assert.match(source, /measurementItems/) assert.doesNotMatch(source, /调整费用|发送费用|下载照片/) assert.doesNotMatch(source, /internalNotes|statusHistory|files/) diff --git a/src/modules/first-leg/components/InboundBillingPanel.vue b/src/modules/first-leg/components/InboundBillingPanel.vue index a857566..0af69bd 100644 --- a/src/modules/first-leg/components/InboundBillingPanel.vue +++ b/src/modules/first-leg/components/InboundBillingPanel.vue @@ -1,6 +1,8 @@ @@ -214,6 +266,7 @@ watch(() => [props.autoOpen, props.data] as const, ([autoOpen, data]) => { .inbound-measurements > div { min-width: 0; padding: 12px; } .measurement-detail-table { margin-top: 12px; } .fee-dialog-summary { margin-top: 0; } +.fee-confirmed-alert { margin-top: 14px; } .pallet-location-list { margin-top: 9px; border-top: 1px solid var(--border); } .pallet-location-list article { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: center; gap: 11px; padding: 12px 0; border-bottom: 1px solid var(--border); } .pallet-location-list article > svg { color: var(--primary); } diff --git a/src/modules/first-leg/types/inboundBilling.ts b/src/modules/first-leg/types/inboundBilling.ts index 9341bd7..bb34de7 100644 --- a/src/modules/first-leg/types/inboundBilling.ts +++ b/src/modules/first-leg/types/inboundBilling.ts @@ -62,6 +62,8 @@ export interface MerchantInboundFeeSnapshot { feeStatus: MerchantInboundFeeStatus inboundId: string inboundCode: string + measurementId?: string | null + measurementSource?: string | null chargeRuleId: string ruleCode: string chargeRuleVersion: number @@ -98,6 +100,10 @@ export interface MerchantInboundFeeSnapshot { confirmedAt: string | null sentAt: string | null nextProcessStatus: MerchantInboundProcessStatus | null + customerConfirmationStatus: 'PENDING_CONFIRMATION' | 'CONFIRMED' + customerConfirmedBy: string | null + customerConfirmedByName: string | null + customerConfirmedAt: string | null } export interface MerchantMeasurementItem { @@ -111,6 +117,9 @@ export interface MerchantMeasurementItem { unitVolumeCbm: number | null totalVolumeCbm: number | null volumeWeightKg: number | null + chargeRuleId: string | null + coefficientMode: 'ADD' | 'MULTIPLY' | null + calculationRuleVersion: string | null } export interface MerchantBillingBasis { @@ -137,6 +146,7 @@ export interface MerchantInboundBilling { actualVolumeCbm: number | null volumeWeightKg: number | null actualGoodsName: string | null + calculationSource: 'OUTER' | string | null } measurementItems: MerchantMeasurementItem[] billingBasis: MerchantBillingBasis diff --git a/src/modules/first-leg/views/FirstLegOrdersView.vue b/src/modules/first-leg/views/FirstLegOrdersView.vue index 984f9aa..aa0f9db 100644 --- a/src/modules/first-leg/views/FirstLegOrdersView.vue +++ b/src/modules/first-leg/views/FirstLegOrdersView.vue @@ -72,6 +72,7 @@ const companyAuditLoaded = ref(false) const companySubmissionBlocked = computed(() => companyAuditLoaded.value && tenantStore.company.auditStatus !== 'ACTIVE') const canCreateOrder = computed(() => auth.hasPermission('order:first-leg:create')) const canEditOrder = computed(() => auth.hasPermission('order:first-leg:edit')) +const canConfirmInboundFee = computed(() => auth.hasPermission('order:first-leg:edit')) const availableWarehouses = computed(() => store.warehouses.filter((warehouse) => warehouse.warehouseType === 'DOMESTIC')) const availableDestinationWarehouses = computed(() => store.warehouses.filter((warehouse) => warehouse.warehouseType === form.destinationType)) const filters = ref({ orderNo: '', receivingWarehouseId: '', channelName: '', cargoKeyword: '', createdRange: [], status: '', destinationType: '', trackingNumber: '' }) @@ -2148,7 +2149,9 @@ async function searchOrders(model: QueryModel) { :unavailable="inboundBillingUnavailable" :unavailable-message="inboundBillingUnavailableMessage" :error="inboundBillingError" + :can-confirm="canConfirmInboundFee" @retry="retryInboundBilling" + @confirmed="retryInboundBilling" />