diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java index 84c787670..f0481134b 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java @@ -927,6 +927,7 @@ public class ReceivingContainerService { private void validateFbaSkuBalance(String customerId, String containerNo) { for (InboundRef ref : refs(customerId, containerNo)) { if (!"FBA".equals(destinationType(ref.line(), ref.order()))) continue; + if ("ONE_CLICK_DROPSHIP".equals(ref.order().getOrderType())) continue; List skuCodes = skuExpectations(ref); int received = 0; for (SkuExpectation sku : skuCodes) { diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerServiceBehaviorTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerServiceBehaviorTest.java index 2d65eb5a1..f078d5977 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerServiceBehaviorTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerServiceBehaviorTest.java @@ -448,6 +448,24 @@ class ReceivingContainerServiceBehaviorTest { verify(workflow, never()).completeContainer(any(), any()); } + @Test + void dropshipWholePalletCanCompleteWithoutBulkSkuReceiptRows() { + stubCurrent("FBA", "CONT-A"); + LastlegOrder dropshipOrder = order("FBA"); + dropshipOrder.setOrderType("ONE_CLICK_DROPSHIP"); + when(orders.selectById(1L)).thenReturn(dropshipOrder); + InboundPlanPalletAllocation received = new InboundPlanPalletAllocation(); + received.setLineId(10L); received.setPickedPackageCount(2); + when(allocations.selectList(any())).thenReturn(List.of(received)); + when(sessions.updateById(any(ReceivingContainerSession.class))).thenReturn(1); + + var completed = service.complete("7", new ReceivingContainerRequests.Action("dropship-complete")); + + assertThat(completed.status()).isEqualTo("COMPLETED"); + verifyNoInteractions(skuReceipts); + verify(workflow).completeContainer(any(), any()); + } + @Test void completedRequestReplaysWithoutAnActiveCustomerSession() { ReceivingContainerOperation operation = new ReceivingContainerOperation(); diff --git a/scripts/admin-test-env-modules/phase4-tail-order-controls-contract.test.mjs b/scripts/admin-test-env-modules/phase4-tail-order-controls-contract.test.mjs index 35edae658..64fe96acb 100644 --- a/scripts/admin-test-env-modules/phase4-tail-order-controls-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-tail-order-controls-contract.test.mjs @@ -41,6 +41,10 @@ test('module covers the public tail-order write surface before PDA receiving', ( assert.match(source, /currency === 'CNY'/) assert.match(source, /reviewStatus === 'PENDING'/) assert.match(source, /'Idempotency-Key': `\$\{context\.marker\}-tail-fee`/) + assert.match(source, /ensureCurrentUsdRate/) + assert.match(source, /monthFrom=\$\{month\}&monthTo=\$\{month\}/) + assert.match(source, /existing\?\.status === 'ACTIVE'/) + assert.match(source, /businessDate: `\$\{feeMonth\}-01`/) assert.doesNotMatch(source, /supplierDescription/) }) diff --git a/scripts/admin-test-env-modules/phase4-tail-order-controls.mjs b/scripts/admin-test-env-modules/phase4-tail-order-controls.mjs index 65edf988f..3201024a2 100644 --- a/scripts/admin-test-env-modules/phase4-tail-order-controls.mjs +++ b/scripts/admin-test-env-modules/phase4-tail-order-controls.mjs @@ -63,6 +63,26 @@ async function detail(context, row, name = '读取尾程订单即时详情') { return api(context, 'GET', name, query(`${ORDERS}/${encodeURIComponent(id(row.id))}`, { customerId: id(row.customerId) })) } +async function ensureCurrentUsdRate(context) { + const month = new Date().toISOString().slice(0, 7) + const path = `/api/platform/v1/last-leg/master-data/exchange-rates?page=1&size=20&monthFrom=${month}&monthTo=${month}` + const listed = await api(context, 'GET', '查询尾程费用业务月汇率', path) + const existing = records(listed.data).find((row) => row?.month === month) + if (existing?.status === 'ACTIVE' && Number(existing?.usdToCnyAverage) > 0) return month + if (existing) { + blocker(context, '尾程费用业务月汇率', `业务月${month}汇率存在但不是ACTIVE,拒绝覆盖既有主数据`, listed.response) + return null + } + const created = await api(context, 'POST', '补齐尾程费用业务月汇率', + '/api/platform/v1/last-leg/master-data/exchange-rates', { + month, usdToCnyAverage: 7.1, remark: `${context.marker} acceptance rate`, + }) + context.assert(MODULE, '尾程费用业务月汇率可用于USD兑CNY', created.response, + created.data?.month === month && created.data?.status === 'ACTIVE' + && Number(created.data?.usdToCnyAverage) > 0, + `month=${month}, status=${created.data?.status || 'missing'}`, OPTIONS) + return created.data?.status === 'ACTIVE' ? month : null +} async function verifyReceivingWarehouseChange(context, producer, details) { const bookingId = id(producer.containerBookingId) if (!bookingId) { @@ -168,11 +188,13 @@ async function verifyDestinationMergeFeeAndExport(context, details, producer) { { orderIds: allIds, palletMergeRequired: false }) context.assert(MODULE, '批量合托分组取消成功', batchCleared.response, batchCleared.data == null, trace(batchCleared.response), OPTIONS) + const feeMonth = await ensureCurrentUsdRate(context) + if (!feeMonth) return false const feeItem = `${context.marker}-TAIL-FEE`.slice(0, 128) const fee = await api(context, 'POST', '新增本轮尾程订单费用', query(`${ORDERS}/${encodeURIComponent(id(representative.id))}/fees`, { customerId: id(representative.customerId) }), { feeItemCode: feeItem.slice(0, 64), feeItemName: feeItem, direction: 'RECEIVABLE', - currency: 'USD', amount: 0.01, businessDate: new Date().toISOString().slice(0, 10) }, + currency: 'USD', amount: 0.01, businessDate: `${feeMonth}-01` }, { 'Idempotency-Key': `${context.marker}-tail-fee`.slice(0, 128) }) context.assert(MODULE, '尾程费用冻结币种金额和审核状态', fee.response, id(fee.data?.orderId) === id(representative.id) && fee.data?.feeItemName === feeItem