Extend runtime event mixing diagnostics

This commit is contained in:
trifonovt 2026-06-15 15:50:26 +02:00
parent 74d479454a
commit 3c5ce9a066
18 changed files with 778 additions and 106 deletions

View File

@ -2,41 +2,39 @@
## 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`;
- 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.
## Implementation
## 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:
- 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 added/updated
## 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;
- file-session CARD activity versus DB VU activity;
- meaningful coordinate conflicts remaining separate.
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.

View File

@ -11,6 +11,8 @@ public record RuntimeEventDescriptor(
String eventIdentityKey,
String eventKey,
RuntimeEventSourceProfile sourceProfile,
RuntimeTachographEvidenceSourceRole evidenceSourceRole,
RuntimeTachographRepresentation representation,
String compatibleActivityKey,
String compatibleSupportEvidenceKey,
boolean driverActivityPoint,
@ -37,6 +39,18 @@ public record RuntimeEventDescriptor(
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) {
return switch (equivalenceType) {
case RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY -> eventKey;

View File

@ -40,6 +40,8 @@ public class RuntimeEventDescriptorFactory {
eventIdentityKey(event),
RuntimeEventIdentityResolver.canonicalEventKey(event),
profile,
profile.evidenceSourceRole(),
profile.representation(),
compatibleActivityKey(event),
compatibleSupportEvidenceKey(event),
isDriverActivityPoint(event),

View File

@ -33,6 +33,10 @@ public class RuntimeEventEvidenceCompatibilityMatcher {
if (rule == null || primary == null || secondary == null) {
return false;
}
if (rule.requireSameSourceRole()
&& primary.evidenceSourceRole() != secondary.evidenceSourceRole()) {
return false;
}
return switch (rule.equivalenceType()) {
case RuntimeEventMixingRule.EQUIVALENCE_EXACT_EVENT_KEY -> true;
case RuntimeEventMixingRule.EQUIVALENCE_COMPATIBLE_ACTIVITY_KEY ->

View File

@ -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
) {
}

View File

@ -14,6 +14,11 @@ public record RuntimeEventMixingRule(
Set<EventLifecycle> lifecycles,
Set<String> primaryExtractionCodes,
Set<String> secondaryExtractionCodes,
Set<RuntimeTachographEvidenceSourceRole> primarySourceRoles,
Set<RuntimeTachographEvidenceSourceRole> secondarySourceRoles,
Set<RuntimeTachographRepresentation> primaryRepresentations,
Set<RuntimeTachographRepresentation> secondaryRepresentations,
boolean requireSameSourceRole,
RuntimeResolvedEventRole primaryRole,
RuntimeResolvedEventRole secondaryRole,
String decision,
@ -29,6 +34,10 @@ public record RuntimeEventMixingRule(
lifecycles = lifecycles == null ? Set.of() : Set.copyOf(lifecycles);
primaryExtractionCodes = normalize(primaryExtractionCodes);
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) {
@ -53,16 +62,55 @@ public record RuntimeEventMixingRule(
if (channel == RuntimeEventMixingChannel.SUPPORT_EVIDENCE && !descriptor.supportEvidenceCandidate()) {
return false;
}
String extractionCode = descriptor.extractionCode();
return primaryExtractionCodes.contains(extractionCode) || secondaryExtractionCodes.contains(extractionCode);
return isPrimary(descriptor) || isSecondary(descriptor);
}
public boolean isPrimary(RuntimeEventDescriptor descriptor) {
return descriptor != null && primaryExtractionCodes.contains(descriptor.extractionCode());
return sideMatches(
descriptor,
primaryExtractionCodes,
primarySourceRoles,
primaryRepresentations
);
}
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) {
@ -71,7 +119,13 @@ public record RuntimeEventMixingRule(
}
return values.stream()
.filter(value -> value != null && !value.isBlank())
.map(value -> value.trim().toUpperCase(java.util.Locale.ROOT))
.map(RuntimeEventMixingRule::normalize)
.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);
}
}

View File

@ -41,6 +41,14 @@ public class RuntimeEventMixingRuleRegistry {
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(
"CARD_POSITION",
"CARD_PLACE",
@ -57,11 +65,18 @@ public class RuntimeEventMixingRuleRegistry {
"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) {
if (RuntimeEventMixingService.MODE_OFF.equals(mode)) {
return List.of();
}
return List.of(
tachographDbFileSessionSameRoleActivityCompatibleKey(),
tachographDbFileSessionSameRoleSupportCompatibleKey(),
tachographCardVuActivityExactEventKey(),
tachographCardVuSupportExactEventKey(),
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() {
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),
ACTIVITY_EVENT_TYPES,
Set.of(EventLifecycle.START, EventLifecycle.END),
Set.of("CARD_ACTIVITY"),
Set.of("VU_ACTIVITY"),
Set.of(RuntimeTachographEvidenceSourceRole.DRIVER_CARD),
Set.of(RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT),
Set.of(),
Set.of(),
false,
RuntimeResolvedEventRole.FUSED_PRIMARY,
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
"FUSED_PRIMARY_SELECTED",
@ -92,10 +156,15 @@ public class RuntimeEventMixingRuleRegistry {
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),
ACTIVITY_EVENT_TYPES,
Set.of(EventLifecycle.START, EventLifecycle.END),
Set.of("CARD_ACTIVITY"),
Set.of("VU_ACTIVITY"),
Set.of(RuntimeTachographEvidenceSourceRole.DRIVER_CARD),
Set.of(RuntimeTachographEvidenceSourceRole.VEHICLE_UNIT),
Set.of(),
Set.of(),
false,
RuntimeResolvedEventRole.FUSED_PRIMARY,
RuntimeResolvedEventRole.SUPPRESSED_DUPLICATE,
"FUSED_PRIMARY_SELECTED",
@ -113,6 +182,11 @@ public class RuntimeEventMixingRuleRegistry {
SUPPORT_EVENT_LIFECYCLES,
CARD_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.SUPPRESSED_DUPLICATE,
"FUSED_PRIMARY_SELECTED",
@ -130,6 +204,11 @@ public class RuntimeEventMixingRuleRegistry {
SUPPORT_EVENT_LIFECYCLES,
CARD_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.SUPPRESSED_DUPLICATE,
"FUSED_PRIMARY_SELECTED",

View File

@ -30,6 +30,10 @@ public class RuntimeEventMixingService {
"tachograph.support.card-vu.same-event-key";
public static final String RULE_TACHOGRAPH_CARD_VU_SUPPORT_COMPATIBLE_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 RuntimeEventMixingRuleRegistry ruleRegistry;
@ -87,6 +91,7 @@ public class RuntimeEventMixingService {
.toList();
List<RuntimeResolvedEvent> resolvedEvents = buildResolvedEvents(state, rawEvents);
RuntimeEventMixingDiagnostics diagnostics = diagnostics(descriptors, state);
List<String> notes = new ArrayList<>();
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 + ".");
@ -94,6 +99,12 @@ public class RuntimeEventMixingService {
+ " 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 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(
rawEvents,
driverPartitionEvents,
@ -103,6 +114,7 @@ public class RuntimeEventMixingService {
state.suppressedEvents(),
resolvedEvents,
state.decisions(),
diagnostics,
notes,
state.warnings()
);
@ -118,6 +130,7 @@ public class RuntimeEventMixingService {
List.of(),
descriptors.stream().map(this::defaultResolvedEvent).toList(),
List.of(),
diagnostics(descriptors, new MixingState(descriptors)),
List.of(note),
List.of()
);
@ -157,12 +170,20 @@ public class RuntimeEventMixingService {
if (primaries.isEmpty() || secondaries.isEmpty()) {
return;
}
state.incrementCandidateGroupCount();
for (RuntimeEventDescriptor primary : primaries) {
List<RuntimeEventDescriptor> compatibleSecondaries = secondaries.stream()
.filter(descriptor -> !state.isSuppressed(descriptor))
.filter(descriptor -> compatibilityMatcher.compatible(rule, primary, descriptor))
.toList();
List<RuntimeEventDescriptor> compatibleSecondaries = new ArrayList<>();
for (RuntimeEventDescriptor secondary : secondaries) {
if (state.isSuppressed(secondary)) {
continue;
}
if (compatibilityMatcher.compatible(rule, primary, secondary)) {
compatibleSecondaries.add(secondary);
} else {
state.incrementCompatibilityRejectedCount();
}
}
if (compatibleSecondaries.isEmpty()) {
continue;
}
@ -220,6 +241,54 @@ public class RuntimeEventMixingService {
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) {
RuntimeEventMixingChannel channel = defaultChannel(descriptor);
RuntimeResolvedEventRole role = switch (channel) {
@ -369,6 +438,8 @@ public class RuntimeEventMixingService {
private final List<EventHubEventDto> suppressedEvents = new ArrayList<>();
private final List<RuntimeEventMixingDecisionDto> decisions = new ArrayList<>();
private final List<String> warnings = new ArrayList<>();
private int candidateGroupCount;
private int compatibilityRejectedCount;
private MixingState(List<RuntimeEventDescriptor> descriptors) {
this.descriptors = descriptors == null ? List.of() : List.copyOf(descriptors);
@ -486,6 +557,22 @@ public class RuntimeEventMixingService {
return decisions;
}
private void incrementCandidateGroupCount() {
candidateGroupCount++;
}
private int candidateGroupCount() {
return candidateGroupCount;
}
private void incrementCompatibilityRejectedCount() {
compatibilityRejectedCount++;
}
private int compatibilityRejectedCount() {
return compatibilityRejectedCount;
}
private List<String> warnings() {
return warnings;
}

View File

@ -1,10 +1,36 @@
package at.procon.eventhub.processing.eventprocessing.mixing;
public record RuntimeEventSourceProfile(
String sourceSystem,
String sourceKind,
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() {
return switch (sourceSystem == null ? "" : sourceSystem) {
case "TACHOGRAPH", "TACHOGRAPH_FILE_SESSION", "COMPOSITE_TACHOGRAPH_FILE_SESSION" -> true;

View File

@ -12,6 +12,7 @@ public record RuntimeMixedEventBundle(
List<EventHubEventDto> suppressedEvents,
List<RuntimeResolvedEvent> resolvedEvents,
List<RuntimeEventMixingDecisionDto> eventMixingDecisions,
RuntimeEventMixingDiagnostics diagnostics,
List<String> notes,
List<String> warnings
) {
@ -24,6 +25,9 @@ public record RuntimeMixedEventBundle(
suppressedEvents = suppressedEvents == null ? List.of() : List.copyOf(suppressedEvents);
resolvedEvents = resolvedEvents == null ? List.of() : List.copyOf(resolvedEvents);
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);
warnings = warnings == null ? List.of() : List.copyOf(warnings);
}

View File

@ -13,10 +13,9 @@ import org.springframework.stereotype.Component;
/**
* Canonical semantic view of tachograph runtime events.
*
* <p>The same tachograph package can be processed directly from an in-memory file session or
* loaded from the tachograph database after serialization. This component normalizes only the
* representation differences that are relevant to downstream mixing. It deliberately leaves the
* original {@link EventHubEventDto} untouched for provenance and audit purposes.</p>
* <p>The same tachograph package can be processed directly from a file session or loaded from
* the tachograph database after serialization. This component normalizes only representation
* differences relevant to runtime mixing and leaves the original event untouched.</p>
*/
@Component
public class RuntimeTachographEventSemantics {
@ -51,24 +50,49 @@ public class RuntimeTachographEventSemantics {
text(raw, "extractionCode"),
extractionCodeFromExternalSourceEventId(event)
));
String sourceKind = normalizeUpper(firstNonBlank(
String sourceKindCandidate = normalizeUpper(firstNonBlank(
text(raw, "sourceKind"),
sourceKind(event),
sourceKey(event),
sourcePackageKind(event),
sourceKindFromExtractionCode(explicitExtractionCode)
));
RuntimeTachographEvidenceSourceRole sourceRole = evidenceSourceRole(
sourceKindCandidate,
explicitExtractionCode,
event
);
String sourceKind = canonicalSourceKind(sourceRole, sourceKindCandidate);
String extractionCode = normalizeUpper(firstNonBlank(
explicitExtractionCode,
inferExtractionCode(event, sourceKind)
inferExtractionCode(event, sourceRole)
));
if (sourceRole == RuntimeTachographEvidenceSourceRole.UNKNOWN) {
sourceRole = evidenceSourceRole(sourceKind, extractionCode, event);
sourceKind = canonicalSourceKind(sourceRole, sourceKind);
}
String sourceSystemCandidate = normalizeUpper(firstNonBlank(
text(raw, "sourceSystem"),
sourceProvider(event),
sourceSystemFromExternalSourceEventId(event)
sourceSystemFromExternalSourceEventId(event),
sourcePackageKind(event)
));
String sourceSystem = isTachographRepresentation(event, sourceSystemCandidate, extractionCode)
? "TACHOGRAPH"
: sourceSystemCandidate;
return new RuntimeEventSourceProfile(sourceSystem, sourceKind, extractionCode);
boolean tachograph = isTachographRepresentation(event, sourceSystemCandidate, extractionCode);
String sourceSystem = tachograph ? "TACHOGRAPH" : sourceSystemCandidate;
RuntimeTachographRepresentation representation = tachograph
? 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) {
if (event == null || event.eventDomain() == null) {
return null;
return inferExtractionCode(event, evidenceSourceRole(sourceKind, null, event));
}
String normalizedSourceKind = normalizeUpper(sourceKind);
if (normalizedSourceKind == null) {
public String inferExtractionCode(
EventHubEventDto event,
RuntimeTachographEvidenceSourceRole sourceRole
) {
if (event == null || event.eventDomain() == null || sourceRole == null) {
return null;
}
if (event.eventDomain() == EventDomain.DRIVER_ACTIVITY) {
return switch (normalizedSourceKind) {
case "DRIVER_CARD" -> "CARD_ACTIVITY";
case "VEHICLE_UNIT" -> "VU_ACTIVITY";
return switch (sourceRole) {
case DRIVER_CARD -> "CARD_ACTIVITY";
case VEHICLE_UNIT -> "VU_ACTIVITY";
default -> null;
};
}
if (event.eventDomain() == EventDomain.DRIVER_CARD) {
return switch (normalizedSourceKind) {
case "DRIVER_CARD" -> "CARD_VEHICLES_USED";
case "VEHICLE_UNIT" -> "IW_CYCLE";
return switch (sourceRole) {
case DRIVER_CARD -> "CARD_VEHICLES_USED";
case VEHICLE_UNIT -> "IW_CYCLE";
default -> null;
};
}
String prefix = switch (normalizedSourceKind) {
case "DRIVER_CARD" -> "CARD";
case "VEHICLE_UNIT" -> "VU";
String prefix = switch (sourceRole) {
case DRIVER_CARD -> "CARD";
case VEHICLE_UNIT -> "VU";
default -> 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) {
RuntimeEventSourceProfile profile = sourceProfile(event);
return profile.isTachographRuntimeSource();
return sourceProfile(event).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(
@ -143,15 +289,12 @@ public class RuntimeTachographEventSemantics {
if (KNOWN_EXTRACTION_CODES.contains(nullToEmpty(extractionCode))) {
return true;
}
String packageKind = event == null || event.sourcePackageRef() == null
? null
: normalizeUpper(event.sourcePackageRef().packageKind());
if (TACHOGRAPH_SOURCE_SYSTEMS.contains(nullToEmpty(packageKind))) {
String packageKind = normalizeUpper(sourcePackageKind(event));
if (TACHOGRAPH_SOURCE_SYSTEMS.contains(nullToEmpty(packageKind))
|| containsFileSessionMarker(packageKind)) {
return true;
}
String sourceKey = event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null
? null
: normalizeUpper(event.packageInfo().eventSource().sourceKey());
String sourceKey = normalizeUpper(sourceKey(event));
if (sourceKey != null && sourceKey.startsWith("TACHOGRAPH")) {
return true;
}
@ -172,12 +315,29 @@ public class RuntimeTachographEventSemantics {
: 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) {
return event == null || event.packageInfo() == null || event.packageInfo().eventSource() == null
? null
: 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) {
String externalId = event == null ? null : event.externalSourceEventId();
if (externalId == null || externalId.isBlank()) {
@ -201,18 +361,24 @@ public class RuntimeTachographEventSemantics {
}
private String sourceKindFromExtractionCode(String extractionCode) {
String normalized = normalizeUpper(extractionCode);
if (normalized == null) {
return null;
RuntimeTachographEvidenceSourceRole role = evidenceSourceRole(null, extractionCode, null);
return canonicalSourceKind(role, 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)) {
return "VEHICLE_UNIT";
private boolean startsWithAny(String value, String... prefixes) {
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) {

View File

@ -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
}

View File

@ -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
}

View File

@ -39,7 +39,7 @@ public class EventEvidenceMixingModule implements RuntimeProcessingModule {
return new RuntimeProcessingModuleDescriptorDto(
moduleKey(),
"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",
Set.of(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY),
Set.of("UnifiedRuntimeEventBundle"),
@ -64,6 +64,15 @@ public class EventEvidenceMixingModule implements RuntimeProcessingModule {
metadata.put("resolvedEventCount", mixed.resolvedEvents().size());
metadata.put("eventMixingDecisionCount", mixed.eventMixingDecisions().size());
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(
moduleKey(),
RuntimeProcessingModuleStatus.SUCCESS,

View File

@ -80,7 +80,7 @@ select
base.country,
base.region,
'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(base.driver_id as varchar(128)) as driver_source_entity_id,

View File

@ -58,7 +58,7 @@ select
base.country,
base.region,
'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(base.driver_id as varchar(128)) as driver_source_entity_id,

View File

@ -491,6 +491,77 @@ class RuntimeEventMixingServiceTest {
.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
void keepsCrossRepresentationSupportEventsWhenMeaningfulCoordinatesConflict() {
OffsetDateTime occurredAt = OffsetDateTime.parse("2026-04-01T04:30:59Z");
@ -524,6 +595,108 @@ class RuntimeEventMixingServiceTest {
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(
boolean fileSessionCard,
String externalId,

View File

@ -52,12 +52,20 @@ class RuntimeTachographRepresentationParityTest {
RuntimeEventDescriptor dbDescriptor = descriptorFactory.describe(dbEvent);
RuntimeEventDescriptor fileDescriptor = descriptorFactory.describe(fileSessionEvent);
assertThat(dbDescriptor.sourceProfile()).isEqualTo(new RuntimeEventSourceProfile(
"TACHOGRAPH",
"DRIVER_CARD",
"CARD_PLACE"
));
assertThat(fileDescriptor.sourceProfile()).isEqualTo(dbDescriptor.sourceProfile());
assertThat(dbDescriptor.sourceProfile().sourceSystem()).isEqualTo("TACHOGRAPH");
assertThat(fileDescriptor.sourceProfile().sourceSystem()).isEqualTo("TACHOGRAPH");
assertThat(dbDescriptor.sourceProfile().sourceKind()).isEqualTo("DRIVER_CARD");
assertThat(fileDescriptor.sourceProfile().sourceKind()).isEqualTo("DRIVER_CARD");
assertThat(dbDescriptor.sourceProfile().extractionCode()).isEqualTo("CARD_PLACE");
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())
.isEqualTo(dbDescriptor.compatibleSupportEvidenceKey());
}
@ -101,14 +109,21 @@ class RuntimeTachographRepresentationParityTest {
EventLifecycle.INSERT
);
assertThat(descriptorFactory.sourceProfile(fileCvu))
.isEqualTo(descriptorFactory.sourceProfile(dbCvu));
assertThat(descriptorFactory.sourceProfile(fileIw))
.isEqualTo(descriptorFactory.sourceProfile(dbIw));
assertThat(descriptorFactory.sourceProfile(dbCvu).extractionCode())
.isEqualTo("CARD_VEHICLES_USED");
assertThat(descriptorFactory.sourceProfile(dbIw).extractionCode())
.isEqualTo("IW_CYCLE");
RuntimeEventSourceProfile dbCvuProfile = descriptorFactory.sourceProfile(dbCvu);
RuntimeEventSourceProfile fileCvuProfile = descriptorFactory.sourceProfile(fileCvu);
RuntimeEventSourceProfile dbIwProfile = descriptorFactory.sourceProfile(dbIw);
RuntimeEventSourceProfile fileIwProfile = descriptorFactory.sourceProfile(fileIw);
assertThat(fileCvuProfile.sourceKind()).isEqualTo(dbCvuProfile.sourceKind());
assertThat(fileCvuProfile.extractionCode()).isEqualTo(dbCvuProfile.extractionCode());
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
@ -276,7 +291,9 @@ class RuntimeTachographRepresentationParityTest {
ImportScopeDto.tenantAll(null, null),
domain.name(),
null,
provider + ":package"
representation == Representation.DB
? "RUNTIME:TACHOGRAPH:" + (extractionCode == null ? "UNKNOWN" : extractionCode) + ":test"
: provider + ":package"
);
ObjectNode raw = JsonNodeFactory.instance.objectNode();
raw.put("sourceKind", sourceKind);
@ -306,7 +323,7 @@ class RuntimeTachographRepresentationParityTest {
null,
new EventDetailsDto(domain.name(), JsonNodeFactory.instance.objectNode()),
new SourcePackageRefDto(
representation == Representation.DB ? sourceKind : "TACHOGRAPH_FILE_SESSION",
"TACHOGRAPH_FILE_SESSION",
representation == Representation.DB ? "package-1" : "session-1",
sourceKind.equals("VEHICLE_UNIT") ? "vehicle-1" : "card-1",
null,