feat: add self-operated analytics and enforce Lastleg module boundaries
This commit is contained in:
+111
@@ -0,0 +1,111 @@
|
||||
package com.baiyue.lastleg.schema;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class SelfoperatedAnalyticsSchemaContractTest {
|
||||
private static final Pattern TABLE = Pattern.compile(
|
||||
"(?s)CREATE TABLE (ll_selfop_\\w+) \\(.*?\\) ENGINE=InnoDB[^;]+;");
|
||||
|
||||
private static Map<String, String> tables(String sql) {
|
||||
var result = new LinkedHashMap<String, String>();
|
||||
var matcher = TABLE.matcher(sql);
|
||||
while (matcher.find()) {
|
||||
assertNull(result.put(matcher.group(1), matcher.group()), "duplicate table definition");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Test
|
||||
void sourceAndEmptyDatabaseBaselineContainIdenticalTenIndependentTables() throws Exception {
|
||||
var source = tables(Files.readString(Path.of("src/main/resources/db/lastleg.sql")));
|
||||
var deployment = tables(Files.readString(Path.of("../deploy/sql/海外区域统一增量-MySQL8.sql")));
|
||||
assertEquals(10, source.size());
|
||||
assertEquals(source, deployment);
|
||||
for (var entry : source.entrySet()) {
|
||||
var ddl = entry.getValue();
|
||||
assertTrue(ddl.contains("COLLATE=utf8mb4_bin"), entry.getKey());
|
||||
assertTrue(ddl.contains("id BIGINT NOT NULL"));
|
||||
assertTrue(ddl.contains("created_at DATETIME(3) NOT NULL"));
|
||||
assertTrue(ddl.contains("updated_at DATETIME(3) NOT NULL"));
|
||||
assertFalse(ddl.contains("AUTO_INCREMENT"));
|
||||
assertFalse(ddl.contains("tenant_id"));
|
||||
assertFalse(ddl.contains("FOREIGN KEY"));
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void unknownSourceTimeAndStockCannotBecomeZeroOrEligibleSalesByDefault() throws Exception {
|
||||
var ddl = tables(Files.readString(Path.of("src/main/resources/db/lastleg.sql")));
|
||||
for (var table : new String[]{"ll_selfop_sync_scope", "ll_selfop_outbound_order"}) {
|
||||
assertTrue(ddl.get(table).contains("source_timezone VARCHAR(64) NULL COMMENT"));
|
||||
}
|
||||
var stock = ddl.get("ll_selfop_inventory_snapshot");
|
||||
for (var column : new String[]{"physical_quantity", "available_quantity", "frozen_quantity", "transit_quantity"}) {
|
||||
assertTrue(stock.contains(column + " DECIMAL(20,6) NULL COMMENT"));
|
||||
}
|
||||
assertTrue(ddl.get("ll_selfop_outbound_order").contains("sales_eligible TINYINT NOT NULL DEFAULT 0"));
|
||||
assertTrue(ddl.get("ll_selfop_outbound_order").contains("source_status = 100 AND source_timezone IS NOT NULL"));
|
||||
assertTrue(ddl.get("ll_selfop_outbound_item").contains("CHECK (actual_quantity >= 0)"));
|
||||
// Negative observed stock must be retained as INVALID, never rejected/coerced by a quantity CHECK.
|
||||
assertFalse(stock.contains("CHECK (available_quantity"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void durableRecoveryAndImmutableVersionKeysArePresent() throws Exception {
|
||||
var ddl = tables(Files.readString(Path.of("src/main/resources/db/lastleg.sql")));
|
||||
var task = ddl.get("ll_selfop_sync_task");
|
||||
for (var column : new String[]{"apply_state", "affected_warehouse_codes_json", "observation_baseline",
|
||||
"routing_manifest_json", "request_hash", "apply_completed_at_utc", "task_no", "execution_purpose",
|
||||
"validation_root_task_id", "validation_budget_json", "result_published_run_id", "progress_json",
|
||||
"progress_updated_at_utc"}) {
|
||||
assertTrue(task.contains(" " + column + " "), column);
|
||||
}
|
||||
assertTrue(task.contains("UNIQUE KEY uk_selfop_task_no (task_no)"));
|
||||
var account = ddl.get("ll_selfop_outbound_discovery_scope");
|
||||
for (var column : new String[]{"api_budget_json", "api_cooldown_until_utc", "api_pause_reason"}) {
|
||||
assertTrue(account.contains(" " + column + " "), column);
|
||||
}
|
||||
var run = ddl.get("ll_selfop_sync_run");
|
||||
for (var column : new String[]{"execution_purpose", "build_started_at_utc", "superseded_at_utc",
|
||||
"progress_snapshot_json", "heartbeat_at_utc", "inventory_daily_sample_date", "inventory_sample_rule_version"}) {
|
||||
assertTrue(run.contains(" " + column + " "), column);
|
||||
}
|
||||
assertTrue(run.contains("uk_selfop_inventory_daily (source_account,warehouse_code,inventory_daily_sample_date)"));
|
||||
assertTrue(run.contains("CONSTRAINT chk_selfop_inventory_daily CHECK"));
|
||||
assertTrue(ddl.get("ll_selfop_sku_daily_sales").contains("idx_selfop_daily_window (sales_run_id,business_date,customer_code,sku_code)"));
|
||||
assertTrue(ddl.get("ll_selfop_outbound_order").contains("idx_selfop_order_daily (source_account,warehouse_code,sales_eligible,business_date,id)"));
|
||||
assertTrue(ddl.get("ll_selfop_sync_stage").contains("stage_batch_id,object_type,object_key_hash"));
|
||||
assertTrue(ddl.get("ll_selfop_sync_stage").contains("observation_sequence IS NOT NULL"));
|
||||
assertTrue(ddl.get("ll_selfop_outbound_order").contains("source_account,customer_code,delivery_no"));
|
||||
assertTrue(ddl.get("ll_selfop_sku_daily_sales").contains("sales_run_id,source_account,customer_code,warehouse_code,sku_code,business_date"));
|
||||
assertTrue(ddl.get("ll_selfop_sku_profile").contains("profile_run_id,source_account,customer_code,warehouse_code,sku_code"));
|
||||
assertTrue(ddl.get("ll_selfop_inventory_snapshot").contains("snapshot_run_id,source_account,customer_code,warehouse_code,sku_code,product_quality"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void readAndSyncPermissionsAreIndependentWithThreeAuthorizedAdminMenus() throws Exception {
|
||||
var seed = Files.readString(Path.of("../iam/src/main/resources/db/selfoperated-analytics-permissions.sql"));
|
||||
var unified = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql"));
|
||||
assertTrue(unified.contains(seed.strip()));
|
||||
assertTrue(seed.contains("platform:self-operated-analytics:list"));
|
||||
assertTrue(seed.contains("platform:self-operated-analytics:sync"));
|
||||
for (var route : new String[]{"/self-operated/sales", "/self-operated/inventory", "/self-operated/collection"}) {
|
||||
assertTrue(seed.contains(route));
|
||||
}
|
||||
assertTrue(seed.contains("'API',5118100,'platform:self-operated-analytics:list'"));
|
||||
assertTrue(seed.contains("'API',5118130,'platform:self-operated-analytics:sync'"));
|
||||
assertFalse(seed.contains("'MERCHANT'"));
|
||||
assertFalse(seed.contains("INSERT IGNORE"));
|
||||
assertFalse(seed.contains("ON DUPLICATE KEY"));
|
||||
assertTrue(seed.contains("(4000001,5118000,0,"));
|
||||
assertTrue(seed.contains("(4000001,5118001,0,"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user