From 0fceece37892e6a97d99e488962fdc36de7aa6e8 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 12 Aug 2026 12:18:59 +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 --- .../firstleg/mapper/InboundOrderMapper.java | 12 ++++++- .../repository/InboundWorkflowRepository.java | 4 +-- .../service/MeasurementReviewService.java | 15 +++++++-- .../service/MeasurementReviewServiceTest.java | 18 ++++++----- .../finance-full-fixtures-contract.test.mjs | 21 +++++++++++-- .../finance-full-fixtures.mjs | 31 +++++++++++++------ .../finance-settlement-full-contract.test.mjs | 19 +++++++++++- .../finance-settlement-full.mjs | 24 +++++++++++--- 8 files changed, 113 insertions(+), 31 deletions(-) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundOrderMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundOrderMapper.java index 6de2f68c4..c5c889094 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundOrderMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/InboundOrderMapper.java @@ -12,6 +12,15 @@ public interface InboundOrderMapper extends BaseMapper { @Update(""" UPDATE fl_inbound_order SET process_status = 'INBOUNDED', measure_review_status = 'APPROVED', + actual_gross_weight_kg = (SELECT COALESCE(SUM(item.total_weight_kg), 0) + FROM fl_inbound_measurement_item item + WHERE item.tenant_id = #{tenantId} AND item.measurement_id = #{measurementId}), + actual_volume_cbm = (SELECT COALESCE(SUM(item.total_volume_cbm), 0) + FROM fl_inbound_measurement_item item + WHERE item.tenant_id = #{tenantId} AND item.measurement_id = #{measurementId}), + volume_weight_kg = (SELECT COALESCE(SUM(item.volume_weight_kg), 0) + FROM fl_inbound_measurement_item item + WHERE item.tenant_id = #{tenantId} AND item.measurement_id = #{measurementId}), inbounded_at = COALESCE(inbounded_at, CURRENT_TIMESTAMP(3)), updated_by = #{operatorId}, version = version + 1 WHERE tenant_id = #{tenantId} AND id = #{inboundId} AND deleted = 0 @@ -20,7 +29,8 @@ public interface InboundOrderMapper extends BaseMapper { """) int approveMeasurement(@Param("tenantId") long tenantId, @Param("inboundId") long inboundId, @Param("operatorId") long operatorId, - @Param("expectedVersion") int expectedVersion); + @Param("expectedVersion") int expectedVersion, + @Param("measurementId") long measurementId); @Update(""" UPDATE fl_inbound_order diff --git a/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundWorkflowRepository.java b/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundWorkflowRepository.java index 817a737d0..7dbe6c5fe 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundWorkflowRepository.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/repository/InboundWorkflowRepository.java @@ -67,9 +67,9 @@ public class InboundWorkflowRepository { } public int transitionReview(long tenantId, long inboundId, long operatorId, int expectedVersion, - boolean approved) { + boolean approved, long measurementId) { return approved - ? orders.approveMeasurement(tenantId, inboundId, operatorId, expectedVersion) + ? orders.approveMeasurement(tenantId, inboundId, operatorId, expectedVersion, measurementId) : orders.rejectMeasurement(tenantId, inboundId, operatorId, expectedVersion); } diff --git a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementReviewService.java b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementReviewService.java index cf6ae0953..623d98386 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementReviewService.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/service/MeasurementReviewService.java @@ -18,6 +18,7 @@ import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.time.LocalDateTime; +import java.math.BigDecimal; /** * 测量审核应用服务。客服确认测量数据后将入库单推进到产品主状态 INBOUNDED; @@ -69,8 +70,8 @@ public class MeasurementReviewService { throw new BizException(ResultCode.CONFLICT, "测量批次不是待审核状态"); } if (approved) { - long confirmed = repository.listMeasurementItems(tenantId, measurement.getId()).stream() - .map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getPackageCount) + var items = repository.listMeasurementItems(tenantId, measurement.getId()); + long confirmed = items.stream().map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getPackageCount) .filter(java.util.Objects::nonNull) .mapToLong(Integer::longValue) .sum(); @@ -78,10 +79,18 @@ public class MeasurementReviewService { throw new BizException(ResultCode.VALIDATION_FAILED, "人工确认箱数合计与入库单总箱数不一致,不能审核通过"); } + BigDecimal weight = items.stream().map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getTotalWeightKg) + .filter(java.util.Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + BigDecimal volume = items.stream().map(com.baiyue.firstleg.entity.InboundMeasurementItemEntity::getTotalVolumeCbm) + .filter(java.util.Objects::nonNull).reduce(BigDecimal.ZERO, BigDecimal::add); + if (weight.signum() <= 0 || volume.signum() <= 0) { + throw new BizException(ResultCode.VALIDATION_FAILED, + "审核通过的测量快照必须包含大于零的实重和体积"); + } } if (repository.transitionReview(tenantId, inboundId, operatorId, - request.expectedVersion(), approved) != 1) { + request.expectedVersion(), approved, request.measurementId()) != 1) { throw concurrentConflict(); } String result = approved ? "APPROVED" : "REJECTED"; diff --git a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java index e927df0dc..ebe3005c2 100644 --- a/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java +++ b/firstleg/src/test/java/com/baiyue/firstleg/service/MeasurementReviewServiceTest.java @@ -18,6 +18,7 @@ import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import java.util.List; +import java.math.BigDecimal; import java.util.concurrent.Callable; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executors; @@ -57,20 +58,20 @@ class MeasurementReviewServiceTest { .isEqualTo(ResultCode.NOT_FOUND); verify(repository).findOwned(10002L, 10L); - verify(repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean()); + verify(repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong()); } @Test @DisplayName("审核通过是测量到计费的唯一正常推进关口") void shouldApproveOnlyPendingSubmittedMeasurement() { Fixture fixture = fixture(false); - when(fixture.repository.transitionReview(10001L, 10L, 9L, 3, true)).thenReturn(1); + when(fixture.repository.transitionReview(10001L, 10L, 9L, 3, true, 20L)).thenReturn(1); when(fixture.repository.transitionMeasurement(10001L, 10L, 20L, "APPROVED")).thenReturn(1); fixture.service.approve(10001L, 9L, "审核员", 10L, "approve-1", new MeasurementReviewRequest(20L, 3, null)); - verify(fixture.repository).transitionReview(10001L, 10L, 9L, 3, true); + verify(fixture.repository).transitionReview(10001L, 10L, 9L, 3, true, 20L); verify(fixture.repository).transitionMeasurement(10001L, 10L, 20L, "APPROVED"); verify(fixture.repository).insertReview(any()); ArgumentCaptor history = @@ -94,7 +95,7 @@ class MeasurementReviewServiceTest { .extracting(error -> ((BizException) error).getResultCode()) .isEqualTo(ResultCode.VALIDATION_FAILED); - verify(fixture.repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean()); + verify(fixture.repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong()); } @Test @@ -106,7 +107,7 @@ class MeasurementReviewServiceTest { .isInstanceOf(BizException.class) .extracting(error -> ((BizException) error).getResultCode()) .isEqualTo(ResultCode.VALIDATION_FAILED); - verify(fixture.repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean()); + verify(fixture.repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong()); } @Test @@ -120,7 +121,7 @@ class MeasurementReviewServiceTest { new MeasurementReviewRequest(20L, 3, null)); verify(fixture.repository, never()).findOwned(anyLong(), anyLong()); - verify(fixture.repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean()); + verify(fixture.repository, never()).transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong()); } @Test @@ -158,7 +159,7 @@ class MeasurementReviewServiceTest { Fixture fixture = fixture(false); AtomicInteger attempts = new AtomicInteger(); CountDownLatch bothArrived = new CountDownLatch(2); - when(fixture.repository.transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean())) + when(fixture.repository.transitionReview(anyLong(), anyLong(), anyLong(), anyInt(), anyBoolean(), anyLong())) .thenAnswer(invocation -> { bothArrived.countDown(); bothArrived.await(); @@ -199,6 +200,9 @@ class MeasurementReviewServiceTest { measurement.setStatus("SUBMITTED"); InboundMeasurementItemEntity item = new InboundMeasurementItemEntity(); item.setPackageCount(5); + item.setTotalWeightKg(new BigDecimal("54.400")); + item.setTotalVolumeCbm(new BigDecimal("0.7480")); + item.setVolumeWeightKg(new BigDecimal("124.916")); when(repository.findOwned(10001L, 10L)).thenReturn(order); when(repository.findMeasurement(10001L, 10L, 20L)).thenReturn(measurement); when(repository.listMeasurementItems(10001L, 20L)).thenReturn(List.of(item)); diff --git a/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs b/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs index 616b4775d..f960634ed 100644 --- a/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs +++ b/scripts/admin-test-env-modules/finance-full-fixtures-contract.test.mjs @@ -21,8 +21,8 @@ test('fixture covers weight, volume, manual order, and container allocation cont }) test('fee writes use verified Firstleg paths and do not fabricate historical IDs', () => { - assert.match(source, /\/api\/platform\/v1\/inbounds\/.*fee-details\?accountId=/) - assert.match(source, /\/api\/platform\/v1\/loading-plans\/.*fee-details\?accountId=/) + assert.match(source, /listPath = `\$\{basePath\}\?accountId=/) + assert.match(source, /const basePath = `\/api\/platform\/v1\/loading-plans\//) assert.match(source, /sourceType: detail\?\.sourceType \|\| 'MANUAL'/) assert.match(source, /created\?\.marker === context\.marker/) assert.match(source, /\/fees`/) @@ -91,13 +91,15 @@ test('run behavior emits consumer shape, both billing bases, distinct dates, sup assert.equal(fixture.orders[0].waybillId, 'WB-1') assert.equal(fixture.orders[0].actualWeightKg, 12.5) assert.equal(fixture.orders[0].totalVolumeCbm, 0.072) - assert.deepEqual(new Set(fixture.feeSources.map((item) => item.billingBasis).filter((value) => value !== 'MANUAL_ORDER_FEE')), + assert.deepEqual(new Set(fixture.feeSources.map((item) => item.billingBasis).filter((value) => ['WEIGHT_KG', 'VOLUME_CBM'].includes(value))), new Set(['WEIGHT_KG', 'VOLUME_CBM'])) assert.ok(new Set(fixture.feeSources.map((item) => item.businessDate)).size >= 2) assert.equal(fixture.supplier.supplierId, 'PARTNER-1') assert.equal(fixture.supplier.supplierName, '往来单位一') assert.equal(fixture.voucherFileId, 'VOUCHER-1') assert.ok(Array.isArray(fixture.allocations)) + assert.ok(fixture.feeSources.filter((item) => item.direction === 'RECEIVABLE').length >= 4) + assert.ok(new Set(fixture.feeSources.filter((item) => item.direction === 'RECEIVABLE').map((item) => item.businessDate)).size >= 2) }) test('run behavior preserves null IDs and deployment blockers when supplier, voucher and container are unavailable', async () => { @@ -113,3 +115,16 @@ test('run behavior preserves null IDs and deployment blockers when supplier, vou assert.ok(context.steps.some((step) => step.deploymentBlocker && step.name.includes('往来单位'))) assert.ok(context.steps.some((step) => step.deploymentBlocker && step.name.includes('voucherFileId'))) }) + +test('manual fee approval path keeps accountId as a query parameter', () => { + assert.match(source, /basePath = `\/api\/platform\/v1\/inbounds\/.*fee-details`/) + assert.match(source, /\$\{basePath\}\/\$\{feeId\}\/approve\?accountId=/) + assert.doesNotMatch(source, /fee-details\?accountId=\$\{encodeURIComponent\(resource\.tenantId\)\}`\)/) +}) + +test('fixture creates four real receivable manual sources before payable source', () => { + assert.match(source, /const receivableResources = measured\.slice\(0, 4\)/) + assert.match(source, /feeItems\.RECEIVABLE, 'RECEIVABLE'/) + assert.match(source, /'MANUAL_RECEIVABLE'/) + assert.match(source, /dateOffset\(-1 - \(index % 2\)\)/) +}) diff --git a/scripts/admin-test-env-modules/finance-full-fixtures.mjs b/scripts/admin-test-env-modules/finance-full-fixtures.mjs index 6d1d2b99d..148516d54 100644 --- a/scripts/admin-test-env-modules/finance-full-fixtures.mjs +++ b/scripts/admin-test-env-modules/finance-full-fixtures.mjs @@ -77,17 +77,18 @@ async function loadFeeItem(context, direction) { async function createManualFee(context, resource, feeItem, direction, amount, basis, businessDate) { if (context.financeFixtureHooks?.createManualFee) return context.financeFixtureHooks.createManualFee(context, resource, feeItem, direction, amount, basis, businessDate) - const path = `/api/platform/v1/inbounds/${encodeURIComponent(resource.inboundId)}/fee-details?accountId=${encodeURIComponent(resource.tenantId)}` + const basePath = `/api/platform/v1/inbounds/${encodeURIComponent(resource.inboundId)}/fee-details` + const listPath = `${basePath}?accountId=${encodeURIComponent(resource.tenantId)}` const body = { feeItemId: Number(feeItem.id), direction, businessDate, unitPrice: amount, currency: 'CNY', amount, settlementIncluded: true, remark: `${context.marker}-${basis}` } - const response = await request(context, 'POST', path, body) + const response = await request(context, 'POST', listPath, body) const detail = context.apiSuccess(MODULE, `${basis}人工费用明细`, response, `inboundId=${resource.inboundId}`, OPTIONS) const feeId = id(detail?.id) context.assert(MODULE, `${basis}费用来源身份和金额`, response, Boolean(feeId) && id(detail?.inboundId) === id(resource.inboundId) && Number(detail?.amount) === Number(amount), `feeId=${feeId || 'missing'}, amount=${detail?.amount ?? 'missing'}`, OPTIONS) if (!feeId) return null - const approve = await request(context, 'POST', `${path}/${feeId}/approve`) + const approve = await request(context, 'POST', `${basePath}/${feeId}/approve?accountId=${encodeURIComponent(resource.tenantId)}`) context.apiSuccess(MODULE, `${basis}费用审核`, approve, '', OPTIONS) return { feeSourceId: feeId, sourceType: detail?.sourceType || 'MANUAL', direction, amount: Number(amount), businessDate, currency: 'CNY', billingBasis: basis, inboundId: id(resource.inboundId), orderId: id(resource.orderId), @@ -218,10 +219,21 @@ export async function run(context) { `activePriceVersions=${pricingEvidence.activeVersionIds?.join(',') || 'none'};本轮隔离订单未产生 ${basis} 来源,价格配置/订单条件不支持,不能伪装成功`) } } - const manualResource = measured.at(-1) - if (manualResource && feeItems.PAYABLE && supplier?.id) { - const fee = await createManualFee(context, manualResource, feeItems.PAYABLE, 'PAYABLE', 80, 'MANUAL_ORDER_FEE', dateOffset(-1)) - if (fee) fixtures.push({ ...fee, supplierId: id(supplier.id), partnerId: id(supplier.id), supplierName: supplier.partnerName || null }) + const receivableResources = measured.slice(0, 4) + if (feeItems.RECEIVABLE) { + for (const [index, resource] of receivableResources.entries()) { + const fee = await createManualFee(context, resource, feeItems.RECEIVABLE, 'RECEIVABLE', 100 + index * 10, + 'MANUAL_RECEIVABLE', dateOffset(-1 - (index % 2))) + if (fee) fixtures.push(fee) + } + } + const manualResources = [measured.at(-1)].filter(Boolean) + if (feeItems.PAYABLE && supplier?.id) { + for (const [index, manualResource] of manualResources.entries()) { + const fee = await createManualFee(context, manualResource, feeItems.PAYABLE, 'PAYABLE', 80 + index * 10, + 'MANUAL_ORDER_FEE', dateOffset(-3 - index)) + if (fee) fixtures.push({ ...fee, supplierId: id(supplier.id), partnerId: id(supplier.id), supplierName: supplier.partnerName || null }) + } } const container = context.createdResources?.phase3ContainerLoading @@ -229,12 +241,13 @@ export async function run(context) { && container?.loadingPlanId && container?.containerBookingId let containerFixture = null if (containerOwned && feeItems.PAYABLE && supplier?.id) { - const path = `/api/platform/v1/loading-plans/${encodeURIComponent(container.loadingPlanId)}/fee-details?accountId=${encodeURIComponent(container.tenantId)}` + const basePath = `/api/platform/v1/loading-plans/${encodeURIComponent(container.loadingPlanId)}/fee-details` + const path = `${basePath}?accountId=${encodeURIComponent(container.tenantId)}` const body = { feeItemId: Number(feeItems.PAYABLE.id), direction: 'PAYABLE', businessDate: dateOffset(-2), amount: 300, currency: 'CNY', settlementIncluded: true, remark: `${context.marker}-CONTAINER_SHARED` } const response = await request(context, 'POST', path, body) const detail = context.apiSuccess(MODULE, '创建按柜费用来源', response, '', OPTIONS) const feeId = id(detail?.id) - if (feeId) { const approval = await request(context, 'POST', `${path}/${feeId}/approve`); context.apiSuccess(MODULE, '审核按柜费用来源', approval, '', OPTIONS) } + if (feeId) { const approval = await request(context, 'POST', `${basePath}/${feeId}/approve?accountId=${encodeURIComponent(container.tenantId)}`); context.apiSuccess(MODULE, '审核按柜费用来源', approval, '', OPTIONS) } containerFixture = { feeSourceId: feeId || null, sourceType: 'MANUAL', direction: 'PAYABLE', amount: 300, businessDate: body.businessDate, currency: 'CNY', billingBasis: 'CONTAINER_SHARED', supplierId: supplier?.id ? id(supplier.id) : null, partnerId: supplier?.id ? id(supplier.id) : null, diff --git a/scripts/admin-test-env-modules/finance-settlement-full-contract.test.mjs b/scripts/admin-test-env-modules/finance-settlement-full-contract.test.mjs index 169f4195e..a24d4774a 100644 --- a/scripts/admin-test-env-modules/finance-settlement-full-contract.test.mjs +++ b/scripts/admin-test-env-modules/finance-settlement-full-contract.test.mjs @@ -41,7 +41,7 @@ function fakeContext(overrides = {}) { } } } -test('values 兼容数组、records 和 named map', () => { +test('values helper supports nested arrays while run consumes frozen top-level arrays', () => { assert.deepEqual(values([{ id: 1 }]), [{ id: 1 }]) assert.deepEqual(values({ records: [{ id: 2 }] }), [{ id: 2 }]) assert.deepEqual(values({ marker: 'x', selected: { id: 3 } }), [{ id: 3 }]) @@ -83,3 +83,20 @@ test('缺 fixture 或真实 voucher 时记录 blocker 且不写接口', async () assert.equal(noVoucher.steps.length, 1); assert.equal(noVoucher.steps[0].passed, false) assert.equal(noVoucher.steps[0].options.deploymentBlocker, true) }) + +test('AR/AP 数组为空或不完整时原子阻断且不查询、不写入', async () => { + const empty = fakeContext({ statements: [], receivableSettlements: [], payableSettlements: [] }) + await run(empty) + assert.equal(empty.calls.length, 0) + assert.equal(empty.gaps.length, 1) + assert.equal(empty.steps[0].passed, false) + assert.match(empty.steps[0].evidence, /actual AR=0, AP=0/) + + const partial = fakeContext({ receivableSettlements: [ + { settlementId: '101', statementId: '11', direction: 'RECEIVABLE', status: 'ISSUED' }, + ] }) + await run(partial) + assert.equal(partial.calls.length, 0) + assert.equal(partial.gaps.length, 1) + assert.match(partial.steps[0].evidence, /actual AR=1, AP=1/) +}) diff --git a/scripts/admin-test-env-modules/finance-settlement-full.mjs b/scripts/admin-test-env-modules/finance-settlement-full.mjs index d7b46225e..d13116394 100644 --- a/scripts/admin-test-env-modules/finance-settlement-full.mjs +++ b/scripts/admin-test-env-modules/finance-settlement-full.mjs @@ -54,8 +54,9 @@ export async function run(context) { gap(context, '取得本轮财务 Fixture', 'financeFullFixtures', '缺少匹配 marker 的 financeFullFixtures,禁止借历史单据') return } - const statements = values(fixtures.statements) - const receivables = values(fixtures.receivableSettlements) + const statements = Array.isArray(fixtures.statements) ? fixtures.statements.filter(Boolean) : [] + const receivables = Array.isArray(fixtures.receivableSettlements) ? fixtures.receivableSettlements.filter(Boolean) : [] + const payables = Array.isArray(fixtures.payableSettlements) ? fixtures.payableSettlements.filter(Boolean) : [] const customerId = id(fixtures.customerId) const voucherFileId = id(fixtures.voucherFileId ?? fixtures.voucher?.fileId ?? fixtures.voucher?.id) if (!customerId || !voucherFileId) { @@ -63,13 +64,26 @@ export async function run(context) { return } + const issuedStatementIds = new Set(statements.filter((item) => item.status === 'ISSUED').map(statementId).filter(Boolean)) + const validReceivables = receivables.filter((item) => item.direction === 'RECEIVABLE' + && settlementId(item) && statementId(item) && issuedStatementIds.has(statementId(item))) + const validPayables = payables.filter((item) => item.direction === 'PAYABLE' && settlementId(item) && item.supplierId) + const arSettlementIds = new Set(validReceivables.map(settlementId)) + const arStatementIds = new Set(validReceivables.map(statementId)) + if (validReceivables.length !== 3 || arSettlementIds.size !== 3 || arStatementIds.size !== 3 || validPayables.length !== 1) { + gap(context, '取得完整收付结算 Fixture', 'three AR and one AP settlements', + `需要 3 张关联本轮 ISSUED 对账单的独立 AR 和 1 张带 supplierId 的 AP;actual AR=${validReceivables.length}, AP=${validPayables.length}`, + 'incomplete-settlement-fixture') + return + } + const manualIds = values(fixtures.feeSources).filter((fee) => fee.sourceType === 'MANUAL').map((fee) => id(fee.feeSourceId)) const usedIds = new Set(statements.flatMap((statement) => values(statement.feeSourceIds).map(id))) context.assert(MODULE, '手动上传费用出账后来源完结', null, manualIds.length > 0 && manualIds.every((feeId) => usedIds.has(feeId)), `manual=${manualIds.length}, settled=${manualIds.filter((feeId) => usedIds.has(feeId)).length}`, OPTIONS) - const multiTargets = receivables.filter((item) => settlementId(item) && statementId(item)).slice(0, 3) + const multiTargets = validReceivables if (multiTargets.length < 3) gap(context, '取得三张应收结算单', 'multi receipt allocation', '本轮没有三张独占应收单,禁止借历史单据') else { const details = await Promise.all(multiTargets.map((item) => detail(context, settlementId(item)))) @@ -81,7 +95,7 @@ export async function run(context) { fixtures.receipts = { ...(fixtures.receipts || {}), multi: data(response) } } - const primary = receivables.find((item) => settlementId(item) && statementId(item)) + const primary = validReceivables[0] if (!primary) gap(context, '取得应收结算单', 'receivable partial/full', 'receivableSettlements 未提供 settlementId 与 statementId') else { const initial = await detail(context, settlementId(primary)) @@ -95,7 +109,7 @@ export async function run(context) { fixtures.settlementFinalStates = { ...(fixtures.settlementFinalStates || {}), receivable: final.settlement } } - const payable = values(fixtures.payableSettlements).find((item) => item.direction === 'PAYABLE' && settlementId(item)) + const payable = validPayables[0] const payableId = settlementId(payable) if (!payableId) gap(context, '取得应付结算单', 'payable partial/full/overpayment', '前置 Fixture 的 payableSettlements 数组为空或缺少 PAYABLE 结算单') else {