From 09f6e28c1f4963b62f8b2fa579ee73afc1ae7eed Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 20 Aug 2026 16:13:51 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FinancePhase1OperationService.java | 15 ++++- .../service/FinanceSettlementService.java | 16 ++++- .../FinancePhase1OperationBehaviorTest.java | 3 +- .../FinanceSettlementBehaviorTest.java | 61 +++++++++++++++++++ ...ance-reconciliation-full-contract.test.mjs | 20 +++++- .../finance-reconciliation-full.mjs | 37 ++++++++++- ...ers-approval-convergence-contract.test.mjs | 6 ++ scripts/admin-test-env-modules/orders.mjs | 2 +- .../phase3-container-loading.mjs | 12 +++- .../phase3-producer-chain-contract.test.mjs | 7 +-- 10 files changed, 164 insertions(+), 15 deletions(-) diff --git a/finance/src/main/java/com/baiyue/finance/service/FinancePhase1OperationService.java b/finance/src/main/java/com/baiyue/finance/service/FinancePhase1OperationService.java index 3ce52020d..cc3786f0c 100644 --- a/finance/src/main/java/com/baiyue/finance/service/FinancePhase1OperationService.java +++ b/finance/src/main/java/com/baiyue/finance/service/FinancePhase1OperationService.java @@ -366,7 +366,20 @@ public class FinancePhase1OperationService { private String json(Object value) { try { return objectMapper.writeValueAsString(value); } catch (Exception e) { throw error("FIN-DATA-500", "财务数据序列化失败"); } } private String hash(Object value) { try { return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(json(value).getBytes(StandardCharsets.UTF_8))); } catch (Exception e) { throw error("FIN-DATA-500", "幂等请求哈希失败"); } } private static String text(JsonNode node, String field) { JsonNode value = node.path(field); return value.isMissingNode() || value.isNull() ? null : value.asText(); } - private static Long jsonLong(JsonNode node, String field) { JsonNode value = node.path(field); return value.isIntegralNumber() && value.asLong() > 0 ? value.asLong() : null; } + private static Long jsonLong(JsonNode node, String field) { + JsonNode value = node.path(field); + if (value.isIntegralNumber() && value.canConvertToLong()) { + long parsed = value.longValue(); + return parsed > 0 ? parsed : null; + } + if (!value.isTextual()) return null; + try { + long parsed = Long.parseLong(value.textValue()); + return parsed > 0 ? parsed : null; + } catch (NumberFormatException ignored) { + return null; + } + } private static Long firstLong(Long first, Long second) { return first != null ? first : second; } private static String firstText(String first, String second) { return StringUtils.hasText(first) ? first : second; } private static String defaultText(JsonNode node, String field, String fallback) { String value = text(node, field); return value == null || value.isBlank() ? fallback : value; } diff --git a/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java b/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java index 97c6f1df4..51e8290b1 100644 --- a/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java +++ b/finance/src/main/java/com/baiyue/finance/service/FinanceSettlementService.java @@ -462,7 +462,9 @@ public class FinanceSettlementService { BigDecimal unitPrice = quantity == null || quantity.signum() == 0 ? null : amount.divide(quantity, 6, java.math.RoundingMode.HALF_UP); Long supplierId = jsonLong(snapshot, "supplierId"); - if (supplierId == null) throw error("FIN_409", "应付结算缺少供应商快照"); + if ("PAYABLE".equals(text(row, "direction")) && supplierId == null) { + throw error("FIN_409", "应付结算缺少供应商快照"); + } return new SettlementLine(number(row, "fee_source_id").longValue(), text(row, "source_type"), number(row, "source_fee_id").longValue(), number(row, "source_fee_version").intValue(), text(row, "direction"), number(row, "customer_id").longValue(), jsonText(snapshot, "customerName"), @@ -594,7 +596,17 @@ public class FinanceSettlementService { } private static Long jsonLong(JsonNode node, String key) { JsonNode value = node.path(key); - return value.isIntegralNumber() ? value.longValue() : null; + if (value.isIntegralNumber() && value.canConvertToLong()) { + long parsed = value.longValue(); + return parsed > 0 ? parsed : null; + } + if (!value.isTextual()) return null; + try { + long parsed = Long.parseLong(value.textValue()); + return parsed > 0 ? parsed : null; + } catch (NumberFormatException ignored) { + return null; + } } private static LocalDate jsonDate(JsonNode node, String key) { String value = jsonText(node, key); diff --git a/finance/src/test/java/com/baiyue/finance/FinancePhase1OperationBehaviorTest.java b/finance/src/test/java/com/baiyue/finance/FinancePhase1OperationBehaviorTest.java index 03e429d9c..a1eeca5df 100644 --- a/finance/src/test/java/com/baiyue/finance/FinancePhase1OperationBehaviorTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinancePhase1OperationBehaviorTest.java @@ -144,7 +144,7 @@ class FinancePhase1OperationBehaviorTest { when(rs.getString("currency_code")).thenReturn("CNY"); when(rs.getString("review_status")).thenReturn("APPROVED"); when(rs.getString("effective_status")).thenReturn("AVAILABLE"); - when(rs.getString("source_snapshot")).thenReturn("{\"customerName\":\"客户9\",\"supplierId\":79,\"supplierName\":\"供应商A\",\"inboundCode\":\"IN-9\",\"containerNo\":\"CONT-9\",\"businessDate\":\"2026-08-11\",\"chargeableQuantity\":\"10\"}"); + when(rs.getString("source_snapshot")).thenReturn("{\"customerName\":\"客户9\",\"supplierId\":\"79\",\"supplierName\":\"供应商A\",\"inboundCode\":\"IN-9\",\"containerNo\":\"CONT-9\",\"businessDate\":\"2026-08-11\",\"chargeableQuantity\":\"10\"}"); when(rs.getBigDecimal("amount")).thenReturn(new BigDecimal("120.00")); when(rs.getBoolean("statement_generated")).thenReturn(false); return List.of(mapper.mapRow(rs, 0)); @@ -164,6 +164,7 @@ class FinancePhase1OperationBehaviorTest { assertThat(source.sourceBusinessId()).isEqualTo("IN-9"); assertThat(source.inboundCode()).isEqualTo("IN-9"); assertThat(source.containerNo()).isEqualTo("CONT-9"); + assertThat(source.supplierId()).isEqualTo(79L); assertThat(source.supplierName()).isEqualTo("供应商A"); assertThat(source.reviewStatus()).isEqualTo("APPROVED"); assertThat(source.businessDate()).isEqualTo(LocalDate.of(2026, 8, 11)); diff --git a/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java b/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java index 333cf48e4..8aecf441c 100644 --- a/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java +++ b/finance/src/test/java/com/baiyue/finance/FinanceSettlementBehaviorTest.java @@ -10,6 +10,7 @@ import com.baiyue.finance.dto.FinanceSettlementDtos.CreatePayableSettlementReque import com.baiyue.finance.dto.FinanceSettlementDtos.CreateReceivableSettlementRequest; import com.baiyue.finance.dto.FinanceSettlementDtos.CreateSettlementPaymentRequest; import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementDetail; +import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementLine; import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementSummary; import com.baiyue.finance.dto.FinanceSettlementDtos.SettlementActionRequest; import com.baiyue.finance.service.FinanceEventException; @@ -286,6 +287,66 @@ class FinanceSettlementBehaviorTest { verify(jdbc).update(startsWith("INSERT INTO fin_settlement_line"), eq(88L), eq(10L), eq(501L)); } + @Test + void receivableSettlementLineDoesNotRequireSupplierSnapshot() { + Map row = new java.util.HashMap<>(Map.ofEntries( + Map.entry("fee_source_id", 31L), Map.entry("source_type", "FBA_PRICING"), + Map.entry("source_fee_id", 41L), Map.entry("source_fee_version", 1), + Map.entry("direction", "RECEIVABLE"), Map.entry("customer_id", 10L), + Map.entry("source_snapshot", "{\"customerName\":\"客户10\",\"businessDate\":\"2026-08-11\"}"), + Map.entry("fee_item_name", "头程运费"), Map.entry("amount", new BigDecimal("100.00")), + Map.entry("currency_code", "CNY"), Map.entry("review_status", "APPROVED"), + Map.entry("status", "GENERATED"))); + row.put("order_id", null); + row.put("inbound_id", null); + row.put("container_id", null); + + SettlementLine line = ReflectionTestUtils.invokeMethod( + service(org.mockito.Mockito.mock(JdbcTemplate.class), org.mockito.Mockito.mock(FinanceFileClient.class)), + "line", row, detail("RECEIVABLE", "ISSUED", BigDecimal.ZERO, + new BigDecimal("100.00"), 0).summary()); + + assertThat(line.direction()).isEqualTo("RECEIVABLE"); + assertThat(line.supplierId()).isNull(); + assertThat(line.customerName()).isEqualTo("客户10"); + } + + @Test + void payableSettlementLineStillRequiresSupplierSnapshot() { + Map row = new java.util.HashMap<>(Map.ofEntries( + Map.entry("direction", "PAYABLE"), + Map.entry("source_snapshot", "{\"customerName\":\"客户10\",\"businessDate\":\"2026-08-11\"}"), + Map.entry("amount", new BigDecimal("100.00")))); + + assertCode(() -> ReflectionTestUtils.invokeMethod( + service(org.mockito.Mockito.mock(JdbcTemplate.class), org.mockito.Mockito.mock(FinanceFileClient.class)), + "line", row, detail("PAYABLE", "ISSUED", BigDecimal.ZERO, + new BigDecimal("100.00"), 0).summary()), "FIN_409"); + } + + @Test + void payableSettlementLineAcceptsStringEncodedSupplierIdSnapshot() { + Map row = new java.util.HashMap<>(Map.ofEntries( + Map.entry("fee_source_id", 31L), Map.entry("source_type", "INBOUND_FEE"), + Map.entry("source_fee_id", 41L), Map.entry("source_fee_version", 1), + Map.entry("direction", "PAYABLE"), Map.entry("customer_id", 10L), + Map.entry("source_snapshot", "{\"customerName\":\"客户10\",\"supplierId\":\"2090349552523644929\",\"supplierName\":\"供应商A\",\"businessDate\":\"2026-08-11\"}"), + Map.entry("fee_item_name", "装车费"), Map.entry("amount", new BigDecimal("80.00")), + Map.entry("currency_code", "CNY"), Map.entry("review_status", "APPROVED"), + Map.entry("status", "AVAILABLE"))); + row.put("order_id", null); + row.put("inbound_id", null); + row.put("container_id", null); + + SettlementLine line = ReflectionTestUtils.invokeMethod( + service(org.mockito.Mockito.mock(JdbcTemplate.class), org.mockito.Mockito.mock(FinanceFileClient.class)), + "line", row, detail("PAYABLE", "ISSUED", BigDecimal.ZERO, + new BigDecimal("80.00"), 0).summary()); + + assertThat(line.supplierId()).isEqualTo(2090349552523644929L); + assertThat(line.supplierName()).isEqualTo("供应商A"); + } + @Test void receivableCreationRejectsAlreadySettledStatement() { JdbcTemplate jdbc = org.mockito.Mockito.mock(JdbcTemplate.class); diff --git a/scripts/admin-test-env-modules/finance-reconciliation-full-contract.test.mjs b/scripts/admin-test-env-modules/finance-reconciliation-full-contract.test.mjs index 212c05c4b..fbc262a34 100644 --- a/scripts/admin-test-env-modules/finance-reconciliation-full-contract.test.mjs +++ b/scripts/admin-test-env-modules/finance-reconciliation-full-contract.test.mjs @@ -15,7 +15,8 @@ test('Finance AP consumes only published fin_fee_source rows and never substitut assert.match(source, /!isUnboundContainerCost\(source\.raw\) \|\| hasContainerAllocations/) }) const financePermissions = [ - 'platform:finance:customer-account:list', 'platform:finance:customer-statement:list', + 'platform:finance:customer-account:list', 'platform:finance:customer-arap:list', + 'platform:finance:fee-source:review', 'platform:finance:customer-statement:list', 'platform:finance:reconciliation:create', 'platform:finance:reconciliation:review', 'platform:finance:reconciliation:issue', 'platform:finance:reconciliation:adjust', 'platform:finance:reconciliation:void', 'platform:finance:settlement-ar:create', @@ -58,12 +59,13 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF const calls = [] const steps = [] const coverageGaps = [] + const reviewedSourceIds = new Set() function responseForStatement(statement) { return structuredClone(statement) } function applyAction(statement, body) { const status = { SUBMIT: 'PENDING_REVIEW', APPROVE: 'APPROVED', REJECT: 'REJECTED', ISSUE: 'ISSUED', ADJUST: 'ISSUED', VOID: 'VOIDED', REOPEN: 'DRAFT' }[body.action] - statement.revisions.push({ action: `RECONCILIATION_${body.action}` }) + statement.revisions.push({ action: body.action }) statement.status = status statement.version += 1 if (body.action === 'ADJUST') { @@ -112,6 +114,20 @@ function fakeContext(withFixture = true, permissions = financePermissions, denyF reviewStatus: 'APPROVED', sourceStatus: 'AVAILABLE' } })) } + const feeSourceDetailMatch = requestPath.match(/\/fee-sources\/(\d+)$/) + if (method === 'GET' && feeSourceDetailMatch) { + const localId = feeSourceDetailMatch[1] + return ok({ feeSourceId: localId, reviewStatus: reviewedSourceIds.has(localId) ? 'APPROVED' : 'PENDING', + sourceStatus: 'AVAILABLE', statementGenerated: false, operationVersion: reviewedSourceIds.has(localId) ? 1 : 0 }) + } + const feeSourceReviewMatch = requestPath.match(/\/fee-sources\/(\d+)\/review$/) + if (method === 'POST' && feeSourceReviewMatch) { + reviewedSourceIds.add(feeSourceReviewMatch[1]) + const reviewed = { feeSourceId: feeSourceReviewMatch[1], reviewStatus: 'APPROVED', + sourceStatus: 'AVAILABLE', statementGenerated: false, operationVersion: 1 } + if (idem) idempotency.set(idem, reviewed) + return ok(reviewed) + } if (method === 'POST' && requestPath.endsWith('/statements')) { if (body.currency !== 'CNY') return { http: 200, body: { code: 'FIN_400' } } if (String(body.customerId) !== '11') return { http: 200, body: { code: 'FIN_404' } } diff --git a/scripts/admin-test-env-modules/finance-reconciliation-full.mjs b/scripts/admin-test-env-modules/finance-reconciliation-full.mjs index 5378d99c2..543f42a44 100644 --- a/scripts/admin-test-env-modules/finance-reconciliation-full.mjs +++ b/scripts/admin-test-env-modules/finance-reconciliation-full.mjs @@ -6,6 +6,8 @@ const STATEMENT_LIST_PERMISSION = 'platform:finance:customer-statement:list' const LEGACY_STATEMENT_VIEW_PERMISSION = 'platform:finance:bill:view' const REQUIRED_PERMISSIONS = [ 'platform:finance:customer-account:list', + 'platform:finance:customer-arap:list', + 'platform:finance:fee-source:review', STATEMENT_LIST_PERMISSION, 'platform:finance:reconciliation:create', 'platform:finance:reconciliation:review', @@ -116,6 +118,31 @@ function finish(context, start) { failed: steps.filter((step) => !step.passed).length, }) } + +async function approveFixtureSources(context, fixture) { + const approved = [] + for (const source of fixture.sources) { + const detailResponse = await context.request('GET', `${BASE}/fee-sources/${encodeURIComponent(source.feeSourceId)}`) + let detail = context.apiSuccess(MODULE, '回读本轮 Finance 费用来源审核状态', detailResponse, + `feeSourceId=${source.feeSourceId}`, OPTIONS) + if (!detail) return false + if (detail.reviewStatus === 'PENDING') { + const reviewResponse = await write(context, 'POST', `${BASE}/fee-sources/${encodeURIComponent(source.feeSourceId)}/review`, { + action: 'APPROVE', reason: context.marker, expectedVersion: Number(detail.operationVersion), + }, `fee-source-review-${source.feeSourceId}`) + detail = context.apiSuccess(MODULE, '审核本轮 Finance 费用来源', reviewResponse, + `feeSourceId=${source.feeSourceId}`, OPTIONS) + context.assert(MODULE, '费用来源审核后成为结算候选', reviewResponse, + detail?.reviewStatus === 'APPROVED' && detail?.sourceStatus === 'AVAILABLE' + && detail?.statementGenerated === false, + `feeSourceId=${source.feeSourceId}, reviewStatus=${detail?.reviewStatus ?? 'missing'}, sourceStatus=${detail?.sourceStatus ?? 'missing'}`, OPTIONS) + } + if (detail?.reviewStatus !== 'APPROVED') return false + approved.push(source.feeSourceId) + } + fixture.root.approvedFinanceFeeSourceIds = approved + return approved.length === fixture.sources.length +} function diagnostic(response) { return { http: response?.http ?? null, code: response?.body?.code == null ? null : String(response.body.code), @@ -306,8 +333,7 @@ async function issueCompleteChain(context, statement) { statement = await act(context, statement, 'ISSUE', 'chain-issue-final') const actions = new Set(list(statement?.revisions).map((revision) => revision.action)) context.assert(MODULE, '完整链保留驳回、重开、调整、作废和再次出账 revision', null, - ['RECONCILIATION_REJECT', 'RECONCILIATION_REOPEN', 'RECONCILIATION_ADJUST', - 'RECONCILIATION_VOID', 'RECONCILIATION_ISSUE'].every((action) => actions.has(action)), + ['REJECT', 'REOPEN', 'ADJUST', 'VOID', 'ISSUE'].every((action) => actions.has(action)), `actions=${[...actions].join(',')}`, OPTIONS) return statement } @@ -445,6 +471,13 @@ export async function run(context) { const groups = [cutoffSources, ...(() => { return [remainingSources.slice(0, 1), remainingSources.slice(1)] })()] + if (!await approveFixtureSources(context, fixture)) { + blocker(context, '本轮 Finance 费用来源全部审核通过', + '费用来源未完成 PENDING -> APPROVED,禁止跳过审核直接生成对账单', + 'finance-fee-source-review-failed') + finish(context, start) + return + } const issuedStatements = [] for (let index = 0; index < groups.length; index += 1) { const created = await createStatement(context, groups[index], `selected-${index + 1}`, diff --git a/scripts/admin-test-env-modules/orders-approval-convergence-contract.test.mjs b/scripts/admin-test-env-modules/orders-approval-convergence-contract.test.mjs index cb4e9ea60..400228596 100644 --- a/scripts/admin-test-env-modules/orders-approval-convergence-contract.test.mjs +++ b/scripts/admin-test-env-modules/orders-approval-convergence-contract.test.mjs @@ -23,6 +23,12 @@ test('measurable inbound without fulfillment task remains pending', () => { }, { http: 200, body: { code: 'A0404' } }, tenantId, orderId), false) }) +test('approval polling remains pending before the first fulfillment task response', () => { + assert.equal(approvedInboundReady({ + summary: { inboundId: null, mainStatus: 'PENDING_REVIEW' }, + }, null, tenantId, orderId), false) +}) + test('approval convergence requires matching inbound state, order and customer task identity', () => { const detail = { summary: { inboundId: '40001', mainStatus: 'WAIT_MEASURE' } } assert.equal(approvedInboundReady(detail, taskResponse, tenantId, orderId), true) diff --git a/scripts/admin-test-env-modules/orders.mjs b/scripts/admin-test-env-modules/orders.mjs index b0bf0ce9e..9702e60a9 100644 --- a/scripts/admin-test-env-modules/orders.mjs +++ b/scripts/admin-test-env-modules/orders.mjs @@ -17,7 +17,7 @@ const ORDER_ESTIMATE_VOLUME_CBM = BOXES.reduce((total, box) => function id(value) { return value == null ? '' : String(value) } function records(value) { return Array.isArray(value?.records) ? value.records : [] } function pageOk(value) { return Array.isArray(value?.records) && Number.isFinite(Number(value?.total)) } -function success(response) { return response.http === 200 && String(response.body?.code) === '200' } +function success(response) { return response?.http === 200 && String(response.body?.code) === '200' } function sleep(milliseconds) { return new Promise((resolve) => setTimeout(resolve, milliseconds)) } function key(context, operation) { return `${context.marker}-${operation}`.slice(0, 128) } export function approvedInboundReady(orderDetail, taskResponse, tenantId, orderId) { diff --git a/scripts/admin-test-env-modules/phase3-container-loading.mjs b/scripts/admin-test-env-modules/phase3-container-loading.mjs index 188cbf5ae..bd83bd062 100644 --- a/scripts/admin-test-env-modules/phase3-container-loading.mjs +++ b/scripts/admin-test-env-modules/phase3-container-loading.mjs @@ -147,12 +147,20 @@ export async function run(context) { const primaryRequirement = { containerType: '40HC', containerKind: 'FCL', capacityCbm: 76, maxWeightKg: 28000 } const realContainerNo = `TST-${suffix}-01`.slice(0, 64) const wrongContainerNo = `TST-${suffix}-02`.slice(0, 64) + const agentResponse = await context.request('GET', + '/api/platform/v1/suppliers?tenantId=0&category=BOOKING&status=ACTIVE&page=1&size=20') + const agentPage = context.apiSuccess(MODULE, '查询放舱所需 ACTIVE 订舱代理', agentResponse, '', OPTIONS) + const bookingAgent = records(agentPage).find((item) => item?.category === 'BOOKING' && item?.status === 'ACTIVE') + context.assert(MODULE, '放舱订舱使用真实启用供应商', agentResponse, + Boolean(bookingAgent?.id && bookingAgent?.name), + `supplierId=${id(bookingAgent?.id) || 'missing'}`, OPTIONS) + if (!bookingAgent?.id || !bookingAgent?.name) return const bookingDraft = { customerId: tenantId, customerName: inbound?.summary?.customerName || `验收客户-${tenantId}`, channelCode, warehouseId, billOfLadingNo: `BL-${suffix}`.slice(0, 128), - shippingCompanyId: null, shippingCompanyName: null, - bookingAgentId: null, bookingAgentName: null, + shippingCompanyId: id(bookingAgent.id), shippingCompanyName: bookingAgent.name, + bookingAgentId: id(bookingAgent.id), bookingAgentName: bookingAgent.name, vesselName: `验收船-${suffix}`.slice(0, 128), voyageNo: `V-${suffix}`.slice(0, 64), departurePortCode: 'CNSZX', departurePortName: '深圳', destinationPortCode: 'USLAX', destinationPortName: '洛杉矶', placeOfDelivery: 'LAX', diff --git a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs index f7483c38d..fdfe9583a 100644 --- a/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase3-producer-chain-contract.test.mjs @@ -372,10 +372,9 @@ test('container loading enforces channel isolation and stable PDA identity acros assert.match(value, /id\(replacedTask\?\.cabinetNo\) === stableIdentity\.cabinetNo/) assert.match(value, /afterReplacementSnapshot\?\.snapshotToken === beforeReplacementSnapshot\?\.snapshotToken/) assert.match(value, /pdaIdentityStable: true/) - assert.match(value, /shippingCompanyId: null, shippingCompanyName: null/) - assert.match(value, /bookingAgentId: null, bookingAgentName: null/) - assert.doesNotMatch(value, /shippingCompanyName: `验收船公司-/) - assert.doesNotMatch(value, /bookingAgentName: `验收代理-/) + assert.match(value, /category=BOOKING&status=ACTIVE/) + assert.match(value, /shippingCompanyId: id\(bookingAgent\.id\), shippingCompanyName: bookingAgent\.name/) + assert.match(value, /bookingAgentId: id\(bookingAgent\.id\), bookingAgentName: bookingAgent\.name/) assert.match(value, /const frozenTransport = Object\.fromEntries\(transportFields\.map/) assert.match(value, /if \(!frozenTransportMatches\) return/) assert.match(value, /expectedVersion: Number\(bookingCreatedDraft\?\.oceanBooking\?\.version\), reason: context\.marker/) From 5b9ae25f29f60286de3228794b600531b8e90b72 Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Thu, 20 Aug 2026 17:00:05 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E4=BC=98=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mapper/ContainerBookingMapper.java | 2 +- .../ContainerBookingMapperContractTest.java | 18 +++++++ .../finance-downloads-contract.test.mjs | 1 + .../finance-downloads.mjs | 2 +- ...ance-reconciliation-full-contract.test.mjs | 24 ++++++++++ .../finance-reconciliation-full.mjs | 48 +++++++++++++++++++ 6 files changed, 93 insertions(+), 2 deletions(-) diff --git a/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java b/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java index 1ae3ac1da..526bb2b11 100644 --- a/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java +++ b/firstleg/src/main/java/com/baiyue/firstleg/mapper/ContainerBookingMapper.java @@ -11,7 +11,7 @@ import org.apache.ibatis.annotations.Update; @Mapper public interface ContainerBookingMapper extends BaseMapper { - @Update("UPDATE fl_container_booking SET container_no=#{containerNo},ocean_booking_id=#{oceanBookingId},ocean_booking_requirement_id=#{requirementId},record_status='ACTIVE',bill_of_lading_no=#{billOfLadingNo},vessel_name=#{vesselName},voyage_no=#{voyageNo},shipping_company_id=#{shippingCompanyId},shipping_company_name=#{shippingCompanyName},departure_port_code=#{departurePortCode},departure_port_name=#{departurePortName},destination_port_code=#{destinationPortCode},destination_port_name=#{destinationPortName},channel_code=#{channelCode},channel_name=#{channelName},customer_id=#{customerId},customer_name=#{customerName},estimated_sailing_at=#{estimatedSailingAt},estimated_port_arrival_at=#{estimatedArrivalAt},updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=NOW(),version=version+1 WHERE id=#{id} AND version=#{version} AND source_type='SIMULATION' AND record_status='ACTIVE' AND ocean_booking_id IS NULL") + @Update("UPDATE fl_container_booking SET container_no=#{containerNo},ocean_booking_id=#{oceanBookingId},ocean_booking_requirement_id=#{requirementId},record_status='ACTIVE',bill_of_lading_no=#{billOfLadingNo},vessel_name=#{vesselName},voyage_no=#{voyageNo},shipping_company_id=#{shippingCompanyId},shipping_company_name=#{shippingCompanyName},departure_port_code=#{departurePortCode},departure_port_name=#{departurePortName},destination_port_code=#{destinationPortCode},destination_port_name=#{destinationPortName},channel_code=#{channelCode},channel_name=#{channelName},customer_id=#{customerId},customer_name=#{customerName},estimated_sailing_at=#{estimatedSailingAt},estimated_port_arrival_at=#{estimatedPortArrivalAt},updated_by=#{operatorId},updated_by_name=#{operatorName},updated_at=NOW(),version=version+1 WHERE id=#{id} AND version=#{version} AND source_type='SIMULATION' AND record_status='ACTIVE' AND ocean_booking_id IS NULL") int linkReleasedBooking(@Param("id") long id, @Param("oceanBookingId") long oceanBookingId, @Param("requirementId") long requirementId, @Param("containerNo") String containerNo, @Param("billOfLadingNo") String billOfLadingNo, @Param("vesselName") String vesselName, @Param("voyageNo") String voyageNo, @Param("shippingCompanyId") Long shippingCompanyId, @Param("shippingCompanyName") String shippingCompanyName, @Param("departurePortCode") String departurePortCode, @Param("departurePortName") String departurePortName, @Param("destinationPortCode") String destinationPortCode, @Param("destinationPortName") String destinationPortName, @Param("channelCode") String channelCode, @Param("channelName") String channelName, @Param("customerId") Long customerId, @Param("customerName") String customerName, @Param("estimatedSailingAt") java.time.LocalDateTime estimatedSailingAt, @Param("estimatedPortArrivalAt") java.time.LocalDateTime estimatedPortArrivalAt, @Param("version") int version, @Param("operatorId") long operatorId, @Param("operatorName") String operatorName); @Select("""