Extend runtime event mixing diagnostics
This commit is contained in:
parent
74d479454a
commit
3c5ce9a066
|
|
@ -2,41 +2,39 @@
|
||||||
|
|
||||||
## Problem
|
## Problem
|
||||||
|
|
||||||
When a runtime request combined `TACHOGRAPH_FILE_SESSION` and `TACHOGRAPH_DB`, CARD and VU observations of the same tachograph fact were often not fused. The old compatible keys embedded representation-specific values such as tenant metadata, coordinate string scale, country representation, region defaults, vehicle completeness and interval metadata.
|
A runtime request that loads both `TACHOGRAPH_FILE_SESSION` and `TACHOGRAPH_DB` can contain the same tachograph observation twice. The earlier mixing implementation primarily handled CARD versus VU evidence. It did not reliably classify the semantic source role and physical representation of every event, and it did not suppress duplicate observations when both copies had the same source role (for example VU file-session plus VU database serialization).
|
||||||
|
|
||||||
Examples of equivalent values that produced different keys:
|
This produced nearly doubled activity and support-event results in mixed executions.
|
||||||
|
|
||||||
- country `13` versus `D`;
|
## Implementation
|
||||||
- region `0` versus `null`;
|
|
||||||
- longitude `9.3883333333333336` versus `9.388333333333334`;
|
|
||||||
- file-session package tenant `default` versus the DB tenant;
|
|
||||||
- CARD events without VIN versus VU events with VIN.
|
|
||||||
|
|
||||||
## Changes
|
- Added semantic tachograph evidence source roles:
|
||||||
|
- `DRIVER_CARD`
|
||||||
|
- `VEHICLE_UNIT`
|
||||||
|
- `UNKNOWN`
|
||||||
|
- Added physical representation classification:
|
||||||
|
- `FILE_SESSION`
|
||||||
|
- `DATABASE`
|
||||||
|
- `UNKNOWN`
|
||||||
|
- Source-role inference now uses extraction code, raw source metadata, package event-source metadata, package kind and external event identifiers.
|
||||||
|
- Database runtime packages identified by `RUNTIME:TACHOGRAPH:*` take precedence over retained original file-session package metadata.
|
||||||
|
- Existing CARD/VU rules now use semantic source roles, with extraction codes retained only as fallback for unclassified events.
|
||||||
|
- Added same-source-role cross-representation rules for activity and support evidence:
|
||||||
|
- database representation is retained as primary;
|
||||||
|
- matching file-session representation is suppressed as a duplicate;
|
||||||
|
- exact timestamps remain required;
|
||||||
|
- semantic compatibility checks still protect meaningful conflicts.
|
||||||
|
- `CARD_VEHICLES_USED` and `IW_CYCLE` remain untouched by event mixing and continue to be handled by interval-level reconciliation.
|
||||||
|
- Added mixing diagnostics for source roles, representations, candidate groups, rejected compatibility pairs and suppressed events.
|
||||||
|
- Fixed tachograph place SQL so entry type `6` is classified as `START`, consistently with file-session parsing.
|
||||||
|
|
||||||
- Compatible activity and support keys now contain only stable candidate identity:
|
## Tests added/updated
|
||||||
- driver;
|
|
||||||
- domain;
|
|
||||||
- event type;
|
|
||||||
- semantic lifecycle;
|
|
||||||
- exact event timestamp.
|
|
||||||
- Exact timestamp behavior is unchanged.
|
|
||||||
- Added `RuntimeEventEvidenceCompatibilityMatcher` to validate grouped candidates semantically.
|
|
||||||
- Support compatibility normalizes:
|
|
||||||
- tachograph nation numeric/alpha forms;
|
|
||||||
- region `0`/blank/null;
|
|
||||||
- coordinate decimal scale with a `1e-9` serialization tolerance;
|
|
||||||
- registration formatting;
|
|
||||||
- optional VIN, odometer and operation data.
|
|
||||||
- Missing optional data is enrichable, while conflicting meaningful values prevent fusion.
|
|
||||||
- Activity compatibility allows source-specific optional metadata differences while still checking tenant, vehicle/registration and card slot compatibility.
|
|
||||||
- Mixing now evaluates compatibility per primary/secondary pair instead of suppressing every secondary in a broad group.
|
|
||||||
- Internal mixing state now tracks events by object identity and uses the UUID before `externalSourceEventId`, avoiding collisions from repeated source-side IDs such as `CARDPLACE-1`.
|
|
||||||
|
|
||||||
## Tests
|
- Same VU place observation from file session and database is reduced to one event.
|
||||||
|
- Same CARD activity observation from file session and database is reduced to one event.
|
||||||
|
- Database/file-session representation classification remains correct even when DB rows retain original file-session package metadata.
|
||||||
|
- Existing CARD/VU parity and CVU/IW preservation tests remain covered.
|
||||||
|
|
||||||
Added regression coverage for:
|
## Validation
|
||||||
|
|
||||||
- file-session `CARD_PLACE` versus DB `VU_PLACE` with `13`/`D`, `0`/null and decimal-scale differences;
|
The modified Java files were passed through a `javac` parser check. Only expected missing dependency/classpath errors were reported; no Java syntax errors were detected. Maven and a Maven wrapper are unavailable in the execution environment, so the full project test suite was not executed.
|
||||||
- file-session CARD activity versus DB VU activity;
|
|
||||||
- meaningful coordinate conflicts remaining separate.
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ public record RuntimeEventDescriptor(
|
||||||
String eventIdentityKey,
|
String eventIdentityKey,
|
||||||
String eventKey,
|
String eventKey,
|
||||||
RuntimeEventSourceProfile sourceProfile,
|
RuntimeEventSourceProfile sourceProfile,
|
||||||
|
RuntimeTachographEvidenceSourceRole evidenceSourceRole,
|
||||||
|
RuntimeTachographRepresentation representation,
|
||||||
String compatibleActivityKey,
|
String compatibleActivityKey,
|
||||||
String compatibleSupportEvidenceKey,
|
String compatibleSupportEvidenceKey,
|
||||||
boolean driverActivityPoint,
|
boolean driverActivityPoint,
|
||||||
|
|
@ -37,6 +39,18 @@ public record RuntimeEventDescriptor(
|
||||||
return sourceProfile == null ? null : sourceProfile.extractionCode();
|
return sourceProfile == null ? null : sourceProfile.extractionCode();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RuntimeTachographEvidenceSourceRole evidenceSourceRole() {
|
||||||
|
return evidenceSourceRole == null
|
||||||
|
? RuntimeTachographEvidenceSourceRole.UNKNOWN
|
||||||
|
: evidenceSourceRole;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeTachographRepresentation representation() {
|
||||||
|
return representation == null
|
||||||
|
? RuntimeTachographRepresentation.UNKNOWN
|
||||||
|
: representation;
|
||||||
|
}
|
||||||
|
|
||||||
public String keyFor(String equivalenceType) {
|
public String keyFor(String equivalenceType) {
|
||||||
return switch (equivalenceType) {
|
return switch (equivalenceType) {
|
||||||
case RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY -> eventKey;
|
case RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY -> eventKey;
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,8 @@ public class RuntimeEventDescriptorFactory {
|
||||||
eventIdentityKey(event),
|
eventIdentityKey(event),
|
||||||
RuntimeEventIdentityResolver.canonicalEventKey(event),
|
RuntimeEventIdentityResolver.canonicalEventKey(event),
|
||||||
profile,
|
profile,
|
||||||
|
profile.evidenceSourceRole(),
|
||||||
|
profile.representation(),
|
||||||
compatibleActivityKey(event),
|
compatibleActivityKey(event),
|
||||||
compatibleSupportEvidenceKey(event),
|
compatibleSupportEvidenceKey(event),
|
||||||
isDriverActivityPoint(event),
|
isDriverActivityPoint(event),
|
||||||
|
|
|
||||||
|
|
@ -33,6 +33,10 @@ public class RuntimeEventEvidenceCompatibilityMatcher {
|
||||||
if (rule == null || primary == null || secondary == null) {
|
if (rule == null || primary == null || secondary == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (rule.requireSameSourceRole()
|
||||||
|
&& primary.evidenceSourceRole() != secondary.evidenceSourceRole()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return switch (rule.equivalenceType()) {
|
return switch (rule.equivalenceType()) {
|
||||||
case RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY -> true;
|
case RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY -> true;
|
||||||
case RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_ACTIVITY_KEY ->
|
case RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_ACTIVITY_KEY ->
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package at.procon.eventhub.processing.eventprocessing.mixing;
|
||||||
|
|
||||||
|
/** Classification and rule-application counters for one event-mixing execution. */
|
||||||
|
public record RuntimeEventMixingDiagnostics(
|
||||||
|
int describedEventCount,
|
||||||
|
int tachographEventCount,
|
||||||
|
int driverCardSourceRoleCount,
|
||||||
|
int vehicleUnitSourceRoleCount,
|
||||||
|
int unknownSourceRoleCount,
|
||||||
|
int databaseRepresentationCount,
|
||||||
|
int fileSessionRepresentationCount,
|
||||||
|
int unknownRepresentationCount,
|
||||||
|
int candidateGroupCount,
|
||||||
|
int compatibilityRejectedCount,
|
||||||
|
int suppressedEventCount
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,11 @@ public record RuntimeEventMixingRule(
|
||||||
Set<EventLifecycle> lifecycles,
|
Set<EventLifecycle> lifecycles,
|
||||||
Set<String> primaryExtractionCodes,
|
Set<String> primaryExtractionCodes,
|
||||||
Set<String> secondaryExtractionCodes,
|
Set<String> secondaryExtractionCodes,
|
||||||
|
Set<RuntimeTachographEvidenceSourceRole> primarySourceRoles,
|
||||||
|
Set<RuntimeTachographEvidenceSourceRole> secondarySourceRoles,
|
||||||
|
Set<RuntimeTachographRepresentation> primaryRepresentations,
|
||||||
|
Set<RuntimeTachographRepresentation> secondaryRepresentations,
|
||||||
|
boolean requireSameSourceRole,
|
||||||
RuntimeResolvedEventRole primaryRole,
|
RuntimeResolvedEventRole primaryRole,
|
||||||
RuntimeResolvedEventRole secondaryRole,
|
RuntimeResolvedEventRole secondaryRole,
|
||||||
String decision,
|
String decision,
|
||||||
|
|
@ -29,6 +34,10 @@ public record RuntimeEventMixingRule(
|
||||||
lifecycles = lifecycles == null ? Set.of() : Set.copyOf(lifecycles);
|
lifecycles = lifecycles == null ? Set.of() : Set.copyOf(lifecycles);
|
||||||
primaryExtractionCodes = normalize(primaryExtractionCodes);
|
primaryExtractionCodes = normalize(primaryExtractionCodes);
|
||||||
secondaryExtractionCodes = normalize(secondaryExtractionCodes);
|
secondaryExtractionCodes = normalize(secondaryExtractionCodes);
|
||||||
|
primarySourceRoles = primarySourceRoles == null ? Set.of() : Set.copyOf(primarySourceRoles);
|
||||||
|
secondarySourceRoles = secondarySourceRoles == null ? Set.of() : Set.copyOf(secondarySourceRoles);
|
||||||
|
primaryRepresentations = primaryRepresentations == null ? Set.of() : Set.copyOf(primaryRepresentations);
|
||||||
|
secondaryRepresentations = secondaryRepresentations == null ? Set.of() : Set.copyOf(secondaryRepresentations);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean matches(RuntimeEventDescriptor descriptor) {
|
public boolean matches(RuntimeEventDescriptor descriptor) {
|
||||||
|
|
@ -53,16 +62,55 @@ public record RuntimeEventMixingRule(
|
||||||
if (channel == RuntimeEventMixingChannel.SUPPORT_EVIDENCE && !descriptor.supportEvidenceCandidate()) {
|
if (channel == RuntimeEventMixingChannel.SUPPORT_EVIDENCE && !descriptor.supportEvidenceCandidate()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
String extractionCode = descriptor.extractionCode();
|
return isPrimary(descriptor) || isSecondary(descriptor);
|
||||||
return primaryExtractionCodes.contains(extractionCode) || secondaryExtractionCodes.contains(extractionCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isPrimary(RuntimeEventDescriptor descriptor) {
|
public boolean isPrimary(RuntimeEventDescriptor descriptor) {
|
||||||
return descriptor != null && primaryExtractionCodes.contains(descriptor.extractionCode());
|
return sideMatches(
|
||||||
|
descriptor,
|
||||||
|
primaryExtractionCodes,
|
||||||
|
primarySourceRoles,
|
||||||
|
primaryRepresentations
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isSecondary(RuntimeEventDescriptor descriptor) {
|
public boolean isSecondary(RuntimeEventDescriptor descriptor) {
|
||||||
return descriptor != null && secondaryExtractionCodes.contains(descriptor.extractionCode());
|
return sideMatches(
|
||||||
|
descriptor,
|
||||||
|
secondaryExtractionCodes,
|
||||||
|
secondarySourceRoles,
|
||||||
|
secondaryRepresentations
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean sideMatches(
|
||||||
|
RuntimeEventDescriptor descriptor,
|
||||||
|
Set<String> extractionCodes,
|
||||||
|
Set<RuntimeTachographEvidenceSourceRole> sourceRoles,
|
||||||
|
Set<RuntimeTachographRepresentation> representations
|
||||||
|
) {
|
||||||
|
if (descriptor == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!representations.isEmpty() && !representations.contains(descriptor.representation())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sourceRoles.isEmpty()) {
|
||||||
|
RuntimeTachographEvidenceSourceRole role = descriptor.evidenceSourceRole();
|
||||||
|
if (role != RuntimeTachographEvidenceSourceRole.UNKNOWN) {
|
||||||
|
return sourceRoles.contains(role);
|
||||||
|
}
|
||||||
|
// Extraction code is a fallback only when the semantic source role could not be
|
||||||
|
// resolved. A conflicting explicit role must never make one event both primary and
|
||||||
|
// secondary.
|
||||||
|
return !extractionCodes.isEmpty()
|
||||||
|
&& extractionCodes.contains(normalize(descriptor.extractionCode()));
|
||||||
|
}
|
||||||
|
if (!extractionCodes.isEmpty()) {
|
||||||
|
return extractionCodes.contains(normalize(descriptor.extractionCode()));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static Set<String> normalize(Set<String> values) {
|
private static Set<String> normalize(Set<String> values) {
|
||||||
|
|
@ -71,7 +119,13 @@ public record RuntimeEventMixingRule(
|
||||||
}
|
}
|
||||||
return values.stream()
|
return values.stream()
|
||||||
.filter(value -> value != null && !value.isBlank())
|
.filter(value -> value != null && !value.isBlank())
|
||||||
.map(value -> value.trim().toUpperCase(java.util.Locale.ROOT))
|
.map(RuntimeEventMixingRule::normalize)
|
||||||
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
.collect(java.util.stream.Collectors.toUnmodifiableSet());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static String normalize(String value) {
|
||||||
|
return value == null || value.isBlank()
|
||||||
|
? null
|
||||||
|
: value.trim().toUpperCase(java.util.Locale.ROOT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,14 @@ public class RuntimeEventMixingRuleRegistry {
|
||||||
EventLifecycle.OUT_EU
|
EventLifecycle.OUT_EU
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private static final Set<EventType> ACTIVITY_EVENT_TYPES = Set.of(
|
||||||
|
EventType.DRIVE,
|
||||||
|
EventType.BREAK_REST,
|
||||||
|
EventType.AVAILABILITY,
|
||||||
|
EventType.WORK,
|
||||||
|
EventType.UNKNOWN_ACTIVITY
|
||||||
|
);
|
||||||
|
|
||||||
private static final Set<String> CARD_SUPPORT_EXTRACTION_CODES = Set.of(
|
private static final Set<String> CARD_SUPPORT_EXTRACTION_CODES = Set.of(
|
||||||
"CARD_POSITION",
|
"CARD_POSITION",
|
||||||
"CARD_PLACE",
|
"CARD_PLACE",
|
||||||
|
|
@ -57,11 +65,18 @@ public class RuntimeEventMixingRuleRegistry {
|
||||||
"VU_SPECIFIC_CONDITION"
|
"VU_SPECIFIC_CONDITION"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
private static final Set<RuntimeTachographEvidenceSourceRole> BOTH_TACHOGRAPH_ROLES = Set.of(
|
||||||
|
RuntimeTachographEvidenceSourceRole.DRIVER_CARD,
|
||||||
|
RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT
|
||||||
|
);
|
||||||
|
|
||||||
public List<RuntimeEventMixingRule> rulesForMode(String mode) {
|
public List<RuntimeEventMixingRule> rulesForMode(String mode) {
|
||||||
if (RuntimeEventMixingService.MODE_OFF.equals(mode)) {
|
if (RuntimeEventMixingService.MODE_OFF.equals(mode)) {
|
||||||
return List.of();
|
return List.of();
|
||||||
}
|
}
|
||||||
return List.of(
|
return List.of(
|
||||||
|
tachographDbFileSessionSameRoleActivityCompatibleKey(),
|
||||||
|
tachographDbFileSessionSameRoleSupportCompatibleKey(),
|
||||||
tachographCardVuActivityExactEventKey(),
|
tachographCardVuActivityExactEventKey(),
|
||||||
tachographCardVuSupportExactEventKey(),
|
tachographCardVuSupportExactEventKey(),
|
||||||
tachographCardVuActivityCompatibleKey(),
|
tachographCardVuActivityCompatibleKey(),
|
||||||
|
|
@ -69,16 +84,65 @@ public class RuntimeEventMixingRuleRegistry {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RuntimeEventMixingRule tachographDbFileSessionSameRoleActivityCompatibleKey() {
|
||||||
|
return new RuntimeEventMixingRule(
|
||||||
|
RuntimeEventMixingService.RULE_TACHOGRAPH_DB_FILE_SESSION_ACTIVITY_SAME_ROLE,
|
||||||
|
RuntimeEventMixingChannel.ACTIVITY_TIMELINE,
|
||||||
|
RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_ACTIVITY_KEY,
|
||||||
|
Set.of(EventDomain.DRIVER_ACTIVITY),
|
||||||
|
ACTIVITY_EVENT_TYPES,
|
||||||
|
Set.of(EventLifecycle.START, EventLifecycle.END),
|
||||||
|
Set.of(),
|
||||||
|
Set.of(),
|
||||||
|
BOTH_TACHOGRAPH_ROLES,
|
||||||
|
BOTH_TACHOGRAPH_ROLES,
|
||||||
|
Set.of(RuntimeTachographRepresentation.DATABASE),
|
||||||
|
Set.of(RuntimeTachographRepresentation.FILE_SESSION),
|
||||||
|
true,
|
||||||
|
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
||||||
|
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
||||||
|
"CROSS_REPRESENTATION_DUPLICATE_SUPPRESSED",
|
||||||
|
"The tachograph database and file-session representations describe the same activity point from the same source role. The database representation is retained and the file-session representation is suppressed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeEventMixingRule tachographDbFileSessionSameRoleSupportCompatibleKey() {
|
||||||
|
return new RuntimeEventMixingRule(
|
||||||
|
RuntimeEventMixingService.RULE_TACHOGRAPH_DB_FILE_SESSION_SUPPORT_SAME_ROLE,
|
||||||
|
RuntimeEventMixingChannel.SUPPORT_EVIDENCE,
|
||||||
|
RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_SUPPORT_KEY,
|
||||||
|
SUPPORT_EVENT_DOMAINS,
|
||||||
|
SUPPORT_EVENT_TYPES,
|
||||||
|
SUPPORT_EVENT_LIFECYCLES,
|
||||||
|
Set.of(),
|
||||||
|
Set.of(),
|
||||||
|
BOTH_TACHOGRAPH_ROLES,
|
||||||
|
BOTH_TACHOGRAPH_ROLES,
|
||||||
|
Set.of(RuntimeTachographRepresentation.DATABASE),
|
||||||
|
Set.of(RuntimeTachographRepresentation.FILE_SESSION),
|
||||||
|
true,
|
||||||
|
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
||||||
|
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
||||||
|
"CROSS_REPRESENTATION_DUPLICATE_SUPPRESSED",
|
||||||
|
"The tachograph database and file-session representations describe the same support event from the same source role. The database representation is retained and the file-session representation is suppressed."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private RuntimeEventMixingRule tachographCardVuActivityExactEventKey() {
|
private RuntimeEventMixingRule tachographCardVuActivityExactEventKey() {
|
||||||
return new RuntimeEventMixingRule(
|
return new RuntimeEventMixingRule(
|
||||||
RuntimeEventMixingService.RULE_TACHOGRAPH_CARD_VU_ACTIVITY_SAME_EVENT_KEY,
|
RuntimeEventMixingService.RULE_TACHOGRAPH_CARD_VU_ACTIVITY_SAME_EVENT_KEY,
|
||||||
RuntimeEventMixingChannel.ACTIVITY_TIMELINE,
|
RuntimeEventMixingChannel.ACTIVITY_TIMELINE,
|
||||||
RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY,
|
RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY,
|
||||||
Set.of(EventDomain.DRIVER_ACTIVITY),
|
Set.of(EventDomain.DRIVER_ACTIVITY),
|
||||||
Set.of(EventType.DRIVE, EventType.BREAK_REST, EventType.AVAILABILITY, EventType.WORK, EventType.UNKNOWN_ACTIVITY),
|
ACTIVITY_EVENT_TYPES,
|
||||||
Set.of(EventLifecycle.START, EventLifecycle.END),
|
Set.of(EventLifecycle.START, EventLifecycle.END),
|
||||||
Set.of("CARD_ACTIVITY"),
|
Set.of("CARD_ACTIVITY"),
|
||||||
Set.of("VU_ACTIVITY"),
|
Set.of("VU_ACTIVITY"),
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.DRIVER_CARD),
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT),
|
||||||
|
Set.of(),
|
||||||
|
Set.of(),
|
||||||
|
false,
|
||||||
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
||||||
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
||||||
"FUSED_PRIMARY_SELECTED",
|
"FUSED_PRIMARY_SELECTED",
|
||||||
|
|
@ -92,10 +156,15 @@ public class RuntimeEventMixingRuleRegistry {
|
||||||
RuntimeEventMixingChannel.ACTIVITY_TIMELINE,
|
RuntimeEventMixingChannel.ACTIVITY_TIMELINE,
|
||||||
RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_ACTIVITY_KEY,
|
RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_ACTIVITY_KEY,
|
||||||
Set.of(EventDomain.DRIVER_ACTIVITY),
|
Set.of(EventDomain.DRIVER_ACTIVITY),
|
||||||
Set.of(EventType.DRIVE, EventType.BREAK_REST, EventType.AVAILABILITY, EventType.WORK, EventType.UNKNOWN_ACTIVITY),
|
ACTIVITY_EVENT_TYPES,
|
||||||
Set.of(EventLifecycle.START, EventLifecycle.END),
|
Set.of(EventLifecycle.START, EventLifecycle.END),
|
||||||
Set.of("CARD_ACTIVITY"),
|
Set.of("CARD_ACTIVITY"),
|
||||||
Set.of("VU_ACTIVITY"),
|
Set.of("VU_ACTIVITY"),
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.DRIVER_CARD),
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT),
|
||||||
|
Set.of(),
|
||||||
|
Set.of(),
|
||||||
|
false,
|
||||||
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
||||||
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
||||||
"FUSED_PRIMARY_SELECTED",
|
"FUSED_PRIMARY_SELECTED",
|
||||||
|
|
@ -113,6 +182,11 @@ public class RuntimeEventMixingRuleRegistry {
|
||||||
SUPPORT_EVENT_LIFECYCLES,
|
SUPPORT_EVENT_LIFECYCLES,
|
||||||
CARD_SUPPORT_EXTRACTION_CODES,
|
CARD_SUPPORT_EXTRACTION_CODES,
|
||||||
VU_SUPPORT_EXTRACTION_CODES,
|
VU_SUPPORT_EXTRACTION_CODES,
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.DRIVER_CARD),
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT),
|
||||||
|
Set.of(),
|
||||||
|
Set.of(),
|
||||||
|
false,
|
||||||
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
||||||
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
||||||
"FUSED_PRIMARY_SELECTED",
|
"FUSED_PRIMARY_SELECTED",
|
||||||
|
|
@ -130,6 +204,11 @@ public class RuntimeEventMixingRuleRegistry {
|
||||||
SUPPORT_EVENT_LIFECYCLES,
|
SUPPORT_EVENT_LIFECYCLES,
|
||||||
CARD_SUPPORT_EXTRACTION_CODES,
|
CARD_SUPPORT_EXTRACTION_CODES,
|
||||||
VU_SUPPORT_EXTRACTION_CODES,
|
VU_SUPPORT_EXTRACTION_CODES,
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.DRIVER_CARD),
|
||||||
|
Set.of(RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT),
|
||||||
|
Set.of(),
|
||||||
|
Set.of(),
|
||||||
|
false,
|
||||||
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
RuntimeResolvedEventRole.FUSED_PRIMARY,
|
||||||
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
|
||||||
"FUSED_PRIMARY_SELECTED",
|
"FUSED_PRIMARY_SELECTED",
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,10 @@ public class RuntimeEventMixingService {
|
||||||
"tachograph.support.card-vu.same-event-key";
|
"tachograph.support.card-vu.same-event-key";
|
||||||
public static final String RULE_TACHOGRAPH_CARD_VU_SUPPORT_COMPATIBLE_KEY =
|
public static final String RULE_TACHOGRAPH_CARD_VU_SUPPORT_COMPATIBLE_KEY =
|
||||||
"tachograph.support.card-vu.compatible-support-key";
|
"tachograph.support.card-vu.compatible-support-key";
|
||||||
|
public static final String RULE_TACHOGRAPH_DB_FILE_SESSION_ACTIVITY_SAME_ROLE =
|
||||||
|
"tachograph.activity.db-file-session.same-source-role";
|
||||||
|
public static final String RULE_TACHOGRAPH_DB_FILE_SESSION_SUPPORT_SAME_ROLE =
|
||||||
|
"tachograph.support.db-file-session.same-source-role";
|
||||||
|
|
||||||
private final RuntimeEventDescriptorFactory descriptorFactory;
|
private final RuntimeEventDescriptorFactory descriptorFactory;
|
||||||
private final RuntimeEventMixingRuleRegistry ruleRegistry;
|
private final RuntimeEventMixingRuleRegistry ruleRegistry;
|
||||||
|
|
@ -87,6 +91,7 @@ public class RuntimeEventMixingService {
|
||||||
.toList();
|
.toList();
|
||||||
|
|
||||||
List<RuntimeResolvedEvent> resolvedEvents = buildResolvedEvents(state, rawEvents);
|
List<RuntimeResolvedEvent> resolvedEvents = buildResolvedEvents(state, rawEvents);
|
||||||
|
RuntimeEventMixingDiagnostics diagnostics = diagnostics(descriptors, state);
|
||||||
List<String> notes = new ArrayList<>();
|
List<String> notes = new ArrayList<>();
|
||||||
notes.add("Runtime event mixing inspected " + rawEvents.size() + " event(s).");
|
notes.add("Runtime event mixing inspected " + rawEvents.size() + " event(s).");
|
||||||
notes.add("Runtime event mixing applied " + ruleRegistry.rulesForMode(mode).size() + " configured rule(s) in mode " + mode + ".");
|
notes.add("Runtime event mixing applied " + ruleRegistry.rulesForMode(mode).size() + " configured rule(s) in mode " + mode + ".");
|
||||||
|
|
@ -94,6 +99,12 @@ public class RuntimeEventMixingService {
|
||||||
+ " duplicate source event(s) from activity/support evidence channels.");
|
+ " duplicate source event(s) from activity/support evidence channels.");
|
||||||
notes.add("Runtime event mixing keeps CARD_POSITION, CARD_PLACE, and CARD_BORDER_CROSSING as primary when matching VU support evidence describes the same semantic event.");
|
notes.add("Runtime event mixing keeps CARD_POSITION, CARD_PLACE, and CARD_BORDER_CROSSING as primary when matching VU support evidence describes the same semantic event.");
|
||||||
notes.add("Runtime event mixing kept all CARD_VEHICLES_USED and IW_CYCLE card-usage point events unchanged for vehicle-usage processing.");
|
notes.add("Runtime event mixing kept all CARD_VEHICLES_USED and IW_CYCLE card-usage point events unchanged for vehicle-usage processing.");
|
||||||
|
notes.add("Runtime event mixing classified " + diagnostics.driverCardSourceRoleCount()
|
||||||
|
+ " DRIVER_CARD and " + diagnostics.vehicleUnitSourceRoleCount()
|
||||||
|
+ " VEHICLE_UNIT event(s); unknown source roles=" + diagnostics.unknownSourceRoleCount() + ".");
|
||||||
|
notes.add("Runtime event mixing classified " + diagnostics.databaseRepresentationCount()
|
||||||
|
+ " database and " + diagnostics.fileSessionRepresentationCount()
|
||||||
|
+ " file-session representation event(s).");
|
||||||
return new RuntimeMixedEventBundle(
|
return new RuntimeMixedEventBundle(
|
||||||
rawEvents,
|
rawEvents,
|
||||||
driverPartitionEvents,
|
driverPartitionEvents,
|
||||||
|
|
@ -103,6 +114,7 @@ public class RuntimeEventMixingService {
|
||||||
state.suppressedEvents(),
|
state.suppressedEvents(),
|
||||||
resolvedEvents,
|
resolvedEvents,
|
||||||
state.decisions(),
|
state.decisions(),
|
||||||
|
diagnostics,
|
||||||
notes,
|
notes,
|
||||||
state.warnings()
|
state.warnings()
|
||||||
);
|
);
|
||||||
|
|
@ -118,6 +130,7 @@ public class RuntimeEventMixingService {
|
||||||
List.of(),
|
List.of(),
|
||||||
descriptors.stream().map(this::defaultResolvedEvent).toList(),
|
descriptors.stream().map(this::defaultResolvedEvent).toList(),
|
||||||
List.of(),
|
List.of(),
|
||||||
|
diagnostics(descriptors, new MixingState(descriptors)),
|
||||||
List.of(note),
|
List.of(note),
|
||||||
List.of()
|
List.of()
|
||||||
);
|
);
|
||||||
|
|
@ -157,12 +170,20 @@ public class RuntimeEventMixingService {
|
||||||
if (primaries.isEmpty() || secondaries.isEmpty()) {
|
if (primaries.isEmpty() || secondaries.isEmpty()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
state.incrementCandidateGroupCount();
|
||||||
|
|
||||||
for (RuntimeEventDescriptor primary : primaries) {
|
for (RuntimeEventDescriptor primary : primaries) {
|
||||||
List<RuntimeEventDescriptor> compatibleSecondaries = secondaries.stream()
|
List<RuntimeEventDescriptor> compatibleSecondaries = new ArrayList<>();
|
||||||
.filter(descriptor -> !state.isSuppressed(descriptor))
|
for (RuntimeEventDescriptor secondary : secondaries) {
|
||||||
.filter(descriptor -> compatibilityMatcher.compatible(rule, primary, descriptor))
|
if (state.isSuppressed(secondary)) {
|
||||||
.toList();
|
continue;
|
||||||
|
}
|
||||||
|
if (compatibilityMatcher.compatible(rule, primary, secondary)) {
|
||||||
|
compatibleSecondaries.add(secondary);
|
||||||
|
} else {
|
||||||
|
state.incrementCompatibilityRejectedCount();
|
||||||
|
}
|
||||||
|
}
|
||||||
if (compatibleSecondaries.isEmpty()) {
|
if (compatibleSecondaries.isEmpty()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -220,6 +241,54 @@ public class RuntimeEventMixingService {
|
||||||
return List.copyOf(resolved);
|
return List.copyOf(resolved);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private RuntimeEventMixingDiagnostics diagnostics(
|
||||||
|
List<RuntimeEventDescriptor> descriptors,
|
||||||
|
MixingState state
|
||||||
|
) {
|
||||||
|
List<RuntimeEventDescriptor> safeDescriptors = descriptors == null ? List.of() : descriptors;
|
||||||
|
int tachographEventCount = (int) safeDescriptors.stream()
|
||||||
|
.filter(descriptor -> descriptor.sourceProfile() != null
|
||||||
|
&& descriptor.sourceProfile().isTachographRuntimeSource())
|
||||||
|
.count();
|
||||||
|
int driverCardCount = (int) safeDescriptors.stream()
|
||||||
|
.filter(descriptor -> descriptor.evidenceSourceRole()
|
||||||
|
== RuntimeTachographEvidenceSourceRole.DRIVER_CARD)
|
||||||
|
.count();
|
||||||
|
int vehicleUnitCount = (int) safeDescriptors.stream()
|
||||||
|
.filter(descriptor -> descriptor.evidenceSourceRole()
|
||||||
|
== RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT)
|
||||||
|
.count();
|
||||||
|
int unknownRoleCount = (int) safeDescriptors.stream()
|
||||||
|
.filter(descriptor -> descriptor.evidenceSourceRole()
|
||||||
|
== RuntimeTachographEvidenceSourceRole.UNKNOWN)
|
||||||
|
.count();
|
||||||
|
int databaseCount = (int) safeDescriptors.stream()
|
||||||
|
.filter(descriptor -> descriptor.representation()
|
||||||
|
== RuntimeTachographRepresentation.DATABASE)
|
||||||
|
.count();
|
||||||
|
int fileSessionCount = (int) safeDescriptors.stream()
|
||||||
|
.filter(descriptor -> descriptor.representation()
|
||||||
|
== RuntimeTachographRepresentation.FILE_SESSION)
|
||||||
|
.count();
|
||||||
|
int unknownRepresentationCount = (int) safeDescriptors.stream()
|
||||||
|
.filter(descriptor -> descriptor.representation()
|
||||||
|
== RuntimeTachographRepresentation.UNKNOWN)
|
||||||
|
.count();
|
||||||
|
return new RuntimeEventMixingDiagnostics(
|
||||||
|
safeDescriptors.size(),
|
||||||
|
tachographEventCount,
|
||||||
|
driverCardCount,
|
||||||
|
vehicleUnitCount,
|
||||||
|
unknownRoleCount,
|
||||||
|
databaseCount,
|
||||||
|
fileSessionCount,
|
||||||
|
unknownRepresentationCount,
|
||||||
|
state == null ? 0 : state.candidateGroupCount(),
|
||||||
|
state == null ? 0 : state.compatibilityRejectedCount(),
|
||||||
|
state == null ? 0 : state.suppressedEvents().size()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private RuntimeResolvedEvent defaultResolvedEvent(RuntimeEventDescriptor descriptor) {
|
private RuntimeResolvedEvent defaultResolvedEvent(RuntimeEventDescriptor descriptor) {
|
||||||
RuntimeEventMixingChannel channel = defaultChannel(descriptor);
|
RuntimeEventMixingChannel channel = defaultChannel(descriptor);
|
||||||
RuntimeResolvedEventRole role = switch (channel) {
|
RuntimeResolvedEventRole role = switch (channel) {
|
||||||
|
|
@ -369,6 +438,8 @@ public class RuntimeEventMixingService {
|
||||||
private final List<EventHubEventDto> suppressedEvents = new ArrayList<>();
|
private final List<EventHubEventDto> suppressedEvents = new ArrayList<>();
|
||||||
private final List<RuntimeEventMixingDecisionDto> decisions = new ArrayList<>();
|
private final List<RuntimeEventMixingDecisionDto> decisions = new ArrayList<>();
|
||||||
private final List<String> warnings = new ArrayList<>();
|
private final List<String> warnings = new ArrayList<>();
|
||||||
|
private int candidateGroupCount;
|
||||||
|
private int compatibilityRejectedCount;
|
||||||
|
|
||||||
private MixingState(List<RuntimeEventDescriptor> descriptors) {
|
private MixingState(List<RuntimeEventDescriptor> descriptors) {
|
||||||
this.descriptors = descriptors == null ? List.of() : List.copyOf(descriptors);
|
this.descriptors = descriptors == null ? List.of() : List.copyOf(descriptors);
|
||||||
|
|
@ -486,6 +557,22 @@ public class RuntimeEventMixingService {
|
||||||
return decisions;
|
return decisions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void incrementCandidateGroupCount() {
|
||||||
|
candidateGroupCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int candidateGroupCount() {
|
||||||
|
return candidateGroupCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void incrementCompatibilityRejectedCount() {
|
||||||
|
compatibilityRejectedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int compatibilityRejectedCount() {
|
||||||
|
return compatibilityRejectedCount;
|
||||||
|
}
|
||||||
|
|
||||||
private List<String> warnings() {
|
private List<String> warnings() {
|
||||||
return warnings;
|
return warnings;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,8 +3,34 @@ package at.procon.eventhub.processing.eventprocessing.mixing;
|
||||||
public record RuntimeEventSourceProfile(
|
public record RuntimeEventSourceProfile(
|
||||||
String sourceSystem,
|
String sourceSystem,
|
||||||
String sourceKind,
|
String sourceKind,
|
||||||
String extractionCode
|
String extractionCode,
|
||||||
|
RuntimeTachographEvidenceSourceRole evidenceSourceRole,
|
||||||
|
RuntimeTachographRepresentation representation
|
||||||
) {
|
) {
|
||||||
|
/** Compatibility constructor retained for existing tests and direct callers. */
|
||||||
|
public RuntimeEventSourceProfile(
|
||||||
|
String sourceSystem,
|
||||||
|
String sourceKind,
|
||||||
|
String extractionCode
|
||||||
|
) {
|
||||||
|
this(
|
||||||
|
sourceSystem,
|
||||||
|
sourceKind,
|
||||||
|
extractionCode,
|
||||||
|
RuntimeTachographEvidenceSourceRole.UNKNOWN,
|
||||||
|
RuntimeTachographRepresentation.UNKNOWN
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeEventSourceProfile {
|
||||||
|
evidenceSourceRole = evidenceSourceRole == null
|
||||||
|
? RuntimeTachographEvidenceSourceRole.UNKNOWN
|
||||||
|
: evidenceSourceRole;
|
||||||
|
representation = representation == null
|
||||||
|
? RuntimeTachographRepresentation.UNKNOWN
|
||||||
|
: representation;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isTachographRuntimeSource() {
|
public boolean isTachographRuntimeSource() {
|
||||||
return switch (sourceSystem == null ? "" : sourceSystem) {
|
return switch (sourceSystem == null ? "" : sourceSystem) {
|
||||||
case "TACHOGRAPH", "TACHOGRAPH_FILE_SESSION", "COMPOSITE_TACHOGRAPH_FILE_SESSION" -> true;
|
case "TACHOGRAPH", "TACHOGRAPH_FILE_SESSION", "COMPOSITE_TACHOGRAPH_FILE_SESSION" -> true;
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ public record RuntimeMixedEventBundle(
|
||||||
List<EventHubEventDto> suppressedEvents,
|
List<EventHubEventDto> suppressedEvents,
|
||||||
List<RuntimeResolvedEvent> resolvedEvents,
|
List<RuntimeResolvedEvent> resolvedEvents,
|
||||||
List<RuntimeEventMixingDecisionDto> eventMixingDecisions,
|
List<RuntimeEventMixingDecisionDto> eventMixingDecisions,
|
||||||
|
RuntimeEventMixingDiagnostics diagnostics,
|
||||||
List<String> notes,
|
List<String> notes,
|
||||||
List<String> warnings
|
List<String> warnings
|
||||||
) {
|
) {
|
||||||
|
|
@ -24,6 +25,9 @@ public record RuntimeMixedEventBundle(
|
||||||
suppressedEvents = suppressedEvents == null ? List.of() : List.copyOf(suppressedEvents);
|
suppressedEvents = suppressedEvents == null ? List.of() : List.copyOf(suppressedEvents);
|
||||||
resolvedEvents = resolvedEvents == null ? List.of() : List.copyOf(resolvedEvents);
|
resolvedEvents = resolvedEvents == null ? List.of() : List.copyOf(resolvedEvents);
|
||||||
eventMixingDecisions = eventMixingDecisions == null ? List.of() : List.copyOf(eventMixingDecisions);
|
eventMixingDecisions = eventMixingDecisions == null ? List.of() : List.copyOf(eventMixingDecisions);
|
||||||
|
diagnostics = diagnostics == null
|
||||||
|
? new RuntimeEventMixingDiagnostics(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)
|
||||||
|
: diagnostics;
|
||||||
notes = notes == null ? List.of() : List.copyOf(notes);
|
notes = notes == null ? List.of() : List.copyOf(notes);
|
||||||
warnings = warnings == null ? List.of() : List.copyOf(warnings);
|
warnings = warnings == null ? List.of() : List.copyOf(warnings);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,9 @@ import org.springframework.stereotype.Component;
|
||||||
/**
|
/**
|
||||||
* Canonical semantic view of tachograph runtime events.
|
* Canonical semantic view of tachograph runtime events.
|
||||||
*
|
*
|
||||||
* <p>The same tachograph package can be processed directly from an in-memory file session or
|
* <p>The same tachograph package can be processed directly from a file session or loaded from
|
||||||
* loaded from the tachograph database after serialization. This component normalizes only the
|
* the tachograph database after serialization. This component normalizes only representation
|
||||||
* representation differences that are relevant to downstream mixing. It deliberately leaves the
|
* differences relevant to runtime mixing and leaves the original event untouched.</p>
|
||||||
* original {@link EventHubEventDto} untouched for provenance and audit purposes.</p>
|
|
||||||
*/
|
*/
|
||||||
@Component
|
@Component
|
||||||
public class RuntimeTachographEventSemantics {
|
public class RuntimeTachographEventSemantics {
|
||||||
|
|
@ -51,24 +50,49 @@ public class RuntimeTachographEventSemantics {
|
||||||
text(raw, "extractionCode"),
|
text(raw, "extractionCode"),
|
||||||
extractionCodeFromExternalSourceEventId(event)
|
extractionCodeFromExternalSourceEventId(event)
|
||||||
));
|
));
|
||||||
String sourceKind = normalizeUpper(firstNonBlank(
|
|
||||||
|
String sourceKindCandidate = normalizeUpper(firstNonBlank(
|
||||||
text(raw, "sourceKind"),
|
text(raw, "sourceKind"),
|
||||||
sourceKind(event),
|
sourceKind(event),
|
||||||
|
sourceKey(event),
|
||||||
|
sourcePackageKind(event),
|
||||||
sourceKindFromExtractionCode(explicitExtractionCode)
|
sourceKindFromExtractionCode(explicitExtractionCode)
|
||||||
));
|
));
|
||||||
|
RuntimeTachographEvidenceSourceRole sourceRole = evidenceSourceRole(
|
||||||
|
sourceKindCandidate,
|
||||||
|
explicitExtractionCode,
|
||||||
|
event
|
||||||
|
);
|
||||||
|
String sourceKind = canonicalSourceKind(sourceRole, sourceKindCandidate);
|
||||||
|
|
||||||
String extractionCode = normalizeUpper(firstNonBlank(
|
String extractionCode = normalizeUpper(firstNonBlank(
|
||||||
explicitExtractionCode,
|
explicitExtractionCode,
|
||||||
inferExtractionCode(event, sourceKind)
|
inferExtractionCode(event, sourceRole)
|
||||||
));
|
));
|
||||||
|
if (sourceRole == RuntimeTachographEvidenceSourceRole.UNKNOWN) {
|
||||||
|
sourceRole = evidenceSourceRole(sourceKind, extractionCode, event);
|
||||||
|
sourceKind = canonicalSourceKind(sourceRole, sourceKind);
|
||||||
|
}
|
||||||
|
|
||||||
String sourceSystemCandidate = normalizeUpper(firstNonBlank(
|
String sourceSystemCandidate = normalizeUpper(firstNonBlank(
|
||||||
text(raw, "sourceSystem"),
|
text(raw, "sourceSystem"),
|
||||||
sourceProvider(event),
|
sourceProvider(event),
|
||||||
sourceSystemFromExternalSourceEventId(event)
|
sourceSystemFromExternalSourceEventId(event),
|
||||||
|
sourcePackageKind(event)
|
||||||
));
|
));
|
||||||
String sourceSystem = isTachographRepresentation(event, sourceSystemCandidate, extractionCode)
|
boolean tachograph = isTachographRepresentation(event, sourceSystemCandidate, extractionCode);
|
||||||
? "TACHOGRAPH"
|
String sourceSystem = tachograph ? "TACHOGRAPH" : sourceSystemCandidate;
|
||||||
: sourceSystemCandidate;
|
RuntimeTachographRepresentation representation = tachograph
|
||||||
return new RuntimeEventSourceProfile(sourceSystem, sourceKind, extractionCode);
|
? representation(event)
|
||||||
|
: RuntimeTachographRepresentation.UNKNOWN;
|
||||||
|
|
||||||
|
return new RuntimeEventSourceProfile(
|
||||||
|
sourceSystem,
|
||||||
|
sourceKind,
|
||||||
|
extractionCode,
|
||||||
|
sourceRole,
|
||||||
|
representation
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -87,30 +111,33 @@ public class RuntimeTachographEventSemantics {
|
||||||
}
|
}
|
||||||
|
|
||||||
public String inferExtractionCode(EventHubEventDto event, String sourceKind) {
|
public String inferExtractionCode(EventHubEventDto event, String sourceKind) {
|
||||||
if (event == null || event.eventDomain() == null) {
|
return inferExtractionCode(event, evidenceSourceRole(sourceKind, null, event));
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
String normalizedSourceKind = normalizeUpper(sourceKind);
|
|
||||||
if (normalizedSourceKind == null) {
|
public String inferExtractionCode(
|
||||||
|
EventHubEventDto event,
|
||||||
|
RuntimeTachographEvidenceSourceRole sourceRole
|
||||||
|
) {
|
||||||
|
if (event == null || event.eventDomain() == null || sourceRole == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (event.eventDomain() == EventDomain.DRIVER_ACTIVITY) {
|
if (event.eventDomain() == EventDomain.DRIVER_ACTIVITY) {
|
||||||
return switch (normalizedSourceKind) {
|
return switch (sourceRole) {
|
||||||
case "DRIVER_CARD" -> "CARD_ACTIVITY";
|
case DRIVER_CARD -> "CARD_ACTIVITY";
|
||||||
case "VEHICLE_UNIT" -> "VU_ACTIVITY";
|
case VEHICLE_UNIT -> "VU_ACTIVITY";
|
||||||
default -> null;
|
default -> null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (event.eventDomain() == EventDomain.DRIVER_CARD) {
|
if (event.eventDomain() == EventDomain.DRIVER_CARD) {
|
||||||
return switch (normalizedSourceKind) {
|
return switch (sourceRole) {
|
||||||
case "DRIVER_CARD" -> "CARD_VEHICLES_USED";
|
case DRIVER_CARD -> "CARD_VEHICLES_USED";
|
||||||
case "VEHICLE_UNIT" -> "IW_CYCLE";
|
case VEHICLE_UNIT -> "IW_CYCLE";
|
||||||
default -> null;
|
default -> null;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
String prefix = switch (normalizedSourceKind) {
|
String prefix = switch (sourceRole) {
|
||||||
case "DRIVER_CARD" -> "CARD";
|
case DRIVER_CARD -> "CARD";
|
||||||
case "VEHICLE_UNIT" -> "VU";
|
case VEHICLE_UNIT -> "VU";
|
||||||
default -> null;
|
default -> null;
|
||||||
};
|
};
|
||||||
if (prefix == null) {
|
if (prefix == null) {
|
||||||
|
|
@ -127,9 +154,128 @@ public class RuntimeTachographEventSemantics {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RuntimeTachographEvidenceSourceRole evidenceSourceRole(EventHubEventDto event) {
|
||||||
|
return sourceProfile(event).evidenceSourceRole();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RuntimeTachographRepresentation representation(EventHubEventDto event) {
|
||||||
|
String externalId = normalizeUpper(event == null ? null : event.externalSourceEventId());
|
||||||
|
String packageKind = normalizeUpper(sourcePackageKind(event));
|
||||||
|
String externalPackageId = normalizeUpper(externalPackageId(event));
|
||||||
|
String rawSourceSystem = normalizeUpper(text(rawPayload(event), "sourceSystem"));
|
||||||
|
String provider = normalizeUpper(sourceProvider(event));
|
||||||
|
String sourceKey = normalizeUpper(sourceKey(event));
|
||||||
|
|
||||||
|
// The runtime DB loader intentionally keeps the original source-package metadata.
|
||||||
|
// Therefore RUNTIME:TACHOGRAPH must take precedence over packageKind markers that may
|
||||||
|
// still say TACHOGRAPH_FILE_SESSION after the package has been serialized in the DB.
|
||||||
|
if (startsWithAny(externalPackageId, "RUNTIME:TACHOGRAPH:")) {
|
||||||
|
return RuntimeTachographRepresentation.DATABASE;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startsWithAny(externalId,
|
||||||
|
"TACHOGRAPH_FILE_SESSION:",
|
||||||
|
"COMPOSITE_TACHOGRAPH_FILE_SESSION:")
|
||||||
|
|| containsFileSessionMarker(externalPackageId)
|
||||||
|
|| containsFileSessionMarker(rawSourceSystem)
|
||||||
|
|| containsFileSessionMarker(provider)
|
||||||
|
|| containsFileSessionMarker(sourceKey)
|
||||||
|
|| containsFileSessionMarker(packageKind)) {
|
||||||
|
return RuntimeTachographRepresentation.FILE_SESSION;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startsWithAny(externalId, "TACHOGRAPH:")
|
||||||
|
|| TACHOGRAPH_SOURCE_SYSTEMS.contains(nullToEmpty(normalizeUpper(sourceProvider(event))))
|
||||||
|
|| KNOWN_EXTRACTION_CODES.contains(nullToEmpty(normalizeUpper(
|
||||||
|
firstNonBlank(
|
||||||
|
text(rawPayload(event), "extractionCode"),
|
||||||
|
extractionCodeFromExternalSourceEventId(event)
|
||||||
|
)
|
||||||
|
)))) {
|
||||||
|
return RuntimeTachographRepresentation.DATABASE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return RuntimeTachographRepresentation.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isTachographRepresentation(EventHubEventDto event) {
|
public boolean isTachographRepresentation(EventHubEventDto event) {
|
||||||
RuntimeEventSourceProfile profile = sourceProfile(event);
|
return sourceProfile(event).isTachographRuntimeSource();
|
||||||
return profile.isTachographRuntimeSource();
|
}
|
||||||
|
|
||||||
|
private RuntimeTachographEvidenceSourceRole evidenceSourceRole(
|
||||||
|
String sourceKind,
|
||||||
|
String extractionCode,
|
||||||
|
EventHubEventDto event
|
||||||
|
) {
|
||||||
|
String normalizedExtraction = normalizeUpper(extractionCode);
|
||||||
|
if (normalizedExtraction != null) {
|
||||||
|
if (normalizedExtraction.startsWith("CARD_")) {
|
||||||
|
return RuntimeTachographEvidenceSourceRole.DRIVER_CARD;
|
||||||
|
}
|
||||||
|
if (normalizedExtraction.startsWith("VU_")
|
||||||
|
|| Objects.equals("IW_CYCLE", normalizedExtraction)
|
||||||
|
|| Objects.equals("SPEEDING_EVENTS", normalizedExtraction)) {
|
||||||
|
return RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String[] candidates = {
|
||||||
|
sourceKind,
|
||||||
|
text(rawPayload(event), "sourceKind"),
|
||||||
|
sourceKind(event),
|
||||||
|
sourceKey(event),
|
||||||
|
sourcePackageKind(event)
|
||||||
|
};
|
||||||
|
for (String value : candidates) {
|
||||||
|
RuntimeTachographEvidenceSourceRole resolved = roleFromToken(value);
|
||||||
|
if (resolved != RuntimeTachographEvidenceSourceRole.UNKNOWN) {
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String externalId = normalizeUpper(event == null ? null : event.externalSourceEventId());
|
||||||
|
if (externalId != null) {
|
||||||
|
if (externalId.contains(":CARD_") || externalId.contains(":CARDPLACE-")
|
||||||
|
|| externalId.contains(":CARDGNSS-")) {
|
||||||
|
return RuntimeTachographEvidenceSourceRole.DRIVER_CARD;
|
||||||
|
}
|
||||||
|
if (externalId.contains(":VU_") || externalId.contains(":VUPLACE-")
|
||||||
|
|| externalId.contains(":VUGNSS-")) {
|
||||||
|
return RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return RuntimeTachographEvidenceSourceRole.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private RuntimeTachographEvidenceSourceRole roleFromToken(String value) {
|
||||||
|
String candidate = normalizeUpper(value);
|
||||||
|
if (candidate == null) {
|
||||||
|
return RuntimeTachographEvidenceSourceRole.UNKNOWN;
|
||||||
|
}
|
||||||
|
if (candidate.equals("DRIVER_CARD")
|
||||||
|
|| candidate.equals("CARD")
|
||||||
|
|| candidate.contains("DRIVER_CARD")
|
||||||
|
|| candidate.startsWith("CARD_")) {
|
||||||
|
return RuntimeTachographEvidenceSourceRole.DRIVER_CARD;
|
||||||
|
}
|
||||||
|
if (candidate.equals("VEHICLE_UNIT")
|
||||||
|
|| candidate.equals("VU")
|
||||||
|
|| candidate.contains("VEHICLE_UNIT")
|
||||||
|
|| candidate.startsWith("VU_")) {
|
||||||
|
return RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT;
|
||||||
|
}
|
||||||
|
return RuntimeTachographEvidenceSourceRole.UNKNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String canonicalSourceKind(
|
||||||
|
RuntimeTachographEvidenceSourceRole role,
|
||||||
|
String fallback
|
||||||
|
) {
|
||||||
|
return switch (role == null ? RuntimeTachographEvidenceSourceRole.UNKNOWN : role) {
|
||||||
|
case DRIVER_CARD -> "DRIVER_CARD";
|
||||||
|
case VEHICLE_UNIT -> "VEHICLE_UNIT";
|
||||||
|
case UNKNOWN -> normalizeUpper(fallback);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isTachographRepresentation(
|
private boolean isTachographRepresentation(
|
||||||
|
|
@ -143,15 +289,12 @@ public class RuntimeTachographEventSemantics {
|
||||||
if (KNOWN_EXTRACTION_CODES.contains(nullToEmpty(extractionCode))) {
|
if (KNOWN_EXTRACTION_CODES.contains(nullToEmpty(extractionCode))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
String packageKind = event == null || event.sourcePackageRef() == null
|
String packageKind = normalizeUpper(sourcePackageKind(event));
|
||||||
? null
|
if (TACHOGRAPH_SOURCE_SYSTEMS.contains(nullToEmpty(packageKind))
|
||||||
: normalizeUpper(event.sourcePackageRef().packageKind());
|
|| containsFileSessionMarker(packageKind)) {
|
||||||
if (TACHOGRAPH_SOURCE_SYSTEMS.contains(nullToEmpty(packageKind))) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
String sourceKey = event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null
|
String sourceKey = normalizeUpper(sourceKey(event));
|
||||||
? null
|
|
||||||
: normalizeUpper(event.packageInfo().eventSource().sourceKey());
|
|
||||||
if (sourceKey != null && sourceKey.startsWith("TACHOGRAPH")) {
|
if (sourceKey != null && sourceKey.startsWith("TACHOGRAPH")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
@ -172,12 +315,29 @@ public class RuntimeTachographEventSemantics {
|
||||||
: event.packageInfo().eventSource().sourceKind();
|
: event.packageInfo().eventSource().sourceKind();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String sourceKey(EventHubEventDto event) {
|
||||||
|
return event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null
|
||||||
|
? null
|
||||||
|
: event.packageInfo().eventSource().sourceKey();
|
||||||
|
}
|
||||||
|
|
||||||
private String sourceProvider(EventHubEventDto event) {
|
private String sourceProvider(EventHubEventDto event) {
|
||||||
return event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null
|
return event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null
|
||||||
? null
|
? null
|
||||||
: event.packageInfo().eventSource().providerKey();
|
: event.packageInfo().eventSource().providerKey();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String sourcePackageKind(EventHubEventDto event) {
|
||||||
|
String fromRef = event == null || event.sourcePackageRef() == null
|
||||||
|
? null
|
||||||
|
: event.sourcePackageRef().packageKind();
|
||||||
|
return firstNonBlank(fromRef, text(rawPayload(event), "sourcePackageKind"));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String externalPackageId(EventHubEventDto event) {
|
||||||
|
return event == null || event.packageInfo() == null ? null : event.packageInfo().externalPackageId();
|
||||||
|
}
|
||||||
|
|
||||||
private String sourceSystemFromExternalSourceEventId(EventHubEventDto event) {
|
private String sourceSystemFromExternalSourceEventId(EventHubEventDto event) {
|
||||||
String externalId = event == null ? null : event.externalSourceEventId();
|
String externalId = event == null ? null : event.externalSourceEventId();
|
||||||
if (externalId == null || externalId.isBlank()) {
|
if (externalId == null || externalId.isBlank()) {
|
||||||
|
|
@ -201,18 +361,24 @@ public class RuntimeTachographEventSemantics {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String sourceKindFromExtractionCode(String extractionCode) {
|
private String sourceKindFromExtractionCode(String extractionCode) {
|
||||||
String normalized = normalizeUpper(extractionCode);
|
RuntimeTachographEvidenceSourceRole role = evidenceSourceRole(null, extractionCode, null);
|
||||||
if (normalized == null) {
|
return canonicalSourceKind(role, null);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
if (normalized.startsWith("CARD_")) {
|
|
||||||
return "DRIVER_CARD";
|
private boolean containsFileSessionMarker(String value) {
|
||||||
|
return value != null && value.contains("FILE_SESSION");
|
||||||
}
|
}
|
||||||
if (normalized.startsWith("VU_") || Objects.equals("IW_CYCLE", normalized)
|
|
||||||
|| Objects.equals("SPEEDING_EVENTS", normalized)) {
|
private boolean startsWithAny(String value, String... prefixes) {
|
||||||
return "VEHICLE_UNIT";
|
if (value == null || prefixes == null) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
return null;
|
for (String prefix : prefixes) {
|
||||||
|
if (prefix != null && value.startsWith(prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String text(JsonNode node, String field) {
|
private String text(JsonNode node, String field) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package at.procon.eventhub.processing.eventprocessing.mixing;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Semantic tachograph evidence side used by runtime mixing rules.
|
||||||
|
*
|
||||||
|
* <p>This is intentionally independent from the physical representation. The same
|
||||||
|
* driver-card or vehicle-unit fact can be loaded from a file session or from the
|
||||||
|
* tachograph database.</p>
|
||||||
|
*/
|
||||||
|
public enum RuntimeTachographEvidenceSourceRole {
|
||||||
|
DRIVER_CARD,
|
||||||
|
VEHICLE_UNIT,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package at.procon.eventhub.processing.eventprocessing.mixing;
|
||||||
|
|
||||||
|
/** Physical representation from which a tachograph runtime event was loaded. */
|
||||||
|
public enum RuntimeTachographRepresentation {
|
||||||
|
FILE_SESSION,
|
||||||
|
DATABASE,
|
||||||
|
UNKNOWN
|
||||||
|
}
|
||||||
|
|
@ -39,7 +39,7 @@ public class EventEvidenceMixingModule implements RuntimeProcessingModule {
|
||||||
return new RuntimeProcessingModuleDescriptorDto(
|
return new RuntimeProcessingModuleDescriptorDto(
|
||||||
moduleKey(),
|
moduleKey(),
|
||||||
"Event evidence mixing",
|
"Event evidence mixing",
|
||||||
"Applies source-aware runtime evidence rules before intervalization. The rule registry currently collapses duplicate tachograph card/VU activity, position, place, and border evidence while keeping CARD_VEHICLES_USED/IW_CYCLE unchanged for vehicle-usage processing.",
|
"Applies source-aware runtime evidence rules before intervalization. The rule registry collapses duplicate tachograph card/VU evidence and duplicate file-session/database representations while keeping CARD_VEHICLES_USED/IW_CYCLE unchanged for vehicle-usage processing.",
|
||||||
"JAVA",
|
"JAVA",
|
||||||
Set.of(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY),
|
Set.of(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY),
|
||||||
Set.of("UnifiedRuntimeEventBundle"),
|
Set.of("UnifiedRuntimeEventBundle"),
|
||||||
|
|
@ -64,6 +64,15 @@ public class EventEvidenceMixingModule implements RuntimeProcessingModule {
|
||||||
metadata.put("resolvedEventCount", mixed.resolvedEvents().size());
|
metadata.put("resolvedEventCount", mixed.resolvedEvents().size());
|
||||||
metadata.put("eventMixingDecisionCount", mixed.eventMixingDecisions().size());
|
metadata.put("eventMixingDecisionCount", mixed.eventMixingDecisions().size());
|
||||||
metadata.put("eventMixingMode", eventMixingMode(context));
|
metadata.put("eventMixingMode", eventMixingMode(context));
|
||||||
|
metadata.put("tachographEventCount", mixed.diagnostics().tachographEventCount());
|
||||||
|
metadata.put("driverCardSourceRoleCount", mixed.diagnostics().driverCardSourceRoleCount());
|
||||||
|
metadata.put("vehicleUnitSourceRoleCount", mixed.diagnostics().vehicleUnitSourceRoleCount());
|
||||||
|
metadata.put("unknownSourceRoleCount", mixed.diagnostics().unknownSourceRoleCount());
|
||||||
|
metadata.put("databaseRepresentationCount", mixed.diagnostics().databaseRepresentationCount());
|
||||||
|
metadata.put("fileSessionRepresentationCount", mixed.diagnostics().fileSessionRepresentationCount());
|
||||||
|
metadata.put("unknownRepresentationCount", mixed.diagnostics().unknownRepresentationCount());
|
||||||
|
metadata.put("candidateGroupCount", mixed.diagnostics().candidateGroupCount());
|
||||||
|
metadata.put("compatibilityRejectedCount", mixed.diagnostics().compatibilityRejectedCount());
|
||||||
return new RuntimeProcessingModuleResult(
|
return new RuntimeProcessingModuleResult(
|
||||||
moduleKey(),
|
moduleKey(),
|
||||||
RuntimeProcessingModuleStatus.SUCCESS,
|
RuntimeProcessingModuleStatus.SUCCESS,
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ select
|
||||||
base.country,
|
base.country,
|
||||||
base.region,
|
base.region,
|
||||||
'WORKING_DAY_PLACE_RECORDED' as event_type,
|
'WORKING_DAY_PLACE_RECORDED' as event_type,
|
||||||
case when base.entry_type in (0, 2, 4) then 'START' else 'END' end as lifecycle,
|
case when base.entry_type in (0, 2, 4, 6) then 'START' else 'END' end as lifecycle,
|
||||||
cast(case when base.entry_type in (2, 3, 4, 5) then 1 else 0 end as bit) as manual_entry,
|
cast(case when base.entry_type in (2, 3, 4, 5) then 1 else 0 end as bit) as manual_entry,
|
||||||
|
|
||||||
cast(base.driver_id as varchar(128)) as driver_source_entity_id,
|
cast(base.driver_id as varchar(128)) as driver_source_entity_id,
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ select
|
||||||
base.country,
|
base.country,
|
||||||
base.region,
|
base.region,
|
||||||
'WORKING_DAY_PLACE_RECORDED' as event_type,
|
'WORKING_DAY_PLACE_RECORDED' as event_type,
|
||||||
case when base.entry_type in (0, 2, 4) then 'START' else 'END' end as lifecycle,
|
case when base.entry_type in (0, 2, 4, 6) then 'START' else 'END' end as lifecycle,
|
||||||
cast(case when base.entry_type in (2, 3, 4, 5) then 1 else 0 end as bit) as manual_entry,
|
cast(case when base.entry_type in (2, 3, 4, 5) then 1 else 0 end as bit) as manual_entry,
|
||||||
|
|
||||||
cast(base.driver_id as varchar(128)) as driver_source_entity_id,
|
cast(base.driver_id as varchar(128)) as driver_source_entity_id,
|
||||||
|
|
|
||||||
|
|
@ -491,6 +491,77 @@ class RuntimeEventMixingServiceTest {
|
||||||
.isEqualTo(RuntimeEventMixingService.RULE_TACHOGRAPH_CARD_VU_ACTIVITY_COMPATIBLE_KEY);
|
.isEqualTo(RuntimeEventMixingService.RULE_TACHOGRAPH_CARD_VU_ACTIVITY_COMPATIBLE_KEY);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void suppressesFileSessionVuPlaceWhenSameVuFactIsLoadedFromDatabase() {
|
||||||
|
OffsetDateTime occurredAt = OffsetDateTime.parse("2026-04-01T04:30:59Z");
|
||||||
|
EventHubEventDto fileVu = sameRoleCrossRepresentationPlace(
|
||||||
|
true,
|
||||||
|
"VEHICLE_UNIT",
|
||||||
|
"TACHOGRAPH_FILE_SESSION:vu-place-28",
|
||||||
|
occurredAt,
|
||||||
|
"13",
|
||||||
|
"0",
|
||||||
|
new BigDecimal("50.6400000000000000"),
|
||||||
|
new BigDecimal("9.3883333333333336")
|
||||||
|
);
|
||||||
|
EventHubEventDto databaseVu = sameRoleCrossRepresentationPlace(
|
||||||
|
false,
|
||||||
|
"VEHICLE_UNIT",
|
||||||
|
"4693459",
|
||||||
|
occurredAt,
|
||||||
|
"D",
|
||||||
|
null,
|
||||||
|
new BigDecimal("50.64"),
|
||||||
|
new BigDecimal("9.388333333333334")
|
||||||
|
);
|
||||||
|
|
||||||
|
RuntimeMixedEventBundle mixed = service.mix(
|
||||||
|
List.of(fileVu, databaseVu),
|
||||||
|
RuntimeEventMixingService.MODE_TACHOGRAPH_SAME_SOURCE
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(mixed.supportEvidenceEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
|
.containsExactly(databaseVu.externalSourceEventId());
|
||||||
|
assertThat(mixed.suppressedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
|
.containsExactly(fileVu.externalSourceEventId());
|
||||||
|
assertThat(mixed.eventMixingDecisions()).hasSize(1);
|
||||||
|
assertThat(mixed.eventMixingDecisions().getFirst().ruleId())
|
||||||
|
.isEqualTo(RuntimeEventMixingService.RULE_TACHOGRAPH_DB_FILE_SESSION_SUPPORT_SAME_ROLE);
|
||||||
|
assertThat(mixed.diagnostics().vehicleUnitSourceRoleCount()).isEqualTo(2);
|
||||||
|
assertThat(mixed.diagnostics().databaseRepresentationCount()).isEqualTo(1);
|
||||||
|
assertThat(mixed.diagnostics().fileSessionRepresentationCount()).isEqualTo(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void suppressesFileSessionCardActivityWhenSameCardFactIsLoadedFromDatabase() {
|
||||||
|
OffsetDateTime occurredAt = OffsetDateTime.parse("2026-04-01T05:22:00Z");
|
||||||
|
EventHubEventDto fileCard = sameRoleCrossRepresentationActivity(
|
||||||
|
true,
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"TACHOGRAPH_FILE_SESSION:card-activity-1",
|
||||||
|
occurredAt
|
||||||
|
);
|
||||||
|
EventHubEventDto databaseCard = sameRoleCrossRepresentationActivity(
|
||||||
|
false,
|
||||||
|
"DRIVER_CARD",
|
||||||
|
"116710708",
|
||||||
|
occurredAt
|
||||||
|
);
|
||||||
|
|
||||||
|
RuntimeMixedEventBundle mixed = service.mix(
|
||||||
|
List.of(fileCard, databaseCard),
|
||||||
|
RuntimeEventMixingService.MODE_TACHOGRAPH_SAME_SOURCE
|
||||||
|
);
|
||||||
|
|
||||||
|
assertThat(mixed.activityTimelineEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
|
.containsExactly(databaseCard.externalSourceEventId());
|
||||||
|
assertThat(mixed.suppressedEvents()).extracting(EventHubEventDto::externalSourceEventId)
|
||||||
|
.containsExactly(fileCard.externalSourceEventId());
|
||||||
|
assertThat(mixed.eventMixingDecisions()).hasSize(1);
|
||||||
|
assertThat(mixed.eventMixingDecisions().getFirst().ruleId())
|
||||||
|
.isEqualTo(RuntimeEventMixingService.RULE_TACHOGRAPH_DB_FILE_SESSION_ACTIVITY_SAME_ROLE);
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void keepsCrossRepresentationSupportEventsWhenMeaningfulCoordinatesConflict() {
|
void keepsCrossRepresentationSupportEventsWhenMeaningfulCoordinatesConflict() {
|
||||||
OffsetDateTime occurredAt = OffsetDateTime.parse("2026-04-01T04:30:59Z");
|
OffsetDateTime occurredAt = OffsetDateTime.parse("2026-04-01T04:30:59Z");
|
||||||
|
|
@ -524,6 +595,108 @@ class RuntimeEventMixingServiceTest {
|
||||||
assertThat(mixed.eventMixingDecisions()).isEmpty();
|
assertThat(mixed.eventMixingDecisions()).isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private EventHubEventDto sameRoleCrossRepresentationPlace(
|
||||||
|
boolean fileSession,
|
||||||
|
String sourceKind,
|
||||||
|
String externalId,
|
||||||
|
OffsetDateTime occurredAt,
|
||||||
|
String country,
|
||||||
|
String region,
|
||||||
|
BigDecimal latitude,
|
||||||
|
BigDecimal longitude
|
||||||
|
) {
|
||||||
|
ObjectNode raw = fileSession
|
||||||
|
? baseRawWithoutExtraction(sourceKind)
|
||||||
|
: baseRaw(sourceKind.equals("DRIVER_CARD") ? "CARD_PLACE" : "VU_PLACE", sourceKind);
|
||||||
|
raw.put("latitude", latitude.toPlainString());
|
||||||
|
raw.put("longitude", longitude.toPlainString());
|
||||||
|
raw.put("odometerM", "172945000");
|
||||||
|
raw.put("country", country);
|
||||||
|
if (region != null) {
|
||||||
|
raw.put("region", region);
|
||||||
|
}
|
||||||
|
ObjectNode payload = JsonNodeFactory.instance.objectNode();
|
||||||
|
payload.set("raw", raw);
|
||||||
|
ObjectNode attributes = JsonNodeFactory.instance.objectNode();
|
||||||
|
attributes.put("country", country);
|
||||||
|
if (region != null) {
|
||||||
|
attributes.put("region", region);
|
||||||
|
}
|
||||||
|
VehicleRefDto vehicleRef = fileSession
|
||||||
|
? new VehicleRefDto(null, null, null, new VehicleRegistrationRefDto("13", 13, "RO BS 2219"))
|
||||||
|
: new VehicleRefDto("3342", "XLRTEH4300G376073", null, new VehicleRegistrationRefDto("D", 13, "RO BS 2219"));
|
||||||
|
return new EventHubEventDto(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
externalId,
|
||||||
|
new DriverRefDto("13:DF000358328840", new DriverCardRefDto("13", 13, "DF000358328840")),
|
||||||
|
vehicleRef,
|
||||||
|
occurredAt,
|
||||||
|
null,
|
||||||
|
occurredAt,
|
||||||
|
EventDomain.PLACE,
|
||||||
|
EventType.WORKING_DAY_PLACE_RECORDED,
|
||||||
|
fileSession ? EventLifecycle.BEGIN : EventLifecycle.START,
|
||||||
|
172945000L,
|
||||||
|
new at.procon.eventhub.dto.GeoPointDto(latitude, longitude),
|
||||||
|
new EventDetailsDto("PLACE", attributes),
|
||||||
|
null,
|
||||||
|
payload,
|
||||||
|
false,
|
||||||
|
packageInfo(
|
||||||
|
fileSession ? "default" : "TENANT_A",
|
||||||
|
fileSession ? "TACHOGRAPH_FILE_SESSION" : "TACHOGRAPH",
|
||||||
|
sourceKind,
|
||||||
|
EventDomain.PLACE,
|
||||||
|
occurredAt.toLocalDate()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private EventHubEventDto sameRoleCrossRepresentationActivity(
|
||||||
|
boolean fileSession,
|
||||||
|
String sourceKind,
|
||||||
|
String externalId,
|
||||||
|
OffsetDateTime occurredAt
|
||||||
|
) {
|
||||||
|
String extractionCode = sourceKind.equals("DRIVER_CARD") ? "CARD_ACTIVITY" : "VU_ACTIVITY";
|
||||||
|
ObjectNode raw = fileSession
|
||||||
|
? baseRawWithoutExtraction(sourceKind)
|
||||||
|
: baseRaw(extractionCode, sourceKind);
|
||||||
|
raw.put("activityType", "DRIVE");
|
||||||
|
raw.put("cardSlot", "DRIVER");
|
||||||
|
raw.put("startedAt", occurredAt.toString());
|
||||||
|
raw.put("endedAt", occurredAt.plusMinutes(30).toString());
|
||||||
|
ObjectNode payload = JsonNodeFactory.instance.objectNode();
|
||||||
|
payload.set("raw", raw);
|
||||||
|
ObjectNode attributes = JsonNodeFactory.instance.objectNode();
|
||||||
|
attributes.put("cardSlot", "DRIVER");
|
||||||
|
return new EventHubEventDto(
|
||||||
|
UUID.randomUUID(),
|
||||||
|
externalId,
|
||||||
|
new DriverRefDto("13:DF000358328840", new DriverCardRefDto("13", 13, "DF000358328840")),
|
||||||
|
new VehicleRefDto(null, null, null, new VehicleRegistrationRefDto("13", 13, "RO BS 2219")),
|
||||||
|
occurredAt,
|
||||||
|
null,
|
||||||
|
occurredAt,
|
||||||
|
EventDomain.DRIVER_ACTIVITY,
|
||||||
|
EventType.DRIVE,
|
||||||
|
EventLifecycle.START,
|
||||||
|
null,
|
||||||
|
null,
|
||||||
|
new EventDetailsDto("DRIVER_ACTIVITY", attributes),
|
||||||
|
null,
|
||||||
|
payload,
|
||||||
|
false,
|
||||||
|
packageInfo(
|
||||||
|
fileSession ? "default" : "TENANT_A",
|
||||||
|
fileSession ? "TACHOGRAPH_FILE_SESSION" : "TACHOGRAPH",
|
||||||
|
sourceKind,
|
||||||
|
EventDomain.DRIVER_ACTIVITY,
|
||||||
|
occurredAt.toLocalDate()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private EventHubEventDto crossRepresentationPlace(
|
private EventHubEventDto crossRepresentationPlace(
|
||||||
boolean fileSessionCard,
|
boolean fileSessionCard,
|
||||||
String externalId,
|
String externalId,
|
||||||
|
|
|
||||||
|
|
@ -52,12 +52,20 @@ class RuntimeTachographRepresentationParityTest {
|
||||||
RuntimeEventDescriptor dbDescriptor = descriptorFactory.describe(dbEvent);
|
RuntimeEventDescriptor dbDescriptor = descriptorFactory.describe(dbEvent);
|
||||||
RuntimeEventDescriptor fileDescriptor = descriptorFactory.describe(fileSessionEvent);
|
RuntimeEventDescriptor fileDescriptor = descriptorFactory.describe(fileSessionEvent);
|
||||||
|
|
||||||
assertThat(dbDescriptor.sourceProfile()).isEqualTo(new RuntimeEventSourceProfile(
|
assertThat(dbDescriptor.sourceProfile().sourceSystem()).isEqualTo("TACHOGRAPH");
|
||||||
"TACHOGRAPH",
|
assertThat(fileDescriptor.sourceProfile().sourceSystem()).isEqualTo("TACHOGRAPH");
|
||||||
"DRIVER_CARD",
|
assertThat(dbDescriptor.sourceProfile().sourceKind()).isEqualTo("DRIVER_CARD");
|
||||||
"CARD_PLACE"
|
assertThat(fileDescriptor.sourceProfile().sourceKind()).isEqualTo("DRIVER_CARD");
|
||||||
));
|
assertThat(dbDescriptor.sourceProfile().extractionCode()).isEqualTo("CARD_PLACE");
|
||||||
assertThat(fileDescriptor.sourceProfile()).isEqualTo(dbDescriptor.sourceProfile());
|
assertThat(fileDescriptor.sourceProfile().extractionCode()).isEqualTo("CARD_PLACE");
|
||||||
|
assertThat(dbDescriptor.evidenceSourceRole())
|
||||||
|
.isEqualTo(RuntimeTachographEvidenceSourceRole.DRIVER_CARD);
|
||||||
|
assertThat(fileDescriptor.evidenceSourceRole())
|
||||||
|
.isEqualTo(RuntimeTachographEvidenceSourceRole.DRIVER_CARD);
|
||||||
|
assertThat(dbDescriptor.representation())
|
||||||
|
.isEqualTo(RuntimeTachographRepresentation.DATABASE);
|
||||||
|
assertThat(fileDescriptor.representation())
|
||||||
|
.isEqualTo(RuntimeTachographRepresentation.FILE_SESSION);
|
||||||
assertThat(fileDescriptor.compatibleSupportEvidenceKey())
|
assertThat(fileDescriptor.compatibleSupportEvidenceKey())
|
||||||
.isEqualTo(dbDescriptor.compatibleSupportEvidenceKey());
|
.isEqualTo(dbDescriptor.compatibleSupportEvidenceKey());
|
||||||
}
|
}
|
||||||
|
|
@ -101,14 +109,21 @@ class RuntimeTachographRepresentationParityTest {
|
||||||
EventLifecycle.INSERT
|
EventLifecycle.INSERT
|
||||||
);
|
);
|
||||||
|
|
||||||
assertThat(descriptorFactory.sourceProfile(fileCvu))
|
RuntimeEventSourceProfile dbCvuProfile = descriptorFactory.sourceProfile(dbCvu);
|
||||||
.isEqualTo(descriptorFactory.sourceProfile(dbCvu));
|
RuntimeEventSourceProfile fileCvuProfile = descriptorFactory.sourceProfile(fileCvu);
|
||||||
assertThat(descriptorFactory.sourceProfile(fileIw))
|
RuntimeEventSourceProfile dbIwProfile = descriptorFactory.sourceProfile(dbIw);
|
||||||
.isEqualTo(descriptorFactory.sourceProfile(dbIw));
|
RuntimeEventSourceProfile fileIwProfile = descriptorFactory.sourceProfile(fileIw);
|
||||||
assertThat(descriptorFactory.sourceProfile(dbCvu).extractionCode())
|
|
||||||
.isEqualTo("CARD_VEHICLES_USED");
|
assertThat(fileCvuProfile.sourceKind()).isEqualTo(dbCvuProfile.sourceKind());
|
||||||
assertThat(descriptorFactory.sourceProfile(dbIw).extractionCode())
|
assertThat(fileCvuProfile.extractionCode()).isEqualTo(dbCvuProfile.extractionCode());
|
||||||
.isEqualTo("IW_CYCLE");
|
assertThat(fileCvuProfile.evidenceSourceRole()).isEqualTo(dbCvuProfile.evidenceSourceRole());
|
||||||
|
assertThat(fileIwProfile.sourceKind()).isEqualTo(dbIwProfile.sourceKind());
|
||||||
|
assertThat(fileIwProfile.extractionCode()).isEqualTo(dbIwProfile.extractionCode());
|
||||||
|
assertThat(fileIwProfile.evidenceSourceRole()).isEqualTo(dbIwProfile.evidenceSourceRole());
|
||||||
|
assertThat(dbCvuProfile.extractionCode()).isEqualTo("CARD_VEHICLES_USED");
|
||||||
|
assertThat(dbIwProfile.extractionCode()).isEqualTo("IW_CYCLE");
|
||||||
|
assertThat(fileCvuProfile.representation()).isEqualTo(RuntimeTachographRepresentation.FILE_SESSION);
|
||||||
|
assertThat(dbCvuProfile.representation()).isEqualTo(RuntimeTachographRepresentation.DATABASE);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
@ -276,7 +291,9 @@ class RuntimeTachographRepresentationParityTest {
|
||||||
ImportScopeDto.tenantAll(null, null),
|
ImportScopeDto.tenantAll(null, null),
|
||||||
domain.name(),
|
domain.name(),
|
||||||
null,
|
null,
|
||||||
provider + ":package"
|
representation == Representation.DB
|
||||||
|
? "RUNTIME:TACHOGRAPH:" + (extractionCode == null ? "UNKNOWN" : extractionCode) + ":test"
|
||||||
|
: provider + ":package"
|
||||||
);
|
);
|
||||||
ObjectNode raw = JsonNodeFactory.instance.objectNode();
|
ObjectNode raw = JsonNodeFactory.instance.objectNode();
|
||||||
raw.put("sourceKind", sourceKind);
|
raw.put("sourceKind", sourceKind);
|
||||||
|
|
@ -306,7 +323,7 @@ class RuntimeTachographRepresentationParityTest {
|
||||||
null,
|
null,
|
||||||
new EventDetailsDto(domain.name(), JsonNodeFactory.instance.objectNode()),
|
new EventDetailsDto(domain.name(), JsonNodeFactory.instance.objectNode()),
|
||||||
new SourcePackageRefDto(
|
new SourcePackageRefDto(
|
||||||
representation == Representation.DB ? sourceKind : "TACHOGRAPH_FILE_SESSION",
|
"TACHOGRAPH_FILE_SESSION",
|
||||||
representation == Representation.DB ? "package-1" : "session-1",
|
representation == Representation.DB ? "package-1" : "session-1",
|
||||||
sourceKind.equals("VEHICLE_UNIT") ? "vehicle-1" : "card-1",
|
sourceKind.equals("VEHICLE_UNIT") ? "vehicle-1" : "card-1",
|
||||||
null,
|
null,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue