package com.baiyue.finance; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.springframework.dao.DuplicateKeyException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DriverManagerDataSource; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.UUID; import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; class FinanceSettlementSchemaContractTest { @Test void moduleSchemaCreatesFormalSettlementTablesAndSummaryIndex() throws Exception { String sql = Files.readString(Path.of("src/main/resources/db/finance.sql"), StandardCharsets.UTF_8); String unified = Files.readString(resolveUnifiedSql(), StandardCharsets.UTF_8); assertThat(sql) .contains("CREATE TABLE IF NOT EXISTS fin_settlement (") .contains("CREATE TABLE IF NOT EXISTS fin_settlement_line (") .contains("CREATE TABLE IF NOT EXISTS fin_settlement_payment (") .contains("idx_fin_settlement_customer_direction_status (tenant_id, direction, status)") .contains("total_amount DECIMAL(20,6)", "paid_amount DECIMAL(20,6)", "outstanding_amount DECIMAL(20,6)") .contains("updated_at DATETIME(3)"); String normalized = unified.toLowerCase().replaceAll("\\s+", ""); assertThat(normalized) .contains("createtablefin_settlement(") .contains("createtablefin_settlement_line(") .contains("createtablefin_settlement_payment(") .contains("idx_fin_settlement_customer_direction_status(tenant_id,direction,status)"); assertThat(unified).doesNotContain("ALTER TABLE fin_settlement", "assert_finance"); } /** 执行两份真实基线的结算表定义,作废历史不占位,有效应收仍由数据库兜底唯一。 */ @ParameterizedTest @ValueSource(booleans = {false, true}) void voidedReceivableCanBeReplacedWithoutAllowingDuplicateActiveSettlement(boolean unified) throws Exception { Path source = unified ? resolveUnifiedSql() : Path.of("src/main/resources/db/finance.sql"); String sql = Files.readString(source, StandardCharsets.UTF_8); var table = Pattern.compile("CREATE TABLE (?:IF NOT EXISTS )?fin_settlement \\(.*?\\) ENGINE=[^;]*;", Pattern.DOTALL).matcher(sql); assertThat(table.find()).isTrue(); // H2 仅去除 MySQL 的存储、注释和普通索引语法,保留生成列表达式及所有唯一约束。 String ddl = table.group().replaceFirst("\\) ENGINE=[^;]*;", ");") .replaceAll("(?i) COMMENT '(?:[^']|'')*'", "") .replaceAll("(?i)\\bUNSIGNED\\b|\\bVIRTUAL\\b|\\bSTORED\\b|_utf8mb4", "") .replaceAll("(?m)^\\s*KEY [^\\r\\n]+\\r?\\n", "") .replaceAll("UNIQUE KEY (\\w+)", "CONSTRAINT $1 UNIQUE"); var jdbc = new JdbcTemplate(new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1")); jdbc.execute(ddl); insertSettlement(jdbc, 1, 10, "AR-OLD", "RECEIVABLE", 501L, "VOIDED"); insertSettlement(jdbc, 2, 10, "AR-REPLACEMENT", "RECEIVABLE", 501L, "ISSUED"); insertSettlement(jdbc, 3, 10, "AR-HISTORY", "RECEIVABLE", 501L, "VOIDED"); for (String status : new String[]{"ISSUED", "PARTIALLY_PAID", "PAID"}) { jdbc.update("UPDATE fin_settlement SET status=? WHERE id=2", status); assertThatThrownBy(() -> insertSettlement(jdbc, 4, 10, "AR-DUP", "RECEIVABLE", 501L, "ISSUED")) .isInstanceOf(DuplicateKeyException.class); assertThatThrownBy(() -> jdbc.update("UPDATE fin_settlement SET status='ISSUED' WHERE id=1")) .isInstanceOf(DuplicateKeyException.class); } jdbc.update("UPDATE fin_settlement SET status='VOIDED' WHERE id=2"); assertThat(jdbc.update("UPDATE fin_settlement SET status='ISSUED' WHERE id=1")).isEqualTo(1); insertSettlement(jdbc, 5, 11, "AR-OTHER-CUSTOMER", "RECEIVABLE", 501L, "ISSUED"); insertSettlement(jdbc, 6, 10, "AP-ONE", "PAYABLE", null, "ISSUED"); insertSettlement(jdbc, 7, 10, "AP-TWO", "PAYABLE", null, "ISSUED"); assertThatThrownBy(() -> insertSettlement(jdbc, 8, 10, "AR-OLD", "RECEIVABLE", 502L, "VOIDED")) .isInstanceOf(DuplicateKeyException.class); assertThat(jdbc.queryForObject("SELECT COUNT(*) FROM fin_settlement", Integer.class)).isEqualTo(6); } /** 构造必填字段完整的结算行,仅改变本测试关注的客户、来源与状态。 */ private static void insertSettlement(JdbcTemplate jdbc, long id, long customerId, String no, String direction, Long billId, String status) { jdbc.update("INSERT INTO fin_settlement(id,tenant_id,settlement_no,direction,customer_name_snapshot," + "reconciliation_statement_id,total_amount,paid_amount,outstanding_amount,generated_date,due_date,status,created_by,updated_by) " + "VALUES (?,?,?,?,'合成测试客户',?,0.03,0,0.03,CURRENT_DATE,CURRENT_DATE,?,7,7)", id, customerId, no, direction, billId, status); } private static Path resolveUnifiedSql() { Path direct = Path.of("deploy", "sql", "头程数据库统一增量-MySQL8.sql"); return Files.exists(direct) ? direct : Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"); } }