From 33a2f52d3320a248c01d3652031418b7401880c7 Mon Sep 17 00:00:00 2001 From: trifonovt <87468028+TihomirTrifonov@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:19:39 +0200 Subject: [PATCH] Extend runtime event mixing rules and descriptors --- .../mixing/RuntimeEventDescriptor.java | 48 +++ .../mixing/RuntimeEventDescriptorFactory.java | 301 ++++++++++++++++++ .../mixing/RuntimeEventMixingChannel.java | 9 + .../mixing/RuntimeEventMixingRule.java | 77 +++++ .../RuntimeEventMixingRuleRegistry.java | 94 ++++++ .../mixing/RuntimeResolvedEvent.java | 22 ++ .../mixing/RuntimeResolvedEventRole.java | 12 + 7 files changed, 563 insertions(+) create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventDescriptor.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventDescriptorFactory.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventMixingChannel.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventMixingRule.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventMixingRuleRegistry.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeResolvedEvent.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeResolvedEventRole.java diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventDescriptor.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventDescriptor.java new file mode 100644 index 0000000..2915790 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventDescriptor.java @@ -0,0 +1,48 @@ +package at.procon.eventhub.processing.eventprocessing.mixing; + +import at.procon.eventhub.dto.EventDomain; +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.dto.EventLifecycle; +import at.procon.eventhub.dto.EventType; +import java.time.OffsetDateTime; + +public record RuntimeEventDescriptor( + EventHubEventDto event, + String eventIdentityKey, + String eventKey, + RuntimeEventSourceProfile sourceProfile, + String compatibleActivityKey, + String compatibleSupportEvidenceKey, + boolean driverActivityPoint, + boolean driverCardUsagePoint, + boolean supportEvidenceCandidate +) { + public EventDomain eventDomain() { + return event == null ? null : event.eventDomain(); + } + + public EventType eventType() { + return event == null ? null : event.eventType(); + } + + public EventLifecycle lifecycle() { + return event == null ? null : event.lifecycle(); + } + + public OffsetDateTime occurredAt() { + return event == null ? null : event.occurredAt(); + } + + public String extractionCode() { + return sourceProfile == null ? null : sourceProfile.extractionCode(); + } + + public String keyFor(String equivalenceType) { + return switch (equivalenceType) { + case RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY -> eventKey; + case RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_ACTIVITY_KEY -> compatibleActivityKey; + case RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_SUPPORT_KEY -> compatibleSupportEvidenceKey; + default -> eventKey; + }; + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventDescriptorFactory.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventDescriptorFactory.java new file mode 100644 index 0000000..ddbf339 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventDescriptorFactory.java @@ -0,0 +1,301 @@ +package at.procon.eventhub.processing.eventprocessing.mixing; + +import at.procon.eventhub.dto.EventDomain; +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.dto.EventLifecycle; +import at.procon.eventhub.dto.GeoPointDto; +import at.procon.eventhub.processing.support.RuntimeEntityReferenceResolver; +import at.procon.eventhub.processing.support.RuntimeEventIdentityResolver; +import com.fasterxml.jackson.databind.JsonNode; +import java.time.OffsetDateTime; +import java.util.Comparator; +import java.util.List; +import java.util.Locale; +import java.util.Objects; +import org.springframework.stereotype.Component; + +@Component +public class RuntimeEventDescriptorFactory { + + public List describeSorted(List events) { + return sort(events).stream() + .map(this::describe) + .toList(); + } + + public RuntimeEventDescriptor describe(EventHubEventDto event) { + RuntimeEventSourceProfile profile = sourceProfile(event); + return new RuntimeEventDescriptor( + event, + eventIdentityKey(event), + RuntimeEventIdentityResolver.canonicalEventKey(event), + profile, + compatibleActivityKey(event), + compatibleSupportEvidenceKey(event), + isDriverActivityPoint(event), + isDriverCardUsagePoint(event), + isSupportEvidenceCandidate(event) + ); + } + + public List sort(List events) { + return (events == null ? List.of() : events).stream() + .filter(Objects::nonNull) + .sorted(eventComparator()) + .toList(); + } + + public boolean isDriverActivityPoint(EventHubEventDto event) { + return event != null + && event.eventDomain() == EventDomain.DRIVER_ACTIVITY + && (event.lifecycle() == EventLifecycle.START || event.lifecycle() == EventLifecycle.END) + && event.occurredAt() != null; + } + + public boolean isDriverCardUsagePoint(EventHubEventDto event) { + return event != null + && event.eventDomain() == EventDomain.DRIVER_CARD + && (event.lifecycle() == EventLifecycle.INSERT || event.lifecycle() == EventLifecycle.WITHDRAW) + && event.occurredAt() != null; + } + + public boolean isSupportEvidenceCandidate(EventHubEventDto event) { + return event != null && !isDriverActivityPoint(event) && !isDriverCardUsagePoint(event); + } + + public RuntimeEventSourceProfile sourceProfile(EventHubEventDto event) { + JsonNode raw = rawPayload(event); + String sourceKind = firstNonBlank(text(raw, "sourceKind"), sourceKind(event)); + String extractionCode = firstNonBlank( + text(raw, "extractionCode"), + fileSessionExtractionCode(event, sourceKind), + extractionCodeFromExternalSourceEventId(event) + ); + String sourceSystem = firstNonBlank( + text(raw, "sourceSystem"), + sourceProvider(event), + sourceSystemFromExternalSourceEventId(event) + ); + if (sourceSystem == null && (extractionCode != null || isTachographFileSessionEvent(event))) { + sourceSystem = "TACHOGRAPH"; + } + return new RuntimeEventSourceProfile( + normalizeUpper(sourceSystem), + normalizeUpper(sourceKind), + normalizeUpper(extractionCode) + ); + } + + public String eventIdentityKey(EventHubEventDto event) { + if (event == null) { + return ""; + } + return firstNonBlank( + event.externalSourceEventId(), + event.eventId() == null ? null : event.eventId().toString(), + RuntimeEventIdentityResolver.canonicalEventKey(event) + ); + } + + public Comparator eventComparator() { + return Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(event -> event.eventDomain() == null ? "" : event.eventDomain().name()) + .thenComparing(event -> event.eventType() == null ? "" : event.eventType().name()) + .thenComparing(event -> event.lifecycle() == null ? "" : event.lifecycle().name()) + .thenComparing(event -> sourceProfile(event).extractionCode(), Comparator.nullsLast(String::compareTo)) + .thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo)); + } + + private String fileSessionExtractionCode(EventHubEventDto event, String sourceKind) { + if (!isTachographFileSessionEvent(event)) { + return null; + } + String normalizedSourceKind = normalizeUpper(sourceKind); + if (normalizedSourceKind == null) { + return null; + } + if (event != null && event.eventDomain() == EventDomain.DRIVER_ACTIVITY) { + return switch (normalizedSourceKind) { + case "DRIVER_CARD" -> "CARD_ACTIVITY"; + case "VEHICLE_UNIT" -> "VU_ACTIVITY"; + default -> null; + }; + } + if (event != null && event.eventDomain() == EventDomain.DRIVER_CARD) { + return switch (normalizedSourceKind) { + case "DRIVER_CARD" -> "CARD_VEHICLES_USED"; + case "VEHICLE_UNIT" -> "IW_CYCLE"; + default -> null; + }; + } + if (event == null || event.eventDomain() == null) { + return null; + } + String prefix = switch (normalizedSourceKind) { + case "DRIVER_CARD" -> "CARD"; + case "VEHICLE_UNIT" -> "VU"; + default -> null; + }; + if (prefix == null) { + return null; + } + return switch (event.eventDomain()) { + case POSITION -> prefix + "_POSITION"; + case PLACE -> prefix + "_PLACE"; + case BORDER_CROSSING -> prefix + "_BORDER_CROSSING"; + case SPEEDING -> Objects.equals("VU", prefix) ? "SPEEDING_EVENTS" : null; + default -> null; + }; + } + + private boolean isTachographFileSessionEvent(EventHubEventDto event) { + if (event == null) { + return false; + } + String packageKind = event.sourcePackageRef() == null ? null : normalizeUpper(event.sourcePackageRef().packageKind()); + if (Objects.equals("TACHOGRAPH_FILE_SESSION", packageKind) + || Objects.equals("COMPOSITE_TACHOGRAPH_FILE_SESSION", packageKind)) { + return true; + } + String provider = sourceProvider(event); + if (Objects.equals("TACHOGRAPH_FILE_SESSION", normalizeUpper(provider)) + || Objects.equals("COMPOSITE_TACHOGRAPH_FILE_SESSION", normalizeUpper(provider))) { + return true; + } + String sourceKey = event.packageInfo() == null || event.packageInfo().eventSource() == null + ? null + : normalizeUpper(event.packageInfo().eventSource().sourceKey()); + if (Objects.equals("TACHOGRAPH_FILE_SESSION", sourceKey) + || Objects.equals("COMPOSITE_TACHOGRAPH_FILE_SESSION", sourceKey)) { + return true; + } + String externalId = event.externalSourceEventId(); + return externalId != null + && (externalId.startsWith("TACHOGRAPH_FILE_SESSION:") + || externalId.startsWith("COMPOSITE_TACHOGRAPH_FILE_SESSION:")); + } + + private String compatibleActivityKey(EventHubEventDto event) { + JsonNode raw = rawPayload(event); + return String.join("|", + "ACTIVITY_COMPATIBLE", + nullToEmpty(event == null || event.packageInfo() == null ? null : event.packageInfo().tenantKey()), + nullToEmpty(RuntimeEntityReferenceResolver.driverKey(event)), + nullToEmpty(event == null || event.eventDomain() == null ? null : event.eventDomain().name()), + nullToEmpty(event == null || event.eventType() == null ? null : event.eventType().name()), + nullToEmpty(event == null || event.lifecycle() == null ? null : event.lifecycle().name()), + normalizeTime(event == null ? null : event.occurredAt()), + nullToEmpty(RuntimeEntityReferenceResolver.registrationKey(event)), + nullToEmpty(firstNonBlank(text(raw, "startedAt"), text(raw, "intervalStartedAt"))), + nullToEmpty(firstNonBlank(text(raw, "endedAt"), text(raw, "intervalEndedAt"))), + nullToEmpty(firstNonBlank(text(raw, "slot"), text(raw, "cardSlot"))), + nullToEmpty(text(raw, "cardStatus")), + nullToEmpty(text(raw, "drivingStatus")) + ); + } + + private String compatibleSupportEvidenceKey(EventHubEventDto event) { + JsonNode raw = rawPayload(event); + GeoPointDto position = event == null ? null : event.position(); + return String.join("|", + "SUPPORT_COMPATIBLE", + nullToEmpty(event == null || event.packageInfo() == null ? null : event.packageInfo().tenantKey()), + nullToEmpty(RuntimeEntityReferenceResolver.driverKey(event)), + nullToEmpty(event == null || event.eventDomain() == null ? null : event.eventDomain().name()), + nullToEmpty(event == null || event.eventType() == null ? null : event.eventType().name()), + nullToEmpty(event == null || event.lifecycle() == null ? null : event.lifecycle().name()), + normalizeTime(event == null ? null : event.occurredAt()), + nullToEmpty(RuntimeEntityReferenceResolver.registrationKey(event)), + nullToEmpty(firstNonBlank(text(raw, "latitude"), position == null || position.latitude() == null ? null : position.latitude().toPlainString())), + nullToEmpty(firstNonBlank(text(raw, "longitude"), position == null || position.longitude() == null ? null : position.longitude().toPlainString())), + nullToEmpty(firstNonBlank(text(raw, "odometerM"), event == null || event.odometerM() == null ? null : String.valueOf(event.odometerM()))), + nullToEmpty(firstNonBlank(text(raw, "country"), detailText(event, "country"))), + nullToEmpty(firstNonBlank(text(raw, "region"), detailText(event, "region"))), + nullToEmpty(firstNonBlank(text(raw, "countryFrom"), detailText(event, "countryFrom"))), + nullToEmpty(firstNonBlank(text(raw, "countryTo"), detailText(event, "countryTo"))), + nullToEmpty(firstNonBlank(text(raw, "operation"), detailText(event, "operation"))) + ); + } + + private JsonNode rawPayload(EventHubEventDto event) { + return RuntimeEntityReferenceResolver.rawPayload(event); + } + + private String sourceKind(EventHubEventDto event) { + return event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null + ? null + : event.packageInfo().eventSource().sourceKind(); + } + + private String sourceProvider(EventHubEventDto event) { + return event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null + ? null + : event.packageInfo().eventSource().providerKey(); + } + + private String sourceSystemFromExternalSourceEventId(EventHubEventDto event) { + String externalId = event == null ? null : event.externalSourceEventId(); + if (externalId == null || externalId.isBlank()) { + return null; + } + String[] parts = externalId.split(":"); + return parts.length >= 1 ? parts[0] : null; + } + + private String extractionCodeFromExternalSourceEventId(EventHubEventDto event) { + String externalId = event == null ? null : event.externalSourceEventId(); + if (externalId == null || externalId.isBlank()) { + return null; + } + String[] parts = externalId.split(":"); + return parts.length >= 2 ? parts[1] : null; + } + + private String detailText(EventHubEventDto event, String field) { + if (event == null || event.eventDetails() == null || event.eventDetails().attributes() == null || field == null) { + return null; + } + JsonNode value = event.eventDetails().attributes().get(field); + if (value == null || value.isNull()) { + return null; + } + String text = value.asText(null); + return text == null || text.isBlank() ? null : text.trim(); + } + + private String text(JsonNode node, String field) { + if (node == null || field == null) { + return null; + } + JsonNode value = node.get(field); + if (value == null || value.isNull()) { + return null; + } + String text = value.asText(null); + return text == null || text.isBlank() ? null : text.trim(); + } + + private String firstNonBlank(String... values) { + if (values == null) { + return null; + } + for (String value : values) { + if (value != null && !value.isBlank()) { + return value.trim(); + } + } + return null; + } + + private String normalizeUpper(String value) { + return value == null || value.isBlank() ? null : value.trim().toUpperCase(Locale.ROOT); + } + + private String nullToEmpty(Object value) { + return value == null ? "" : String.valueOf(value); + } + + private String normalizeTime(OffsetDateTime value) { + return value == null ? "" : value.toInstant().toString(); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventMixingChannel.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventMixingChannel.java new file mode 100644 index 0000000..2958cb3 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventMixingChannel.java @@ -0,0 +1,9 @@ +package at.procon.eventhub.processing.eventprocessing.mixing; + +public enum RuntimeEventMixingChannel { + ACTIVITY_TIMELINE, + VEHICLE_USAGE, + SUPPORT_EVIDENCE, + VALIDATION, + AUDIT +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventMixingRule.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventMixingRule.java new file mode 100644 index 0000000..a08fcec --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventMixingRule.java @@ -0,0 +1,77 @@ +package at.procon.eventhub.processing.eventprocessing.mixing; + +import at.procon.eventhub.dto.EventDomain; +import at.procon.eventhub.dto.EventLifecycle; +import at.procon.eventhub.dto.EventType; +import java.util.Set; + +public record RuntimeEventMixingRule( + String ruleId, + RuntimeEventMixingChannel channel, + String equivalenceType, + Set eventDomains, + Set eventTypes, + Set lifecycles, + Set primaryExtractionCodes, + Set secondaryExtractionCodes, + RuntimeResolvedEventRole primaryRole, + RuntimeResolvedEventRole secondaryRole, + String decision, + String reason +) { + public static final String EQUIVALENCE_EXACT_EVENT_KEY = "EXACT_EVENT_KEY"; + public static final String EQUIVALENCE_COMPATIBLE_ACTIVITY_KEY = "COMPATIBLE_ACTIVITY_KEY"; + public static final String EQUIVALENCE_COMPATIBLE_SUPPORT_KEY = "COMPATIBLE_SUPPORT_KEY"; + + public RuntimeEventMixingRule { + eventDomains = eventDomains == null ? Set.of() : Set.copyOf(eventDomains); + eventTypes = eventTypes == null ? Set.of() : Set.copyOf(eventTypes); + lifecycles = lifecycles == null ? Set.of() : Set.copyOf(lifecycles); + primaryExtractionCodes = normalize(primaryExtractionCodes); + secondaryExtractionCodes = normalize(secondaryExtractionCodes); + } + + public boolean matches(RuntimeEventDescriptor descriptor) { + if (descriptor == null || descriptor.event() == null || descriptor.sourceProfile() == null) { + return false; + } + if (!descriptor.sourceProfile().isTachographRuntimeSource()) { + return false; + } + if (!eventDomains.isEmpty() && !eventDomains.contains(descriptor.eventDomain())) { + return false; + } + if (!eventTypes.isEmpty() && !eventTypes.contains(descriptor.eventType())) { + return false; + } + if (!lifecycles.isEmpty() && !lifecycles.contains(descriptor.lifecycle())) { + return false; + } + if (channel == RuntimeEventMixingChannel.ACTIVITY_TIMELINE && !descriptor.driverActivityPoint()) { + return false; + } + if (channel == RuntimeEventMixingChannel.SUPPORT_EVIDENCE && !descriptor.supportEvidenceCandidate()) { + return false; + } + String extractionCode = descriptor.extractionCode(); + return primaryExtractionCodes.contains(extractionCode) || secondaryExtractionCodes.contains(extractionCode); + } + + public boolean isPrimary(RuntimeEventDescriptor descriptor) { + return descriptor != null && primaryExtractionCodes.contains(descriptor.extractionCode()); + } + + public boolean isSecondary(RuntimeEventDescriptor descriptor) { + return descriptor != null && secondaryExtractionCodes.contains(descriptor.extractionCode()); + } + + private static Set normalize(Set values) { + if (values == null || values.isEmpty()) { + return Set.of(); + } + return values.stream() + .filter(value -> value != null && !value.isBlank()) + .map(value -> value.trim().toUpperCase(java.util.Locale.ROOT)) + .collect(java.util.stream.Collectors.toUnmodifiableSet()); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventMixingRuleRegistry.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventMixingRuleRegistry.java new file mode 100644 index 0000000..321bc5e --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeEventMixingRuleRegistry.java @@ -0,0 +1,94 @@ +package at.procon.eventhub.processing.eventprocessing.mixing; + +import at.procon.eventhub.dto.EventDomain; +import at.procon.eventhub.dto.EventLifecycle; +import at.procon.eventhub.dto.EventType; +import java.util.List; +import java.util.Set; +import org.springframework.stereotype.Component; + +@Component +public class RuntimeEventMixingRuleRegistry { + + public List rulesForMode(String mode) { + if (RuntimeEventMixingService.MODE_OFF.equals(mode)) { + return List.of(); + } + return List.of( + tachographCardVuActivityExactEventKey(), + tachographCardVuSupportExactEventKey(), + tachographCardVuActivityCompatibleKey(), + tachographCardVuSupportCompatibleKey() + ); + } + + private RuntimeEventMixingRule tachographCardVuActivityExactEventKey() { + return new RuntimeEventMixingRule( + RuntimeEventMixingService.RULE_TACHOGRAPH_CARD_VU_ACTIVITY_SAME_EVENT_KEY, + RuntimeEventMixingChannel.ACTIVITY_TIMELINE, + RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY, + Set.of(EventDomain.DRIVER_ACTIVITY), + Set.of(EventType.DRIVE, EventType.BREAK_REST, EventType.AVAILABILITY, EventType.WORK, EventType.UNKNOWN_ACTIVITY), + Set.of(EventLifecycle.START, EventLifecycle.END), + Set.of("CARD_ACTIVITY"), + Set.of("VU_ACTIVITY"), + RuntimeResolvedEventRole.FUSED_PRIMARY, + RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE, + "FUSED_PRIMARY_SELECTED", + "CARD_ACTIVITY and VU_ACTIVITY describe the same driver activity point. CARD_ACTIVITY is kept as primary for the activity timeline; VU_ACTIVITY is suppressed from activity intervalization." + ); + } + + private RuntimeEventMixingRule tachographCardVuActivityCompatibleKey() { + return new RuntimeEventMixingRule( + RuntimeEventMixingService.RULE_TACHOGRAPH_CARD_VU_ACTIVITY_COMPATIBLE_KEY, + RuntimeEventMixingChannel.ACTIVITY_TIMELINE, + RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_ACTIVITY_KEY, + Set.of(EventDomain.DRIVER_ACTIVITY), + Set.of(EventType.DRIVE, EventType.BREAK_REST, EventType.AVAILABILITY, EventType.WORK, EventType.UNKNOWN_ACTIVITY), + Set.of(EventLifecycle.START, EventLifecycle.END), + Set.of("CARD_ACTIVITY"), + Set.of("VU_ACTIVITY"), + RuntimeResolvedEventRole.FUSED_PRIMARY, + RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE, + "FUSED_PRIMARY_SELECTED", + "CARD_ACTIVITY and VU_ACTIVITY describe a compatible driver activity point. CARD_ACTIVITY is kept as primary for the activity timeline; VU_ACTIVITY is suppressed from activity intervalization." + ); + } + + private RuntimeEventMixingRule tachographCardVuSupportExactEventKey() { + return new RuntimeEventMixingRule( + RuntimeEventMixingService.RULE_TACHOGRAPH_CARD_VU_SUPPORT_SAME_EVENT_KEY, + RuntimeEventMixingChannel.SUPPORT_EVIDENCE, + RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY, + Set.of(EventDomain.POSITION, EventDomain.PLACE, EventDomain.BORDER_CROSSING), + Set.of(EventType.POSITION_RECORDED, EventType.WORKING_DAY_PLACE_RECORDED, + EventType.BORDER_INBOUND, EventType.BORDER_OUTBOUND, EventType.BORDER_OUT_EU), + Set.of(EventLifecycle.SNAPSHOT, EventLifecycle.INBOUND, EventLifecycle.OUTBOUND, EventLifecycle.OUT_EU), + Set.of("CARD_POSITION", "CARD_PLACE", "CARD_BORDER_CROSSING"), + Set.of("VU_POSITION", "VU_PLACE", "VU_BORDER_CROSSING"), + RuntimeResolvedEventRole.FUSED_PRIMARY, + RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE, + "FUSED_PRIMARY_SELECTED", + "CARD and VU support evidence describe the same semantic event. CARD evidence is kept as primary support evidence; VU evidence is suppressed from support-evidence normalization but retained as audit/corroborating evidence." + ); + } + + private RuntimeEventMixingRule tachographCardVuSupportCompatibleKey() { + return new RuntimeEventMixingRule( + RuntimeEventMixingService.RULE_TACHOGRAPH_CARD_VU_SUPPORT_COMPATIBLE_KEY, + RuntimeEventMixingChannel.SUPPORT_EVIDENCE, + RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_SUPPORT_KEY, + Set.of(EventDomain.POSITION, EventDomain.PLACE, EventDomain.BORDER_CROSSING), + Set.of(EventType.POSITION_RECORDED, EventType.WORKING_DAY_PLACE_RECORDED, + EventType.BORDER_INBOUND, EventType.BORDER_OUTBOUND, EventType.BORDER_OUT_EU), + Set.of(EventLifecycle.SNAPSHOT, EventLifecycle.INBOUND, EventLifecycle.OUTBOUND, EventLifecycle.OUT_EU), + Set.of("CARD_POSITION", "CARD_PLACE", "CARD_BORDER_CROSSING"), + Set.of("VU_POSITION", "VU_PLACE", "VU_BORDER_CROSSING"), + RuntimeResolvedEventRole.FUSED_PRIMARY, + RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE, + "FUSED_PRIMARY_SELECTED", + "CARD and VU support evidence describe a compatible semantic event. CARD evidence is kept as primary support evidence; VU evidence is suppressed from support-evidence normalization. Vehicle/VIN identity from the VU event is copied to the primary event when the card event has weaker vehicle identity." + ); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeResolvedEvent.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeResolvedEvent.java new file mode 100644 index 0000000..abcd996 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeResolvedEvent.java @@ -0,0 +1,22 @@ +package at.procon.eventhub.processing.eventprocessing.mixing; + +import at.procon.eventhub.dto.EventHubEventDto; +import java.util.List; + +public record RuntimeResolvedEvent( + EventHubEventDto event, + RuntimeEventMixingChannel channel, + RuntimeResolvedEventRole role, + String ruleId, + String equivalenceType, + String effectiveEventKey, + String primaryExternalSourceEventId, + List relatedExternalSourceEventIds, + String reason +) { + public RuntimeResolvedEvent { + relatedExternalSourceEventIds = relatedExternalSourceEventIds == null + ? List.of() + : List.copyOf(relatedExternalSourceEventIds); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeResolvedEventRole.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeResolvedEventRole.java new file mode 100644 index 0000000..ebc3f0c --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/mixing/RuntimeResolvedEventRole.java @@ -0,0 +1,12 @@ +package at.procon.eventhub.processing.eventprocessing.mixing; + +public enum RuntimeResolvedEventRole { + PRIMARY, + FUSED_PRIMARY, + SECONDARY_CORROBORATING, + FALLBACK_PRIMARY, + SUPPRESSED_DUPLICATE, + SUPPORT_ONLY, + CONFLICTING_EVIDENCE, + VEHICLE_USAGE_INPUT +}