diff --git a/finance/src/test/java/com/baiyue/finance/BusinessFeeSourceEventServiceTest.java b/finance/src/test/java/com/baiyue/finance/BusinessFeeSourceEventServiceTest.java index b1967442e..a997a3a92 100644 --- a/finance/src/test/java/com/baiyue/finance/BusinessFeeSourceEventServiceTest.java +++ b/finance/src/test/java/com/baiyue/finance/BusinessFeeSourceEventServiceTest.java @@ -164,4 +164,77 @@ class BusinessFeeSourceEventServiceTest { null, base.traceId(), "LASTLEG:FEE:701:" + version, null, payload, EventPayloadHash.sha256(EventPayloadCanonicalizer.canonicalize(payload))); } + + @Test + void sameEventIdWithDifferentPayloadIsRejectedWithoutWrites() { + var event = payableEvent("PUBLISHED", 1); + when(jdbc.queryForList(startsWith("SELECT payload_hash,status"), eq("LASTLEG"), eq(event.eventId()))) + .thenReturn(List.of(Map.of("payload_hash", "different", "status", "PROCESSED", "result_resource_id", 901L))); + assertThatThrownBy(() -> service.consume(event)).hasMessageContaining("同一事件ID"); + verify(jdbc, never()).update(anyString(), any(Object[].class)); + } + + @Test + void sameSourceVersionWithDifferentHashCannotOverwriteCost() { + var event = payableEvent("PUBLISHED", 1); + existingVersion("different", "AVAILABLE"); + assertThatThrownBy(() -> service.consume(event)).hasMessageContaining("同一来源版本"); + noFinancialWrites(); + } + + @Test + void voidedSourceCannotBeRevivedByPublicationReplay() { + var event = payableEvent("PUBLISHED", 1); + existingVersion(event.payload().get("sourceHash").asText(), "VOIDED"); + assertThat(service.consume(event)).isEqualTo(901L); + noFinancialWrites(); + } + + @Test + void voidTargetsOnlyMatchingOverseasCostVersion() { + var event = payableEvent("VOIDED", 1); + existingVersion(event.payload().get("sourceHash").asText(), "AVAILABLE"); + assertThat(service.consume(event)).isEqualTo(901L); + verify(jdbc).update(startsWith("UPDATE fin_cost SET status='VOIDED'"), + eq("LASTLEG"), eq("OVERSEAS_CONTAINER_FEE"), eq(701L), eq(1)); + verify(jdbc, never()).update(startsWith("INSERT INTO fin_cost"), any(Object[].class)); + } + + @Test + void frozenSourceRejectsAdjustmentWithoutReplacingSourceOrCost() { + var event = payableEvent("ADJUSTED", 2); + when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_fee_source f WHERE f.source_system=?"), + eq(Integer.class), eq("LASTLEG"), eq("OVERSEAS_CONTAINER_FEE"), eq(701L))).thenReturn(1); + assertThatThrownBy(() -> service.consume(event)).hasMessageContaining("禁止调整或替代"); + noFinancialWrites(); + } + + @Test + void frozenSourceRejectsVoidWithoutChangingSourceOrCost() { + var event = payableEvent("VOIDED", 1); + existingVersion(event.payload().get("sourceHash").asText(), "AVAILABLE"); + when(jdbc.queryForObject(startsWith("SELECT COUNT(*) FROM fin_fee_source f WHERE f.id=?"), + eq(Integer.class), eq(901L))).thenReturn(1); + assertThatThrownBy(() -> service.consume(event)).hasMessageContaining("禁止作废"); + noFinancialWrites(); + } + + private EventEnvelope payableEvent(String action, int version) { + when(suppliers.requireActive(71L, null)).thenReturn( + new com.baiyue.finance.client.WarehouseSupplierClient.SupplierSnapshot(71L, "供应商", null, "ACTIVE")); + return containerEvent("CNY", "61", action, version); + } + + private void existingVersion(String hash, String status) { + when(jdbc.queryForList(startsWith("SELECT id,source_hash,status FROM fin_fee_source"), + eq("LASTLEG"), eq("OVERSEAS_CONTAINER_FEE"), eq(701L), eq(1))) + .thenReturn(List.of(Map.of("id", 901L, "source_hash", hash, "status", status))); + } + + private void noFinancialWrites() { + verify(jdbc, never()).update(startsWith("INSERT INTO fin_fee_source"), any(Object[].class)); + verify(jdbc, never()).update(startsWith("UPDATE fin_fee_source"), any(Object[].class)); + verify(jdbc, never()).update(startsWith("INSERT INTO fin_cost"), any(Object[].class)); + verify(jdbc, never()).update(startsWith("UPDATE fin_cost"), any(Object[].class)); + } }