From 82e2bd086033f1ff13c70c3c6e93ea7fe4103c4a Mon Sep 17 00:00:00 2001 From: trifonovt <87468028+TihomirTrifonov@users.noreply.github.com> Date: Tue, 26 May 2026 15:27:29 +0200 Subject: [PATCH] Add runtime processing pipeline and validation flows --- .../runtime-driver-working-time-processing.md | 70 + docs/runtime-event-processing.md | 407 +--- ...ntime-tachograph-esper-scope-processing.md | 75 +- ...e-event-processing.postman_collection.json | 362 +++- .../UnifiedRuntimeProcessingController.java | 74 +- .../DriverWorkingTimeProcessingResultDto.java | 52 + .../DriverWorkingTimeProcessingCore.java | 28 + ...eSupportEvidenceNormalizationDebugDto.java | 17 + ...fiedRuntimeDerivedProjectionResultDto.java | 36 +- ...untimeDriverWorkingTimeScopeResultDto.java | 74 + ...dRuntimeTachographEsperScopeResultDto.java | 19 + ...timeEventProcessingPartitionResultDto.java | 15 +- .../dto/RuntimeEventProcessingResultDto.java | 23 + .../AbstractDriverWorkingTimePhaseModule.java | 60 + .../module/DriverActivityIntervalsModule.java | 82 + .../DriverVehicleUsageIntervalsModule.java | 82 + .../module/DriverVehicleUsageMergeModule.java | 18 + ...erWorkingTimeDerivedProjectionsModule.java | 77 + .../module/DriverWorkingTimeModuleKeys.java | 15 + .../module/RuntimeEventAssemblyModule.java | 62 + .../module/RuntimeProcessingModule.java | 20 + .../RuntimeProcessingModuleContext.java | 21 + .../RuntimeProcessingModuleRegistry.java | 48 + .../module/RuntimeProcessingModuleResult.java | 19 + .../module/RuntimeProcessingModuleStatus.java | 7 + .../RuntimeProcessingPipelineExecutor.java | 46 + .../SupportEvidenceNormalizationModule.java | 18 + .../VehicleEvidenceAttachmentModule.java | 18 + .../epl/DriverWorkingTimeEplEventMapper.java | 386 ++++ .../epl/RuntimeEplInputEventStream.java | 17 + .../module/epl/RuntimeEplModule.java | 10 + .../epl/RuntimeEplModuleDefinition.java | 31 + .../epl/RuntimeEplModuleExecutionResult.java | 26 + .../module/epl/RuntimeEplModuleExecutor.java | 120 ++ ...riverWorkingTimeRuntimeProcessingPlan.java | 480 +++++ .../RuntimeProcessingExecutionApiRequest.java | 59 + .../RuntimeProcessingExecutionResultDto.java | 53 + .../RuntimeProcessingExecutionService.java | 24 + .../RuntimeProcessingModuleDescriptorDto.java | 22 + .../plan/RuntimeProcessingPlan.java | 55 + .../RuntimeProcessingPlanDescriptorDto.java | 44 + .../plan/RuntimeProcessingPlanRegistry.java | 72 + ...riverWorkingTimeRuntimeProcessingPlan.java | 16 + ...verEsperRuntimeEventProcessingProfile.java | 236 +-- ...dSourceEvidencePartitionValidationDto.java | 23 + ...xedSourceEvidenceValidationApiRequest.java | 37 + ...ixedSourceEvidenceValidationResultDto.java | 25 + ...eMixedSourceEvidenceValidationService.java | 174 ++ ...timeTachographParityValidationService.java | 5 + ...iverWorkingTimeScopeProcessingService.java | 319 +++ ...nifiedRuntimeDerivedProjectionService.java | 27 +- .../UnifiedRuntimeEventAssemblyService.java | 8 +- ...TachographEsperScopeProcessingService.java | 319 +-- ...hographEsperDriverProcessingResultDto.java | 84 + ...iverTimelineReusableProjectionBuilder.java | 4 +- .../TachographEsperProcessingCore.java | 8 +- ...river-working-time-derived-projections.epl | 1792 +++++++++++++++++ .../runtime-driver-activity-intervals.epl | 74 + ...ime-driver-event-interval-preprocessor.epl | 348 ++++ ...runtime-driver-vehicle-usage-intervals.epl | 64 + ...nifiedRuntimeProcessingControllerTest.java | 8 +- .../RuntimeProcessingPlanRegistryTest.java | 60 + ...sperRuntimeEventProcessingProfileTest.java | 10 +- ...edSourceEvidenceValidationServiceTest.java | 135 ++ ...TachographParityValidationServiceTest.java | 2 +- 65 files changed, 6143 insertions(+), 879 deletions(-) create mode 100644 docs/runtime-driver-working-time-processing.md create mode 100644 src/main/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDto.java create mode 100644 src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeProcessingCore.java create mode 100644 src/main/java/at/procon/eventhub/processing/dto/RuntimeSupportEvidenceNormalizationDebugDto.java create mode 100644 src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeDriverWorkingTimeScopeResultDto.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/AbstractDriverWorkingTimePhaseModule.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverActivityIntervalsModule.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverVehicleUsageIntervalsModule.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverVehicleUsageMergeModule.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeDerivedProjectionsModule.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeModuleKeys.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeEventAssemblyModule.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModule.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleContext.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleRegistry.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleResult.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleStatus.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingPipelineExecutor.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/SupportEvidenceNormalizationModule.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/VehicleEvidenceAttachmentModule.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/DriverWorkingTimeEplEventMapper.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplInputEventStream.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModule.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModuleDefinition.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModuleExecutionResult.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModuleExecutor.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/plan/DriverWorkingTimeRuntimeProcessingPlan.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingExecutionApiRequest.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingExecutionResultDto.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingExecutionService.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingModuleDescriptorDto.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlan.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlanDescriptorDto.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlanRegistry.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/plan/TachographDriverWorkingTimeRuntimeProcessingPlan.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidencePartitionValidationDto.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationApiRequest.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationResultDto.java create mode 100644 src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationService.java create mode 100644 src/main/java/at/procon/eventhub/processing/service/RuntimeDriverWorkingTimeScopeProcessingService.java create mode 100644 src/main/resources/esper/driver-working-time-derived-projections.epl create mode 100644 src/main/resources/esper/runtime-driver-activity-intervals.epl create mode 100644 src/main/resources/esper/runtime-driver-event-interval-preprocessor.epl create mode 100644 src/main/resources/esper/runtime-driver-vehicle-usage-intervals.epl create mode 100644 src/test/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlanRegistryTest.java create mode 100644 src/test/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationServiceTest.java diff --git a/docs/runtime-driver-working-time-processing.md b/docs/runtime-driver-working-time-processing.md new file mode 100644 index 0000000..532a324 --- /dev/null +++ b/docs/runtime-driver-working-time-processing.md @@ -0,0 +1,70 @@ +# Runtime driver working-time processing + +Runtime driver working-time processing is a source-neutral processing plan over canonical EventHub events. + +The preferred runtime execution endpoint is: + +```http +POST /api/eventhub/runtime-processing/executions +``` + +Use: + +```json +{ + "processingPlanKey": "driver-working-time-v1" +} +``` + +## Canonical input idea + +The plan should work with events from any source once they are normalized into canonical EventHub event semantics: + +```text +DRIVER_ACTIVITY START/END +DRIVER_CARD or DRIVER_VEHICLE_USAGE INSERT/WITHDRAW +POSITION / PLACE / BORDER_CROSSING / LOAD_UNLOAD / IGNITION / ODOMETER support evidence +``` + +Tachograph files and tachograph databases are only two possible sources. YellowFox and future telematics providers can contribute vehicle-only evidence that is attached to driver partitions by vehicle/time overlap. + +## New source-neutral artifacts + +```text +DriverWorkingTimeProcessingResultDto +DriverWorkingTimeProcessingCore +RuntimeDriverWorkingTimeScopeProcessingService +DriverWorkingTimeRuntimeProcessingPlan +runtime-driver-event-interval-preprocessor.epl +driver-working-time-derived-projections.epl +``` + +## Compatibility artifacts + +The following names are kept only for backward compatibility with existing file-session APIs and older Postman calls: + +```text +TachographEsperDriverProcessingResultDto +TachographEsperProcessingCore +UnifiedRuntimeTachographEsperScopeProcessingService +tachograph-driving-derived-projection-events-preprocessor.epl +tachograph-driving-derived-projection-bundle.epl +``` + +New runtime-processing code should use the driver-working-time names. + +## EPL-backed phase modules + +The driver working-time plan now contains first-class EPL-backed phase modules for event-to-interval conversion: + +```text +event-to-activity-intervals + EventHub DRIVER_ACTIVITY START/END events + -> DriverActivityIntervalEvent + + event-to-vehicle-usage-intervals + EventHub DRIVER_CARD INSERT/WITHDRAW events + -> DriverVehicleUsageIntervalEvent +``` + +The final derived projection module still delegates to the shared working-time projection service for parity with existing file-session processing. This lets us migrate the pipeline gradually: common event-to-interval conversion is now EPL-module based, while the larger rest/trip/overnight projection bundle remains compatibility-safe. diff --git a/docs/runtime-event-processing.md b/docs/runtime-event-processing.md index c92cf03..5715510 100644 --- a/docs/runtime-event-processing.md +++ b/docs/runtime-event-processing.md @@ -1,64 +1,67 @@ -# Runtime Event Processing +# Runtime event processing -Runtime Processing is now source-neutral. The API receives a runtime scope, selects a processing profile, partitions the normalized EventHub-style events, and delegates execution to the selected profile. +Runtime processing is now centered on **processing executions**. An execution loads canonical EventHub runtime events from selected sources, optionally partitions the event set, then runs a configured processing plan made of common EPL and/or Java modules. -The tachograph Esper processing is no longer the root concept. It is one profile: - -```text -tachograph-driver-esper-v1 -``` - -## Profile discovery endpoint +The preferred endpoint is: ```http -GET /api/eventhub/runtime-processing/event-processing/profiles +POST /api/eventhub/runtime-processing/executions ``` -Example response: +List available plans: -```json -[ - { - "profileKey": "tachograph-driver-esper-v1", - "displayName": "Tachograph Driver Esper Processing", - "description": "Runs the shared tachograph driver Esper processing pipeline over Runtime Processing event scopes.", - "defaultPartitioningStrategy": "DRIVER", - "supportedPartitioningStrategies": ["DRIVER"], - "requiredParameters": [], - "optionalParameters": [ - "significantDrivingMinutes", - "minimumRestPeriodMinutes", - "attachVehicleOnlyEvents", - "vehicleEvidencePaddingMinutes", - "includePartitionDebug" - ] - } -] +```http +GET /api/eventhub/runtime-processing/executions/plans ``` -Clients should prefer this endpoint instead of hardcoding profile metadata. - -## Generic execution endpoint +The older endpoint is still available for compatibility: ```http POST /api/eventhub/runtime-processing/event-processing ``` -## Request shape +but it should be treated as a legacy profile adapter. New clients should use `processingPlanKey`, not `profileKey`. + +## First predefined plan + +The first predefined plan is: + +```text +driver-working-time-v1 +``` + +Legacy alias: + +```text +tachograph-driver-esper-v1 +``` + +This plan runs the source-neutral driver working-time chain over canonical runtime events. Tachograph file/database data is only one possible source of those canonical events. Its modules include: + +```text +event-to-activity-intervals +event-to-vehicle-usage-intervals +vehicle-evidence-attachment +support-evidence-normalization +driving-derived-projections +``` + +The important design point is that runtime processing is not tachograph-specific. Tachograph is now treated as a source/compatibility layer; the working-time plan consumes canonical driver activity, vehicle usage, and support-evidence events from any supported source. + +## Example execution ```json { - "profileKey": "tachograph-driver-esper-v1", - "scope": { + "processingPlanKey": "driver-working-time-v1", + "sourceSelection": { "sessionIds": [ "11111111-1111-1111-1111-111111111111", "22222222-2222-2222-2222-222222222222" ], - "sourceFamilies": ["TACHOGRAPH_FILE_SESSION"], + "sourceFamilies": ["TACHOGRAPH_FILE_SESSION", "YELLOWFOX_DB"], "occurredFrom": "2026-05-01T00:00:00Z", "occurredTo": "2026-05-31T23:59:59Z", - "expandVehicleEvents": true, - "vehicleExpansionPaddingMinutes": 15 + "expandVehicleEvents": true }, "partitioning": { "strategy": "DRIVER", @@ -70,175 +73,44 @@ POST /api/eventhub/runtime-processing/event-processing "parameters": { "significantDrivingMinutes": 3, "minimumRestPeriodMinutes": 720, - "attachVehicleOnlyEvents": true, - "vehicleEvidencePaddingMinutes": 15, "includePartitionDebug": true } } ``` -## Response shape - -```json -{ - "profileKey": "tachograph-driver-esper-v1", - "partitioningStrategy": "DRIVER", - "inputEventCount": 1234, - "selectedPartitionCount": 2, - "discoveredVehicleCount": 3, - "partitionResults": { - "12:12345678901234": { - "partitionType": "DRIVER", - "partitionKey": "12:12345678901234", - "resultType": "UnifiedRuntimeDerivedProjectionResultDto", - "result": { - "projection": { - "activityIntervals": [], - "drivingIntervals": [], - "drivingInterruptionIntervals": [], - "dailyWeeklyRestCandidateIntervals": [], - "dailyWeeklyRestCandidateCoverageIntervals": [], - "unclassifiedDailyWeeklyRestCandidateCoverageIntervals": [], - "potentialHomeOvernightStayIntervals": [], - "potentialInVehicleOvernightStayIntervals": [], - "potentialInVehicleTripIntervals": [], - "vehicleUsageIntervals": [], - "vuCardAbsentIntervals": [], - "supportGeoEvents": [] - } - } - } - }, - "notes": [], - "warnings": [] -} -``` - -## Concepts - -### Scope - -`scope` is the existing runtime selection model. It can select events from: +## Conceptual flow ```text -TACHOGRAPH_FILE_SESSION -TACHOGRAPH_DB -YELLOWFOX_DB +source selection + -> runtime event loaders + -> canonical EventHub events + -> partitioning + -> vehicle evidence attachment + -> support evidence normalization + -> event-to-interval EPL modules + -> driver working-time derived projection EPL/Java modules + -> partition result map ``` -and can use: +## Why processing plans instead of profiles + +A plan describes **which modules should run over a runtime event set**. This matches the EventHub goal better than domain-oriented profiles. + +Future plans can reuse the same event-loading infrastructure: ```text -SOURCE_DB -EVENTHUB_DB -``` - -where supported. - -For uploaded tachograph files, the scope can use: - -```text -sessionId -sessionIds -compositeSessionId -``` - -### Profile - -A profile owns domain-specific processing semantics. It defines: - -```text -profile key -expected partitioning strategy -profile-specific parameters -result type -``` - -Current profile: - -```text -tachograph-driver-esper-v1 -``` - -Future profiles can include: - -```text -vehicle-stop-detection-v1 vehicle-trip-detection-v1 -telematics-poi-clustering-v1 +vehicle-stop-detection-v1 driver-settlement-v1 mixed-driver-vehicle-correlation-v1 +telematics-poi-clustering-v1 ``` -### Partitioning +Each plan may use different partitioning and modules, but the source loading and canonical event model remain common. -The common API supports generic partitioning options: +## Compatibility -```text -NONE -DRIVER -VEHICLE -DRIVER_VEHICLE -SOURCE_FAMILY -CUSTOM_PROFILE -``` - -The first tachograph profile currently supports `DRIVER` partitioning. The service partitions mixed event scopes in Java before invoking Esper so that existing single-driver EPL windows cannot mix driver states. - -## Partition debug / audit output - -For mixed-source scopes, request debug output when validating why events were or were not attached to a driver partition: - -```json -{ - "partitioning": { - "strategy": "DRIVER", - "includeAllPartitions": true, - "attachVehicleEvidence": true, - "vehicleEvidencePaddingMinutes": 15, - "includeDebug": true - }, - "parameters": { - "includePartitionDebug": true - } -} -``` - -When enabled, each generic `partitionResults[*].metadata.partitionDebug` contains: - -```text -directDriverEventCount -vehicleUsageIntervalCount -candidateVehicleEvidenceEventCount -attachedVehicleEvidenceEventCount -ignoredVehicleEvidenceEventCount -mergedEventCount -vehicleUsageIntervals -vehicleEvidenceDecisions -notes -warnings -``` - -`vehicleEvidenceDecisions` explains every relevant decision, for example: - -```text -DIRECT_DRIVER_EVENT -ATTACHED_VEHICLE_EVIDENCE -IGNORED_NO_OVERLAPPING_VEHICLE_USAGE -IGNORED_ATTACHMENT_DISABLED -``` - -The compatibility response type also has `partitionDebugByDriver`; use the generic endpoint when you need to enable debug output explicitly. Keep debug disabled for high-volume production requests unless you need attribution diagnostics, because the decision list can be large. - -## Compatibility endpoint - -The old tachograph endpoint remains available: - -```http -POST /api/eventhub/runtime-processing/tachograph/esper-processing -``` - -It now acts as a compatibility adapter for: +Legacy profile endpoint: ```http POST /api/eventhub/runtime-processing/event-processing @@ -246,143 +118,72 @@ POST /api/eventhub/runtime-processing/event-processing with: -```text -profileKey = tachograph-driver-esper-v1 -partitioning.strategy = DRIVER -``` - -## Tachograph profile processing flow - -```text -runtime event scope - -> broad event assembly - -> driver partition discovery - -> for each driver: - direct driver events - reconstruct driver vehicle-usage intervals - attach only vehicle-scoped events whose vehicle matches and whose timestamp falls inside a usage interval plus configured padding - shared event-input Esper projection pipeline - -> generic partitionResults map -``` - -The tachograph profile reuses the same `TachographEsperProcessingCore` used by the file-session endpoint. This prevents the file-session API and runtime-processing API from drifting into separate rule chains. -## Vehicle-only evidence attachment - -For driver-partitioned profiles, vehicle-only events are no longer attached only by vehicle identity. They are attached to a driver partition only when there is temporal evidence: - -```text -vehicle-only event.vehicleKey/registrationKey matches a reconstructed driver vehicle-usage interval -and event.occurredAt is inside [usage.from - padding, usage.to + padding] -``` - -This prevents unrelated vehicle events from being copied into a driver result simply because the driver used the same vehicle on another day. The tachograph profile currently uses: - ```json { - "partitioning": { - "attachVehicleEvidence": true, - "vehicleEvidencePaddingMinutes": 15, - "includeDebug": true - }, - "parameters": { - "attachVehicleOnlyEvents": true, - "vehicleEvidencePaddingMinutes": 15, - "includePartitionDebug": true - } + "profileKey": "tachograph-driver-esper-v1", + "scope": {}, + "partitioning": {}, + "parameters": {} } ``` -`parameters` take precedence in the tachograph profile. The compatibility endpoint maps these values to `expandVehicleEvents` and `vehicleExpansionPaddingMinutes`. - - -## Tachograph parity validation - -A validation endpoint is available to compare the legacy tachograph file-session Esper endpoint with the generic runtime event-processing profile: - -```http -POST /api/eventhub/runtime-processing/event-processing/validation/tachograph-parity -``` - -This endpoint runs both paths for the selected session(s) and driver(s): +internally delegates to: ```text -legacy file-session path - /api/eventhub/tachograph-file-sessions/{sessionId}/drivers/{driverKey}/processing/esper-events - -runtime event-processing path - /api/eventhub/runtime-processing/event-processing - profileKey = tachograph-driver-esper-v1 +processingPlanKey = driver-working-time-v1 ``` -Example request: +## Source-neutral driver working-time modules -```json -{ - "sessionId": "{{sessionId}}", - "driverKey": "{{driverKey}}", - "occurredFrom": "2026-05-01T00:00:00Z", - "occurredTo": "2026-05-31T23:59:59Z", - "expandVehicleEvents": true, - "vehicleExpansionPaddingMinutes": 15, - "significantDrivingMinutes": 3, - "minimumRestPeriodMinutes": 720, - "includeDebug": true -} -``` - -For multiple uploaded tachograph sessions, use `sessionIds` or `compositeSessionId`. The validation service resolves the selected drivers, executes the generic runtime profile, then compares count-level parity per driver. - -Compared categories include: +The former tachograph-named processing artifacts are now represented by source-neutral driver working-time names: ```text -activityIntervals -drivingIntervals -drivingInterruptionIntervals -drivingInterruptionVehicleChangeIntervals -dailyWeeklyRestCandidateIntervals -dailyWeeklyRestCandidateCoverageIntervals -unclassifiedDailyWeeklyRestCandidateCoverageIntervals -potentialHomeOvernightStayIntervals -potentialInVehicleOvernightStayIntervals -potentialInVehicleTripIntervals -vehicleUsageIntervals -vuCardAbsentIntervals -supportGeoEvents +runtime-driver-event-interval-preprocessor.epl +driver-working-time-derived-projections.epl +DriverWorkingTimeProcessingResultDto +DriverWorkingTimeProcessingCore +RuntimeDriverWorkingTimeScopeProcessingService ``` -For a single session, this is a direct parity check against the original file-session endpoint. For multiple sessions, the reference side is the sum of the individual file-session endpoint results per driver; runtime processing may intentionally deduplicate or merge across session boundaries, so differences should be reviewed with the debug/audit output. +Legacy tachograph names are kept as compatibility adapters for existing file-session endpoints and Postman requests. New runtime code should use the `driver-working-time-*` classes/resources and the `driver-working-time-v1` processing plan. -## Runtime support evidence normalization +## Module execution results -The tachograph profile now normalizes mixed-source support events before invoking the shared Esper core: +`/api/eventhub/runtime-processing/executions` now exposes module execution metadata explicitly. + +Top-level execution response includes: ```text -runtime partition events - -> RuntimeSupportEvidenceNormalizer - -> tachograph-consumable support evidence view - -> event-input Esper preprocessor / driving-derived bundle +moduleResults ``` -The normalizer does not change driver activity events or driver-card usage events. It only adapts support/vehicle events that carry geo or odometer evidence. Provider-specific semantics are preserved in the payload under `raw.originalEventDomain`, `raw.originalEventType`, `raw.originalLifecycle`, `raw.supportEventDomain`, and `raw.supportEventType`. +This map contains one entry per executed module. The top-level entries are intentionally sanitized: they expose status, warnings, and metadata without duplicating the full partition output payload. -Examples: +Each partition result also includes: ```text -IGNITION / IGNITION_ON with position - -> POSITION / POSITION_RECORDED / SNAPSHOT - -> raw.supportEventType = IGNITION_ON - -TELEMATICS_DATA with position - -> POSITION / POSITION_RECORDED / SNAPSHOT - -> raw.supportEventType = TELEMATICS_DATA - -BORDER_CROSSING with position - -> BORDER_CROSSING, preserving the original border event type/lifecycle - -LOAD_UNLOAD with position - -> LOAD_UNLOAD, preserving the original load/unload event type/lifecycle +partitionResults[*].moduleResults ``` -This keeps the EPL rules provider-neutral. YellowFox, tachograph VU, or future telematics events are converted to the common support-evidence shape before the tachograph profile consumes them. +For `driver-working-time-v1`, the partition-level module results currently expose: -Runtime result notes include how many events were inspected and how many support events were adapted. Use partition debug together with normalization notes when validating mixed-source attribution. +```text +vehicle-evidence-attachment +support-evidence-normalization +driving-derived-projections +``` + +The first implementation uses wrapper modules around the existing validated driver-working-time scope service. Logical phase modules such as `event-to-activity-intervals`, `event-to-vehicle-usage-intervals`, and `vehicle-usage-merge` are registered and executed as delegated modules so they can later be split into true standalone EPL/Java modules without changing the external processing-plan contract. + +## First-class EPL modules + +Runtime processing now supports modules implemented directly with Esper EPL through the common `RuntimeEplModuleExecutor`. + +The first source-neutral EPL modules are: + +| Module key | Engine | EPL resource | Output statement | +|---|---|---|---| +| `event-to-activity-intervals` | `EPL` | `esper/runtime-driver-activity-intervals.epl` | `driverActivityIntervals` | +| `event-to-vehicle-usage-intervals` | `EPL` | `esper/runtime-driver-vehicle-usage-intervals.epl` | `driverVehicleUsageIntervals` | + +These modules operate on canonical EventHub runtime events, not on tachograph-specific source rows. They are currently used as first-class phase modules in `driver-working-time-v1`; the final `driving-derived-projections` module remains a compatibility adapter over the validated working-time projection service until the remaining projection stages are split into direct EPL modules. diff --git a/docs/runtime-tachograph-esper-scope-processing.md b/docs/runtime-tachograph-esper-scope-processing.md index 967ba1c..75788d8 100644 --- a/docs/runtime-tachograph-esper-scope-processing.md +++ b/docs/runtime-tachograph-esper-scope-processing.md @@ -1,60 +1,53 @@ # Runtime tachograph Esper scope processing -This document is kept for compatibility with earlier patches. +This document is kept for compatibility. The preferred architecture is now the common Runtime Processing execution model. -The preferred endpoint is now the generic Runtime Event Processing endpoint: +Preferred endpoint: ```http -POST /api/eventhub/runtime-processing/event-processing +POST /api/eventhub/runtime-processing/executions ``` -Use the tachograph profile key: +Use: -```text -tachograph-driver-esper-v1 +```json +{ + "processingPlanKey": "driver-working-time-v1" +} ``` -The old endpoint remains available as an adapter: +Legacy compatibility endpoint: ```http POST /api/eventhub/runtime-processing/tachograph/esper-processing ``` -It delegates to the same profile infrastructure used by the generic endpoint. See: +still exists and delegates through the common runtime processing infrastructure. + +Legacy profile endpoint: + +```http +POST /api/eventhub/runtime-processing/event-processing +``` + +with: + +```json +{ + "profileKey": "tachograph-driver-esper-v1" +} +``` + +also remains available, but new clients should use `processingPlanKey` and `/executions`. + +The current `driver-working-time-v1` plan uses these modules: ```text -docs/runtime-event-processing.md +event-to-activity-intervals +event-to-vehicle-usage-intervals +vehicle-evidence-attachment +support-evidence-normalization +driving-derived-projections ``` -Vehicle-only evidence is now attached through the same common runtime event-processing profile. The old compatibility endpoint maps: - -```json -{ - "expandVehicleEvents": true, - "vehicleExpansionPaddingMinutes": 15 -} -``` - -to the generic profile behavior: - -```json -{ - "parameters": { - "attachVehicleOnlyEvents": true, - "vehicleEvidencePaddingMinutes": 15 - } -} -``` - -Attachment is temporal: a vehicle-only event must match a reconstructed driver vehicle-usage interval and occur inside the interval plus configured padding. - - -## Debugging vehicle evidence attachment - -Prefer the generic `/api/eventhub/runtime-processing/event-processing` endpoint with `partitioning.includeDebug=true` or `parameters.includePartitionDebug=true`. The compatibility response type has `partitionDebugByDriver`, but the generic endpoint is the preferred way to enable debug output explicitly. The generic response exposes debug data under `partitionResults[*].metadata.partitionDebug`. - -## Support evidence normalization - -The tachograph runtime profile is now implemented as a specialization of the generic runtime event-processing framework. Before calling the shared tachograph Esper core, mixed support events are normalized by `RuntimeSupportEvidenceNormalizer`. - -This means that attached vehicle-only evidence from sources such as YellowFox ignition/position events can be consumed by the tachograph profile as support geo evidence when the event contains a position or odometer value. The provider-specific event meaning is preserved in the raw payload, while the Esper-facing event domain is adapted to the common tachograph support evidence contract. +It can load runtime events from multiple sources and sessions, partition them by driver, attach vehicle-only evidence by vehicle/time overlap, normalize support evidence, and run the existing tachograph Esper/Java processing chain per driver partition. diff --git a/postman/eventhub-runtime-event-processing.postman_collection.json b/postman/eventhub-runtime-event-processing.postman_collection.json index acbd9c2..c08db6b 100644 --- a/postman/eventhub-runtime-event-processing.postman_collection.json +++ b/postman/eventhub-runtime-event-processing.postman_collection.json @@ -2,7 +2,7 @@ "info": { "name": "EventHub Runtime Event Processing", "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", - "description": "Examples for source-neutral Runtime Event Processing and the tachograph-driver-esper-v1 profile." + "description": "Examples for source-neutral Runtime Event Processing and the tachograph-driver-esper-v1 profile. Includes plan execution, compatibility profile execution, validation endpoints, and legacy runtime diagnostic endpoints (driver-events, driver-timeline, driver-derived-projections)." }, "variable": [ { @@ -27,6 +27,138 @@ } ], "item": [ + { + "name": "List runtime processing plans", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions/plans", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "executions", + "plans" + ] + } + } + }, + { + "name": "Execute processing plan - driver working time single session", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "executions" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"processingPlanKey\": \"driver-working-time-v1\",\n \"sourceSelection\": {\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": false,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n}" + } + } + }, + { + "name": "Execute processing plan - driver working time multiple sessions all drivers", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "executions" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"processingPlanKey\": \"driver-working-time-v1\",\n \"sourceSelection\": {\n \"sessionIds\": [\n \"{{sessionId}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n}" + } + } + }, + { + "name": "Execute processing plan - driver working time composite session all drivers", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "executions" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"processingPlanKey\": \"driver-working-time-v1\",\n \"sourceSelection\": {\n \"compositeSessionId\": \"{{compositeSessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n}" + } + } + }, + { + "name": "Execute processing plan - driver working time source DB mixed sources", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/executions", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "executions" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"processingPlanKey\": \"driver-working-time-v1\",\n \"sourceSelection\": {\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_DB\",\n \"YELLOWFOX_DB\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverSourceEntityId\": \"{{driverSourceEntityId}}\",\n \"driverCardNation\": \"{{driverCardNation}}\",\n \"driverCardNumber\": \"{{driverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": false,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n}" + } + } + }, { "name": "List runtime event-processing profiles", "request": { @@ -189,6 +321,232 @@ "raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15,\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includeDebug\": true\n}" } } + }, + { + "name": "Validate mixed-source vehicle evidence attachment and normalization", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"processingRequest\": {\n \"profileKey\": \"tachograph-driver-esper-v1\",\n \"scope\": {\n \"sessionIds\": [\n \"{{sessionId1}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\",\n \"YELLOWFOX_DB\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"occurredFrom\": \"2026-05-01T00:00:00Z\",\n \"occurredTo\": \"2026-05-31T23:59:59Z\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n },\n \"partitioning\": {\n \"strategy\": \"DRIVER\",\n \"includeAllPartitions\": true,\n \"attachVehicleEvidence\": true,\n \"vehicleEvidencePaddingMinutes\": 15,\n \"includeDebug\": true\n },\n \"parameters\": {\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720,\n \"includePartitionDebug\": true\n }\n },\n \"minimumAttachedVehicleEvidenceEvents\": 1,\n \"minimumNormalizedSupportEvidenceEvents\": 1,\n \"failWhenPartitionDebugMissing\": true\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/event-processing/validation/mixed-source-evidence", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "event-processing", + "validation", + "mixed-source-evidence" + ] + } + } + }, + { + "name": "Runtime diagnostics - driver events from single file session", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "driver-events" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}" + } + } + }, + { + "name": "Runtime diagnostics - driver timeline from single file session", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "driver-timeline" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}" + } + } + }, + { + "name": "Runtime diagnostics - driver derived projections from single file session", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-derived-projections", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "driver-derived-projections" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"sessionId\": \"{{sessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15,\n \"significantDrivingMinutes\": 3,\n \"minimumRestPeriodMinutes\": 720\n}" + } + } + }, + { + "name": "Runtime diagnostics - driver events from multiple file sessions", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "driver-events" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"sessionIds\": [\n \"{{sessionId}}\",\n \"{{sessionId2}}\"\n ],\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}" + } + } + }, + { + "name": "Runtime diagnostics - driver timeline from composite session", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "driver-timeline" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"compositeSessionId\": \"{{compositeSessionId}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_FILE_SESSION\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverKey\": \"{{driverKey}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}" + } + } + }, + { + "name": "Runtime diagnostics - driver events from source DB", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-events", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "driver-events" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_DB\",\n \"YELLOWFOX_DB\"\n ],\n \"eventBackend\": \"SOURCE_DB\",\n \"driverSourceEntityId\": \"{{driverSourceEntityId}}\",\n \"driverCardNation\": \"{{driverCardNation}}\",\n \"driverCardNumber\": \"{{driverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}" + } + } + }, + { + "name": "Runtime diagnostics - driver timeline from EventHub DB", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/eventhub/runtime-processing/driver-timeline", + "host": [ + "{{baseUrl}}" + ], + "path": [ + "api", + "eventhub", + "runtime-processing", + "driver-timeline" + ] + }, + "body": { + "mode": "raw", + "raw": "{\n \"tenantKey\": \"{{tenantKey}}\",\n \"sourceFamilies\": [\n \"TACHOGRAPH_DB\",\n \"YELLOWFOX_DB\"\n ],\n \"eventBackend\": \"EVENTHUB_DB\",\n \"driverCardNation\": \"{{driverCardNation}}\",\n \"driverCardNumber\": \"{{driverCardNumber}}\",\n \"occurredFrom\": \"{{occurredFrom}}\",\n \"occurredTo\": \"{{occurredTo}}\",\n \"expandVehicleEvents\": true,\n \"vehicleExpansionPaddingMinutes\": 15\n}" + } + } } ] -} \ No newline at end of file +} diff --git a/src/main/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingController.java b/src/main/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingController.java index 6c0f0e4..718f98a 100644 --- a/src/main/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingController.java +++ b/src/main/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingController.java @@ -7,6 +7,13 @@ import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingServi import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingProfileDescriptorDto; +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionApiRequest; +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionResultDto; +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionService; +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingPlanDescriptorDto; +import at.procon.eventhub.processing.eventprocessing.validation.RuntimeMixedSourceEvidenceValidationApiRequest; +import at.procon.eventhub.processing.eventprocessing.validation.RuntimeMixedSourceEvidenceValidationResultDto; +import at.procon.eventhub.processing.eventprocessing.validation.RuntimeMixedSourceEvidenceValidationService; import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationApiRequest; import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationResultDto; import at.procon.eventhub.processing.eventprocessing.validation.RuntimeTachographParityValidationService; @@ -14,7 +21,7 @@ import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle; import at.procon.eventhub.processing.service.UnifiedRuntimeDerivedProjectionService; import at.procon.eventhub.processing.service.UnifiedRuntimeDriverTimelineService; import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService; -import at.procon.eventhub.processing.service.UnifiedRuntimeTachographEsperScopeProcessingService; +import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; @@ -32,27 +39,29 @@ public class UnifiedRuntimeProcessingController { private final UnifiedRuntimeEventAssemblyService eventAssemblyService; private final UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService; private final UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService; - private final UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService; + private final RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService; private final RuntimeEventProcessingService runtimeEventProcessingService; + private final RuntimeProcessingExecutionService runtimeProcessingExecutionService; private final RuntimeTachographParityValidationService tachographParityValidationService; + private final RuntimeMixedSourceEvidenceValidationService mixedSourceEvidenceValidationService; public UnifiedRuntimeProcessingController( UnifiedRuntimeEventAssemblyService eventAssemblyService, UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService, UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService ) { - this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService, null, null, null); + this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService, null, null, null, null, null); } public UnifiedRuntimeProcessingController( UnifiedRuntimeEventAssemblyService eventAssemblyService, UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService, UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService, - UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService, + RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService, RuntimeEventProcessingService runtimeEventProcessingService ) { this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService, - tachographEsperScopeProcessingService, runtimeEventProcessingService, null); + tachographEsperScopeProcessingService, runtimeEventProcessingService, null, null, null); } @Autowired @@ -60,16 +69,33 @@ public class UnifiedRuntimeProcessingController { UnifiedRuntimeEventAssemblyService eventAssemblyService, UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService, UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService, - UnifiedRuntimeTachographEsperScopeProcessingService tachographEsperScopeProcessingService, + RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService, RuntimeEventProcessingService runtimeEventProcessingService, - RuntimeTachographParityValidationService tachographParityValidationService + RuntimeTachographParityValidationService tachographParityValidationService, + RuntimeMixedSourceEvidenceValidationService mixedSourceEvidenceValidationService, + RuntimeProcessingExecutionService runtimeProcessingExecutionService ) { this.eventAssemblyService = eventAssemblyService; this.runtimeDriverTimelineService = runtimeDriverTimelineService; this.runtimeDerivedProjectionService = runtimeDerivedProjectionService; this.tachographEsperScopeProcessingService = tachographEsperScopeProcessingService; this.runtimeEventProcessingService = runtimeEventProcessingService; + this.runtimeProcessingExecutionService = runtimeProcessingExecutionService; this.tachographParityValidationService = tachographParityValidationService; + this.mixedSourceEvidenceValidationService = mixedSourceEvidenceValidationService; + } + + public UnifiedRuntimeProcessingController( + UnifiedRuntimeEventAssemblyService eventAssemblyService, + UnifiedRuntimeDriverTimelineService runtimeDriverTimelineService, + UnifiedRuntimeDerivedProjectionService runtimeDerivedProjectionService, + RuntimeDriverWorkingTimeScopeProcessingService tachographEsperScopeProcessingService, + RuntimeEventProcessingService runtimeEventProcessingService, + RuntimeTachographParityValidationService tachographParityValidationService + ) { + this(eventAssemblyService, runtimeDriverTimelineService, runtimeDerivedProjectionService, + tachographEsperScopeProcessingService, runtimeEventProcessingService, + tachographParityValidationService, null, null); } @PostMapping("/driver-events") @@ -93,6 +119,25 @@ public class UnifiedRuntimeProcessingController { return ResponseEntity.ok(runtimeDerivedProjectionService.loadDriverDerivedProjections(request)); } + + @GetMapping("/executions/plans") + public ResponseEntity> listRuntimeProcessingPlans() { + if (runtimeProcessingExecutionService == null) { + throw new IllegalStateException("Runtime processing execution service is not configured."); + } + return ResponseEntity.ok(runtimeProcessingExecutionService.listPlans()); + } + + @PostMapping("/executions") + public ResponseEntity runRuntimeProcessingExecution( + @RequestBody RuntimeProcessingExecutionApiRequest request + ) { + if (runtimeProcessingExecutionService == null) { + throw new IllegalStateException("Runtime processing execution service is not configured."); + } + return ResponseEntity.ok(runtimeProcessingExecutionService.execute(request)); + } + @GetMapping("/event-processing/profiles") public ResponseEntity> listEventProcessingProfiles() { if (runtimeEventProcessingService == null) { @@ -122,6 +167,17 @@ public class UnifiedRuntimeProcessingController { return ResponseEntity.ok(tachographParityValidationService.validate(request)); } + + @PostMapping("/event-processing/validation/mixed-source-evidence") + public ResponseEntity validateMixedSourceEvidence( + @RequestBody RuntimeMixedSourceEvidenceValidationApiRequest request + ) { + if (mixedSourceEvidenceValidationService == null) { + throw new IllegalStateException("Runtime mixed-source evidence validation service is not configured."); + } + return ResponseEntity.ok(mixedSourceEvidenceValidationService.validate(request)); + } + @PostMapping("/tachograph/esper-processing") public ResponseEntity runTachographEsperProcessing( @RequestBody UnifiedRuntimeProcessingApiRequest request @@ -135,6 +191,8 @@ public class UnifiedRuntimeProcessingController { if (tachographEsperScopeProcessingService == null) { throw new IllegalStateException("Tachograph Esper scope processing service is not configured."); } - return ResponseEntity.ok(tachographEsperScopeProcessingService.processScope(request)); + return ResponseEntity.ok(UnifiedRuntimeTachographEsperScopeResultDto.fromDriverWorkingTime( + tachographEsperScopeProcessingService.processScope(request) + )); } } diff --git a/src/main/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDto.java b/src/main/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDto.java new file mode 100644 index 0000000..d0e14be --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/driverworkingtime/dto/DriverWorkingTimeProcessingResultDto.java @@ -0,0 +1,52 @@ +package at.procon.eventhub.processing.driverworkingtime.dto; + +import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialHomeOvernightStayIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleOvernightStayIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperPotentialInVehicleTripIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperSupportGeoEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsageIntervalEvent; +import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.UUID; + +public record DriverWorkingTimeProcessingResultDto( + UUID sessionId, + String driverKey, + String sourceKind, + OffsetDateTime loadedFrom, + OffsetDateTime loadedTo, + OffsetDateTime requestedFrom, + OffsetDateTime requestedTo, + int activityIntervalCount, + int drivingIntervalCount, + int drivingInterruptionIntervalCount, + int drivingInterruptionVehicleChangeIntervalCount, + int dailyWeeklyRestCandidateIntervalCount, + int dailyWeeklyRestCandidateCoverageIntervalCount, + int unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount, + int potentialHomeOvernightStayIntervalCount, + int potentialInVehicleOvernightStayIntervalCount, + int potentialInVehicleTripIntervalCount, + int vehicleUsageIntervalCount, + int vuCardAbsentIntervalCount, + int supportGeoEventCount, + List activityIntervals, + List drivingIntervals, + List drivingInterruptionIntervals, + List drivingInterruptionVehicleChangeIntervals, + List dailyWeeklyRestCandidateIntervals, + List dailyWeeklyRestCandidateCoverageIntervals, + List unclassifiedDailyWeeklyRestCandidateCoverageIntervals, + List potentialHomeOvernightStayIntervals, + List potentialInVehicleOvernightStayIntervals, + List potentialInVehicleTripIntervals, + List vehicleUsageIntervals, + List vuCardAbsentIntervals, + List supportGeoEvents, + List notes +) { +} diff --git a/src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeProcessingCore.java b/src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeProcessingCore.java new file mode 100644 index 0000000..06242ac --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/driverworkingtime/service/DriverWorkingTimeProcessingCore.java @@ -0,0 +1,28 @@ +package at.procon.eventhub.processing.driverworkingtime.service; + +import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto; +import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingCore; +import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingInput; +import org.springframework.stereotype.Service; + +/** + * Source-neutral driver working-time processing core. + * + *

Tachograph file/database data is only one source of the canonical driver activity, + * vehicle-usage, and support-evidence event streams consumed here. The legacy + * TachographEsperProcessingCore delegates to the same processing logic for backward + * compatibility.

+ */ +@Service +public class DriverWorkingTimeProcessingCore { + + private final TachographEsperProcessingCore delegate; + + public DriverWorkingTimeProcessingCore(TachographEsperProcessingCore delegate) { + this.delegate = delegate; + } + + public DriverWorkingTimeProcessingResultDto process(TachographEsperProcessingInput input) { + return delegate.processDriverWorkingTime(input); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/dto/RuntimeSupportEvidenceNormalizationDebugDto.java b/src/main/java/at/procon/eventhub/processing/dto/RuntimeSupportEvidenceNormalizationDebugDto.java new file mode 100644 index 0000000..b52a0f4 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/dto/RuntimeSupportEvidenceNormalizationDebugDto.java @@ -0,0 +1,17 @@ +package at.procon.eventhub.processing.dto; + +import java.util.List; + +public record RuntimeSupportEvidenceNormalizationDebugDto( + int inputEventCount, + int normalizedSupportEvidenceEventCount, + int unchangedEventCount, + List notes +) { + public RuntimeSupportEvidenceNormalizationDebugDto { + inputEventCount = Math.max(0, inputEventCount); + normalizedSupportEvidenceEventCount = Math.max(0, normalizedSupportEvidenceEventCount); + unchangedEventCount = Math.max(0, unchangedEventCount); + notes = notes == null ? List.of() : List.copyOf(notes); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeDerivedProjectionResultDto.java b/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeDerivedProjectionResultDto.java index e13c4ab..d1c774a 100644 --- a/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeDerivedProjectionResultDto.java +++ b/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeDerivedProjectionResultDto.java @@ -2,7 +2,7 @@ package at.procon.eventhub.processing.dto; import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef; import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; -import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; +import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto; import java.util.List; public record UnifiedRuntimeDerivedProjectionResultDto( @@ -12,8 +12,9 @@ public record UnifiedRuntimeDerivedProjectionResultDto( int expandedVehicleEventCount, int mergedEventCount, List discoveredVehicles, - TachographEsperDriverProcessingResultDto projection, + DriverWorkingTimeProcessingResultDto projection, List notes, + RuntimeSupportEvidenceNormalizationDebugDto supportEvidenceNormalization, RuntimeDriverPartitionDebugDto partitionDebug ) { public UnifiedRuntimeDerivedProjectionResultDto { @@ -28,7 +29,7 @@ public record UnifiedRuntimeDerivedProjectionResultDto( int expandedVehicleEventCount, int mergedEventCount, List discoveredVehicles, - TachographEsperDriverProcessingResultDto projection, + DriverWorkingTimeProcessingResultDto projection, List notes ) { this( @@ -40,10 +41,38 @@ public record UnifiedRuntimeDerivedProjectionResultDto( discoveredVehicles, projection, notes, + null, null ); } + public UnifiedRuntimeDerivedProjectionResultDto( + UnifiedRuntimeProcessingRequest request, + int driverSeedEventCount, + int discoveredVehicleCount, + int expandedVehicleEventCount, + int mergedEventCount, + List discoveredVehicles, + DriverWorkingTimeProcessingResultDto projection, + List notes, + RuntimeSupportEvidenceNormalizationDebugDto supportEvidenceNormalization + ) { + this( + request, + driverSeedEventCount, + discoveredVehicleCount, + expandedVehicleEventCount, + mergedEventCount, + discoveredVehicles, + projection, + notes, + supportEvidenceNormalization, + null + ); + } + + + public UnifiedRuntimeDerivedProjectionResultDto withPartitionDebug(RuntimeDriverPartitionDebugDto debug) { return new UnifiedRuntimeDerivedProjectionResultDto( request, @@ -54,6 +83,7 @@ public record UnifiedRuntimeDerivedProjectionResultDto( discoveredVehicles, projection, notes, + supportEvidenceNormalization, debug ); } diff --git a/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeDriverWorkingTimeScopeResultDto.java b/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeDriverWorkingTimeScopeResultDto.java new file mode 100644 index 0000000..1012a74 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeDriverWorkingTimeScopeResultDto.java @@ -0,0 +1,74 @@ +package at.procon.eventhub.processing.dto; + +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto; +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto; +import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef; +import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public record UnifiedRuntimeDriverWorkingTimeScopeResultDto( + UnifiedRuntimeProcessingRequest request, + int inputEventCount, + int selectedDriverCount, + int discoveredVehicleCount, + List discoveredVehicles, + Map driverResults, + Map partitionDebugByDriver, + List notes, + List warnings +) { + public UnifiedRuntimeDriverWorkingTimeScopeResultDto { + discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles); + driverResults = driverResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(driverResults)); + partitionDebugByDriver = partitionDebugByDriver == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(partitionDebugByDriver)); + notes = notes == null ? List.of() : List.copyOf(notes); + warnings = warnings == null ? List.of() : List.copyOf(warnings); + } + + public static UnifiedRuntimeDriverWorkingTimeScopeResultDto fromGenericRuntimeEventProcessingResult( + RuntimeEventProcessingResultDto genericResult + ) { + if (genericResult == null) { + throw new IllegalArgumentException("genericResult must not be null"); + } + LinkedHashMap driverResults = new LinkedHashMap<>(); + for (Map.Entry entry : genericResult.partitionResults().entrySet()) { + Object value = entry.getValue().result(); + if (value instanceof UnifiedRuntimeDerivedProjectionResultDto projectionResult) { + driverResults.put(entry.getKey(), projectionResult); + } else { + throw new IllegalArgumentException("Cannot convert generic partition result for key " + + entry.getKey() + " to UnifiedRuntimeDerivedProjectionResultDto: " + + (value == null ? "null" : value.getClass().getName())); + } + } + return new UnifiedRuntimeDriverWorkingTimeScopeResultDto( + genericResult.request(), + genericResult.inputEventCount(), + genericResult.selectedPartitionCount(), + genericResult.discoveredVehicleCount(), + genericResult.discoveredVehicles(), + driverResults, + extractPartitionDebug(genericResult), + genericResult.notes(), + genericResult.warnings() + ); + } + + private static Map extractPartitionDebug( + RuntimeEventProcessingResultDto genericResult + ) { + LinkedHashMap debugByDriver = new LinkedHashMap<>(); + for (Map.Entry entry : genericResult.partitionResults().entrySet()) { + Object debug = entry.getValue().metadata().get("partitionDebug"); + if (debug instanceof RuntimeDriverPartitionDebugDto partitionDebug) { + debugByDriver.put(entry.getKey(), partitionDebug); + } + } + return debugByDriver; + } + +} diff --git a/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeTachographEsperScopeResultDto.java b/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeTachographEsperScopeResultDto.java index 0d9b0a5..8e8bed1 100644 --- a/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeTachographEsperScopeResultDto.java +++ b/src/main/java/at/procon/eventhub/processing/dto/UnifiedRuntimeTachographEsperScopeResultDto.java @@ -28,6 +28,25 @@ public record UnifiedRuntimeTachographEsperScopeResultDto( warnings = warnings == null ? List.of() : List.copyOf(warnings); } + public static UnifiedRuntimeTachographEsperScopeResultDto fromDriverWorkingTime( + UnifiedRuntimeDriverWorkingTimeScopeResultDto result + ) { + if (result == null) { + return null; + } + return new UnifiedRuntimeTachographEsperScopeResultDto( + result.request(), + result.inputEventCount(), + result.selectedDriverCount(), + result.discoveredVehicleCount(), + result.discoveredVehicles(), + result.driverResults(), + result.partitionDebugByDriver(), + result.notes(), + result.warnings() + ); + } + public static UnifiedRuntimeTachographEsperScopeResultDto fromGenericRuntimeEventProcessingResult( RuntimeEventProcessingResultDto genericResult ) { diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/dto/RuntimeEventProcessingPartitionResultDto.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/dto/RuntimeEventProcessingPartitionResultDto.java index 982ba2b..4038d6a 100644 --- a/src/main/java/at/procon/eventhub/processing/eventprocessing/dto/RuntimeEventProcessingPartitionResultDto.java +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/dto/RuntimeEventProcessingPartitionResultDto.java @@ -1,5 +1,6 @@ package at.procon.eventhub.processing.eventprocessing.dto; +import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleResult; import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -9,9 +10,21 @@ public record RuntimeEventProcessingPartitionResultDto( String partitionKey, String resultType, Object result, - Map metadata + Map metadata, + Map moduleResults ) { public RuntimeEventProcessingPartitionResultDto { metadata = metadata == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(metadata)); + moduleResults = moduleResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(moduleResults)); + } + + public RuntimeEventProcessingPartitionResultDto( + String partitionType, + String partitionKey, + String resultType, + Object result, + Map metadata + ) { + this(partitionType, partitionKey, resultType, result, metadata, Map.of()); } } diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/dto/RuntimeEventProcessingResultDto.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/dto/RuntimeEventProcessingResultDto.java index 48568a2..33c999b 100644 --- a/src/main/java/at/procon/eventhub/processing/eventprocessing/dto/RuntimeEventProcessingResultDto.java +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/dto/RuntimeEventProcessingResultDto.java @@ -2,6 +2,7 @@ package at.procon.eventhub.processing.eventprocessing.dto; import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy; import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef; +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionResultDto; import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; import java.util.Collections; import java.util.LinkedHashMap; @@ -26,4 +27,26 @@ public record RuntimeEventProcessingResultDto( notes = notes == null ? List.of() : List.copyOf(notes); warnings = warnings == null ? List.of() : List.copyOf(warnings); } + + public static RuntimeEventProcessingResultDto fromExecution( + RuntimeProcessingExecutionResultDto result, + String legacyProfileKey + ) { + if (result == null) { + throw new IllegalArgumentException("result must not be null"); + } + return new RuntimeEventProcessingResultDto( + legacyProfileKey == null || legacyProfileKey.isBlank() ? result.processingPlanKey() : legacyProfileKey, + result.partitioningStrategy(), + result.request(), + result.inputEventCount(), + result.selectedPartitionCount(), + result.discoveredVehicleCount(), + result.discoveredVehicles(), + result.partitionResults(), + result.notes(), + result.warnings() + ); + } + } diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/AbstractDriverWorkingTimePhaseModule.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/AbstractDriverWorkingTimePhaseModule.java new file mode 100644 index 0000000..b8aadd1 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/AbstractDriverWorkingTimePhaseModule.java @@ -0,0 +1,60 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +abstract class AbstractDriverWorkingTimePhaseModule implements RuntimeProcessingModule { + + private final String moduleKey; + private final String displayName; + private final String description; + private final String engine; + private final Set outputTypes; + + protected AbstractDriverWorkingTimePhaseModule( + String moduleKey, + String displayName, + String description, + String engine, + Set outputTypes + ) { + this.moduleKey = moduleKey; + this.displayName = displayName; + this.description = description; + this.engine = engine; + this.outputTypes = outputTypes == null ? Set.of() : Set.copyOf(outputTypes); + } + + @Override + public String moduleKey() { + return moduleKey; + } + + @Override + public RuntimeProcessingModuleDescriptorDto descriptor() { + return new RuntimeProcessingModuleDescriptorDto( + moduleKey, + displayName, + description, + engine, + outputTypes + ); + } + + @Override + public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) { + Map metadata = new LinkedHashMap<>(); + metadata.put("executionModel", "delegated"); + metadata.put("delegatedTo", DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS); + metadata.put("note", "This logical module is currently executed inside the driver-working-time derived projections adapter. It is registered separately so it can be split into a standalone EPL/Java module without changing the processing plan contract."); + return new RuntimeProcessingModuleResult( + moduleKey, + RuntimeProcessingModuleStatus.SUCCESS, + null, + metadata, + java.util.List.of() + ); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverActivityIntervalsModule.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverActivityIntervalsModule.java new file mode 100644 index 0000000..7570250 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverActivityIntervalsModule.java @@ -0,0 +1,82 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.processing.eventprocessing.module.epl.DriverWorkingTimeEplEventMapper; +import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplInputEventStream; +import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModule; +import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModuleDefinition; +import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModuleExecutionResult; +import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModuleExecutor; +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class DriverActivityIntervalsModule implements RuntimeEplModule { + + public static final String OUTPUT_STATEMENT = "driverActivityIntervals"; + private static final String EPL_RESOURCE = "esper/runtime-driver-activity-intervals.epl"; + + private final RuntimeEplModuleExecutor eplModuleExecutor; + + @Autowired + public DriverActivityIntervalsModule(RuntimeEplModuleExecutor eplModuleExecutor) { + this.eplModuleExecutor = eplModuleExecutor; + } + + /** Compatibility constructor for legacy tests that instantiate a local module registry. */ + public DriverActivityIntervalsModule() { + this(new RuntimeEplModuleExecutor()); + } + + @Override + public String moduleKey() { + return DriverWorkingTimeModuleKeys.EVENT_TO_ACTIVITY_INTERVALS; + } + + @Override + public RuntimeProcessingModuleDescriptorDto descriptor() { + return new RuntimeProcessingModuleDescriptorDto( + moduleKey(), + "Event to activity intervals", + "EPL module that pairs canonical DRIVER_ACTIVITY START/END point events into source-neutral driver activity intervals.", + engine(), + Set.of("DriverActivityIntervalEvent") + ); + } + + @Override + public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) { + List sourceEvents = DriverWorkingTimeEplEventMapper.sourceEvents(context); + List> pointEvents = DriverWorkingTimeEplEventMapper.activityPointEvents(sourceEvents); + RuntimeEplModuleExecutionResult eplResult = eplModuleExecutor.execute(new RuntimeEplModuleDefinition( + moduleKey(), + Map.of( + DriverWorkingTimeEplEventMapper.DRIVER_ACTIVITY_POINT_EVENT, + DriverWorkingTimeEplEventMapper.activityPointEventDefinition() + ), + List.of(EPL_RESOURCE), + List.of(OUTPUT_STATEMENT), + List.of(new RuntimeEplInputEventStream( + DriverWorkingTimeEplEventMapper.DRIVER_ACTIVITY_POINT_EVENT, + pointEvents + )) + )); + List> intervals = eplResult.output(OUTPUT_STATEMENT); + Map metadata = new LinkedHashMap<>(eplResult.metadata()); + metadata.put("inputEventCount", sourceEvents.size()); + metadata.put("activityPointEventCount", pointEvents.size()); + metadata.put("activityIntervalCount", intervals.size()); + return new RuntimeProcessingModuleResult( + moduleKey(), + RuntimeProcessingModuleStatus.SUCCESS, + intervals, + metadata, + List.of() + ); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverVehicleUsageIntervalsModule.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverVehicleUsageIntervalsModule.java new file mode 100644 index 0000000..d34ebaa --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverVehicleUsageIntervalsModule.java @@ -0,0 +1,82 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.processing.eventprocessing.module.epl.DriverWorkingTimeEplEventMapper; +import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplInputEventStream; +import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModule; +import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModuleDefinition; +import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModuleExecutionResult; +import at.procon.eventhub.processing.eventprocessing.module.epl.RuntimeEplModuleExecutor; +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class DriverVehicleUsageIntervalsModule implements RuntimeEplModule { + + public static final String OUTPUT_STATEMENT = "driverVehicleUsageIntervals"; + private static final String EPL_RESOURCE = "esper/runtime-driver-vehicle-usage-intervals.epl"; + + private final RuntimeEplModuleExecutor eplModuleExecutor; + + @Autowired + public DriverVehicleUsageIntervalsModule(RuntimeEplModuleExecutor eplModuleExecutor) { + this.eplModuleExecutor = eplModuleExecutor; + } + + /** Compatibility constructor for legacy tests that instantiate a local module registry. */ + public DriverVehicleUsageIntervalsModule() { + this(new RuntimeEplModuleExecutor()); + } + + @Override + public String moduleKey() { + return DriverWorkingTimeModuleKeys.EVENT_TO_VEHICLE_USAGE_INTERVALS; + } + + @Override + public RuntimeProcessingModuleDescriptorDto descriptor() { + return new RuntimeProcessingModuleDescriptorDto( + moduleKey(), + "Event to vehicle usage intervals", + "EPL module that pairs canonical driver/card INSERT/WITHDRAW point events into source-neutral driver vehicle-usage intervals.", + engine(), + Set.of("DriverVehicleUsageIntervalEvent") + ); + } + + @Override + public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) { + List sourceEvents = DriverWorkingTimeEplEventMapper.sourceEvents(context); + List> pointEvents = DriverWorkingTimeEplEventMapper.vehicleUsagePointEvents(sourceEvents); + RuntimeEplModuleExecutionResult eplResult = eplModuleExecutor.execute(new RuntimeEplModuleDefinition( + moduleKey(), + Map.of( + DriverWorkingTimeEplEventMapper.DRIVER_VEHICLE_USAGE_POINT_EVENT, + DriverWorkingTimeEplEventMapper.vehicleUsagePointEventDefinition() + ), + List.of(EPL_RESOURCE), + List.of(OUTPUT_STATEMENT), + List.of(new RuntimeEplInputEventStream( + DriverWorkingTimeEplEventMapper.DRIVER_VEHICLE_USAGE_POINT_EVENT, + pointEvents + )) + )); + List> intervals = eplResult.output(OUTPUT_STATEMENT); + Map metadata = new LinkedHashMap<>(eplResult.metadata()); + metadata.put("inputEventCount", sourceEvents.size()); + metadata.put("vehicleUsagePointEventCount", pointEvents.size()); + metadata.put("vehicleUsageIntervalCount", intervals.size()); + return new RuntimeProcessingModuleResult( + moduleKey(), + RuntimeProcessingModuleStatus.SUCCESS, + intervals, + metadata, + List.of() + ); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverVehicleUsageMergeModule.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverVehicleUsageMergeModule.java new file mode 100644 index 0000000..192c3e6 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverVehicleUsageMergeModule.java @@ -0,0 +1,18 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +import java.util.Set; +import org.springframework.stereotype.Component; + +@Component +public class DriverVehicleUsageMergeModule extends AbstractDriverWorkingTimePhaseModule { + + public DriverVehicleUsageMergeModule() { + super( + DriverWorkingTimeModuleKeys.VEHICLE_USAGE_MERGE, + "Vehicle usage merge", + "Merges adjacent or continuous same-driver/same-vehicle usage intervals, including 23:59:59 to 00:00:00 continuations.", + "JAVA/ESPER", + Set.of("DriverVehicleUsageIntervalInputEvent") + ); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeDerivedProjectionsModule.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeDerivedProjectionsModule.java new file mode 100644 index 0000000..dc00bba --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeDerivedProjectionsModule.java @@ -0,0 +1,77 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto; +import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest; +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto; +import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import org.springframework.stereotype.Component; + +@Component +public class DriverWorkingTimeDerivedProjectionsModule implements RuntimeProcessingModule { + + private final RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService; + + public DriverWorkingTimeDerivedProjectionsModule( + RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService + ) { + this.scopeProcessingService = scopeProcessingService; + } + + @Override + public String moduleKey() { + return DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS; + } + + @Override + public RuntimeProcessingModuleDescriptorDto descriptor() { + return new RuntimeProcessingModuleDescriptorDto( + moduleKey(), + "Driving-derived projections", + "Executes the shared driver working-time pipeline for driving interruptions, rest candidates, card-absence coverage, overnight candidates, and trip candidates.", + "ESPER+JAVA", + Set.of("DriverWorkingTimeProcessingResultDto") + ); + } + + @Override + public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) { + UnifiedRuntimeProcessingApiRequest scopeRequest = scopeRequest(context); + boolean includePartitionDebug = booleanAttribute(context, "includePartitionDebug", false); + UnifiedRuntimeDriverWorkingTimeScopeResultDto result = scopeProcessingService.processScope( + scopeRequest, + includePartitionDebug + ); + + Map metadata = new LinkedHashMap<>(); + metadata.put("inputEventCount", result.inputEventCount()); + metadata.put("selectedDriverCount", result.selectedDriverCount()); + metadata.put("discoveredVehicleCount", result.discoveredVehicleCount()); + metadata.put("driverResultCount", result.driverResults().size()); + return new RuntimeProcessingModuleResult( + moduleKey(), + RuntimeProcessingModuleStatus.SUCCESS, + result, + metadata, + result.warnings() + ); + } + + private UnifiedRuntimeProcessingApiRequest scopeRequest(RuntimeProcessingModuleContext context) { + Object value = context.attributes().get("runtimeScopeApiRequest"); + if (value instanceof UnifiedRuntimeProcessingApiRequest request) { + return request; + } + return context.request().sourceSelection(); + } + + private boolean booleanAttribute(RuntimeProcessingModuleContext context, String key, boolean fallback) { + Object value = context.attributes().get(key); + if (value instanceof Boolean booleanValue) { + return booleanValue; + } + return fallback; + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeModuleKeys.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeModuleKeys.java new file mode 100644 index 0000000..5a52668 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/DriverWorkingTimeModuleKeys.java @@ -0,0 +1,15 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +public final class DriverWorkingTimeModuleKeys { + + public static final String RUNTIME_EVENT_ASSEMBLY = "runtime-event-assembly"; + public static final String EVENT_TO_ACTIVITY_INTERVALS = "event-to-activity-intervals"; + public static final String EVENT_TO_VEHICLE_USAGE_INTERVALS = "event-to-vehicle-usage-intervals"; + public static final String VEHICLE_USAGE_MERGE = "vehicle-usage-merge"; + public static final String VEHICLE_EVIDENCE_ATTACHMENT = "vehicle-evidence-attachment"; + public static final String SUPPORT_EVIDENCE_NORMALIZATION = "support-evidence-normalization"; + public static final String DRIVING_DERIVED_PROJECTIONS = "driving-derived-projections"; + + private DriverWorkingTimeModuleKeys() { + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeEventAssemblyModule.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeEventAssemblyModule.java new file mode 100644 index 0000000..335037f --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeEventAssemblyModule.java @@ -0,0 +1,62 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest; +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto; +import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle; +import at.procon.eventhub.processing.service.UnifiedRuntimeEventAssemblyService; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import org.springframework.stereotype.Component; + +@Component +public class RuntimeEventAssemblyModule implements RuntimeProcessingModule { + + private final UnifiedRuntimeEventAssemblyService eventAssemblyService; + + public RuntimeEventAssemblyModule(UnifiedRuntimeEventAssemblyService eventAssemblyService) { + this.eventAssemblyService = eventAssemblyService; + } + + @Override + public String moduleKey() { + return DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY; + } + + @Override + public RuntimeProcessingModuleDescriptorDto descriptor() { + return new RuntimeProcessingModuleDescriptorDto( + moduleKey(), + "Runtime event assembly", + "Loads and merges canonical runtime events from the selected source scope before plan-specific processing.", + "JAVA", + Set.of("UnifiedRuntimeEventBundle") + ); + } + + @Override + public RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context) { + UnifiedRuntimeProcessingApiRequest scopeRequest = scopeRequest(context); + UnifiedRuntimeEventBundle bundle = eventAssemblyService.assembleDriverScopedEvents(scopeRequest.toRuntimeRequest()); + Map metadata = new LinkedHashMap<>(); + metadata.put("driverSeedEventCount", bundle.driverSeedEvents().size()); + metadata.put("expandedVehicleEventCount", bundle.expandedVehicleEvents().size()); + metadata.put("mergedEventCount", bundle.mergedEvents().size()); + metadata.put("discoveredVehicleCount", bundle.discoveredVehicles().size()); + return new RuntimeProcessingModuleResult( + moduleKey(), + RuntimeProcessingModuleStatus.SUCCESS, + bundle, + metadata, + bundle.notes() + ); + } + + private UnifiedRuntimeProcessingApiRequest scopeRequest(RuntimeProcessingModuleContext context) { + Object value = context.attributes().get("runtimeScopeApiRequest"); + if (value instanceof UnifiedRuntimeProcessingApiRequest request) { + return request; + } + return context.request().sourceSelection(); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModule.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModule.java new file mode 100644 index 0000000..65f23a5 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModule.java @@ -0,0 +1,20 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto; + +/** + * Source-neutral runtime processing module. + * + *

Modules are the executable building blocks used by processing plans. A module can be + * implemented in EPL, Java, or a combination of both. The first predefined driver-working-time + * plan still uses an adapter over the existing processing services, but its descriptor now + * exposes the logical module sequence so the plan can be split into concrete modules incrementally.

+ */ +public interface RuntimeProcessingModule { + + String moduleKey(); + + RuntimeProcessingModuleDescriptorDto descriptor(); + + RuntimeProcessingModuleResult execute(RuntimeProcessingModuleContext context); +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleContext.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleContext.java new file mode 100644 index 0000000..f9e8bbb --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleContext.java @@ -0,0 +1,21 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionApiRequest; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public record RuntimeProcessingModuleContext( + RuntimeProcessingExecutionApiRequest request, + List events, + Map attributes, + Map previousResults +) { + public RuntimeProcessingModuleContext { + events = events == null ? List.of() : List.copyOf(events); + attributes = attributes == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(attributes)); + previousResults = previousResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(previousResults)); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleRegistry.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleRegistry.java new file mode 100644 index 0000000..ad957af --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleRegistry.java @@ -0,0 +1,48 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingModuleDescriptorDto; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.springframework.stereotype.Component; + +@Component +public class RuntimeProcessingModuleRegistry { + + private final Map modulesByKey; + + public RuntimeProcessingModuleRegistry(List modules) { + LinkedHashMap byKey = new LinkedHashMap<>(); + if (modules != null) { + for (RuntimeProcessingModule module : modules) { + if (module == null || module.moduleKey() == null || module.moduleKey().isBlank()) { + continue; + } + RuntimeProcessingModule previous = byKey.putIfAbsent(module.moduleKey(), module); + if (previous != null) { + throw new IllegalStateException("Duplicate runtime processing module key: " + module.moduleKey()); + } + } + } + this.modulesByKey = Map.copyOf(byKey); + } + + public Collection modules() { + return modulesByKey.values(); + } + + public List descriptors() { + return modulesByKey.values().stream() + .map(RuntimeProcessingModule::descriptor) + .toList(); + } + + public RuntimeProcessingModule require(String moduleKey) { + RuntimeProcessingModule module = modulesByKey.get(moduleKey); + if (module == null) { + throw new IllegalArgumentException("Unknown runtime processing module: " + moduleKey); + } + return module; + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleResult.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleResult.java new file mode 100644 index 0000000..99bc58d --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleResult.java @@ -0,0 +1,19 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public record RuntimeProcessingModuleResult( + String moduleKey, + RuntimeProcessingModuleStatus status, + Object output, + Map metadata, + List warnings +) { + public RuntimeProcessingModuleResult { + metadata = metadata == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(metadata)); + warnings = warnings == null ? List.of() : List.copyOf(warnings); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleStatus.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleStatus.java new file mode 100644 index 0000000..809ed93 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingModuleStatus.java @@ -0,0 +1,7 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +public enum RuntimeProcessingModuleStatus { + SUCCESS, + SKIPPED, + FAILED +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingPipelineExecutor.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingPipelineExecutor.java new file mode 100644 index 0000000..db179bf --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/RuntimeProcessingPipelineExecutor.java @@ -0,0 +1,46 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionApiRequest; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.springframework.stereotype.Service; + +@Service +public class RuntimeProcessingPipelineExecutor { + + private final RuntimeProcessingModuleRegistry moduleRegistry; + + public RuntimeProcessingPipelineExecutor(RuntimeProcessingModuleRegistry moduleRegistry) { + this.moduleRegistry = moduleRegistry; + } + + public Map execute( + RuntimeProcessingExecutionApiRequest request, + List moduleKeys, + RuntimeProcessingModuleContext initialContext + ) { + LinkedHashMap results = new LinkedHashMap<>(); + RuntimeProcessingModuleContext context = initialContext == null + ? new RuntimeProcessingModuleContext(request, List.of(), Map.of(), Map.of()) + : initialContext; + for (String moduleKey : moduleKeys == null ? List.of() : moduleKeys) { + if (moduleKey == null || moduleKey.isBlank()) { + continue; + } + RuntimeProcessingModule module = moduleRegistry.require(moduleKey.trim()); + RuntimeProcessingModuleResult result = module.execute(context); + results.put(module.moduleKey(), result); + context = new RuntimeProcessingModuleContext( + request, + context.events(), + context.attributes(), + results + ); + if (result.status() == RuntimeProcessingModuleStatus.FAILED) { + break; + } + } + return Map.copyOf(results); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/SupportEvidenceNormalizationModule.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/SupportEvidenceNormalizationModule.java new file mode 100644 index 0000000..b5df72c --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/SupportEvidenceNormalizationModule.java @@ -0,0 +1,18 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +import java.util.Set; +import org.springframework.stereotype.Component; + +@Component +public class SupportEvidenceNormalizationModule extends AbstractDriverWorkingTimePhaseModule { + + public SupportEvidenceNormalizationModule() { + super( + DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION, + "Support evidence normalization", + "Normalizes mixed-source support evidence for driver working-time processing.", + "JAVA", + Set.of("RuntimeSupportEvidenceNormalizationDebugDto") + ); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/VehicleEvidenceAttachmentModule.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/VehicleEvidenceAttachmentModule.java new file mode 100644 index 0000000..9300063 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/VehicleEvidenceAttachmentModule.java @@ -0,0 +1,18 @@ +package at.procon.eventhub.processing.eventprocessing.module; + +import java.util.Set; +import org.springframework.stereotype.Component; + +@Component +public class VehicleEvidenceAttachmentModule extends AbstractDriverWorkingTimePhaseModule { + + public VehicleEvidenceAttachmentModule() { + super( + DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT, + "Vehicle evidence attachment", + "Attaches vehicle-only evidence to driver partitions by overlapping driver vehicle-usage intervals.", + "JAVA", + Set.of("RuntimeDriverPartitionDebugDto") + ); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/DriverWorkingTimeEplEventMapper.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/DriverWorkingTimeEplEventMapper.java new file mode 100644 index 0000000..5bfbb6d --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/DriverWorkingTimeEplEventMapper.java @@ -0,0 +1,386 @@ +package at.procon.eventhub.processing.eventprocessing.module.epl; + +import at.procon.eventhub.dto.EventDomain; +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.dto.EventLifecycle; +import at.procon.eventhub.dto.EventType; +import at.procon.eventhub.processing.eventprocessing.module.DriverWorkingTimeModuleKeys; +import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleContext; +import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleResult; +import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle; +import com.fasterxml.jackson.databind.JsonNode; +import java.time.OffsetDateTime; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; + +public final class DriverWorkingTimeEplEventMapper { + + public static final String DRIVER_ACTIVITY_POINT_EVENT = "DriverActivityPointEvent"; + public static final String DRIVER_ACTIVITY_INTERVAL_EVENT = "DriverActivityIntervalEvent"; + public static final String DRIVER_VEHICLE_USAGE_POINT_EVENT = "DriverVehicleUsagePointEvent"; + public static final String DRIVER_VEHICLE_USAGE_INTERVAL_EVENT = "DriverVehicleUsageIntervalEvent"; + + private DriverWorkingTimeEplEventMapper() { + } + + public static List sourceEvents(RuntimeProcessingModuleContext context) { + RuntimeProcessingModuleResult assemblyResult = context.previousResults().get(DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY); + if (assemblyResult != null && assemblyResult.output() instanceof UnifiedRuntimeEventBundle bundle) { + return safeList(bundle.mergedEvents()); + } + return safeList(context.events()); + } + + public static List> activityPointEvents(List sourceEvents) { + return safeList(sourceEvents).stream() + .map(DriverWorkingTimeEplEventMapper::toActivityPointEvent) + .filter(Objects::nonNull) + .sorted(pointEventComparator()) + .toList(); + } + + public static List> vehicleUsagePointEvents(List sourceEvents) { + return safeList(sourceEvents).stream() + .map(DriverWorkingTimeEplEventMapper::toVehicleUsagePointEvent) + .filter(Objects::nonNull) + .sorted(pointEventComparator()) + .toList(); + } + + public static Map activityPointEventDefinition() { + Map definition = new LinkedHashMap<>(); + definition.put("sessionId", UUID.class); + definition.put("driverKey", String.class); + definition.put("eventId", String.class); + definition.put("intervalId", String.class); + definition.put("sourceRowId", String.class); + definition.put("sourceRowIds", java.util.List.class); + definition.put("activityType", String.class); + definition.put("lifecycle", String.class); + definition.put("occurredAt", OffsetDateTime.class); + definition.put("occurredAtEpochSecond", long.class); + definition.put("cardSlot", String.class); + definition.put("cardStatus", String.class); + definition.put("drivingStatus", String.class); + definition.put("registrationKey", String.class); + definition.put("vehicleKey", String.class); + definition.put("sourceKind", String.class); + definition.put("synthetic", boolean.class); + definition.put("clippedToRequestedPeriod", boolean.class); + definition.put("level", String.class); + return definition; + } + + public static Map activityIntervalEventDefinition() { + Map definition = new LinkedHashMap<>(); + definition.put("sessionId", UUID.class); + definition.put("driverKey", String.class); + definition.put("intervalId", String.class); + definition.put("activityType", String.class); + definition.put("cardSlot", String.class); + definition.put("cardStatus", String.class); + definition.put("drivingStatus", String.class); + definition.put("registrationKey", String.class); + definition.put("vehicleKey", String.class); + definition.put("sourceKind", String.class); + definition.put("firstSourceIntervalId", String.class); + definition.put("lastSourceIntervalId", String.class); + definition.put("startedAt", OffsetDateTime.class); + definition.put("endedAt", OffsetDateTime.class); + definition.put("startedAtEpochSecond", long.class); + definition.put("endedAtEpochSecond", long.class); + definition.put("durationSeconds", long.class); + definition.put("sourceIntervalIds", java.util.List.class); + definition.put("synthetic", boolean.class); + definition.put("clippedToRequestedPeriod", boolean.class); + definition.put("level", String.class); + return definition; + } + + public static Map vehicleUsagePointEventDefinition() { + Map definition = new LinkedHashMap<>(); + definition.put("sessionId", UUID.class); + definition.put("driverKey", String.class); + definition.put("eventId", String.class); + definition.put("intervalId", String.class); + definition.put("sourceRowId", String.class); + definition.put("sourceRowIds", java.util.List.class); + definition.put("lifecycle", String.class); + definition.put("occurredAt", OffsetDateTime.class); + definition.put("occurredAtEpochSecond", long.class); + definition.put("registrationKey", String.class); + definition.put("vehicleKey", String.class); + definition.put("sourceKind", String.class); + definition.put("odometerKm", Long.class); + return definition; + } + + public static Map vehicleUsageIntervalEventDefinition() { + Map definition = new LinkedHashMap<>(); + definition.put("sessionId", UUID.class); + definition.put("driverKey", String.class); + definition.put("intervalId", String.class); + definition.put("firstSourceIntervalId", String.class); + definition.put("lastSourceIntervalId", String.class); + definition.put("startedAt", OffsetDateTime.class); + definition.put("endedAt", OffsetDateTime.class); + definition.put("startedAtEpochSecond", long.class); + definition.put("endedAtEpochSecond", Long.class); + definition.put("durationSeconds", long.class); + definition.put("odometerBeginKm", Long.class); + definition.put("odometerEndKm", Long.class); + definition.put("registrationKey", String.class); + definition.put("vehicleKey", String.class); + definition.put("sourceKind", String.class); + definition.put("sourceIntervalIds", java.util.List.class); + return definition; + } + + private static Map toActivityPointEvent(EventHubEventDto sourceEvent) { + if (sourceEvent == null + || sourceEvent.eventDomain() != EventDomain.DRIVER_ACTIVITY + || (sourceEvent.lifecycle() != EventLifecycle.START && sourceEvent.lifecycle() != EventLifecycle.END) + || sourceEvent.occurredAt() == null) { + return null; + } + JsonNode raw = rawPayload(sourceEvent); + JsonNode attributes = attributes(sourceEvent); + String intervalId = firstNonBlank(text(raw, "intervalId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId()); + String driverKey = firstNonBlank(text(raw, "driverKey"), driverKey(sourceEvent)); + if (driverKey == null || intervalId == null) { + return null; + } + Map event = new LinkedHashMap<>(); + event.put("sessionId", sessionId(raw, sourceEvent)); + event.put("driverKey", driverKey); + event.put("eventId", sourceEvent.externalSourceEventId()); + event.put("intervalId", intervalId); + event.put("sourceRowId", firstNonBlank(text(raw, "sourceRowId"), intervalId)); + event.put("sourceRowIds", stringList(raw, "sourceRowIds", intervalId)); + event.put("activityType", firstNonBlank(text(raw, "activityType"), eventTypeAsActivity(sourceEvent.eventType()))); + event.put("lifecycle", sourceEvent.lifecycle().name()); + event.put("occurredAt", sourceEvent.occurredAt()); + event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond()); + event.put("cardSlot", firstNonBlank(text(raw, "cardSlot"), text(attributes, "cardSlot"))); + event.put("cardStatus", firstNonBlank(text(raw, "cardStatus"), text(attributes, "cardStatus"))); + event.put("drivingStatus", firstNonBlank(text(raw, "drivingStatus"), text(attributes, "drivingStatus"))); + event.put("registrationKey", firstNonBlank(text(raw, "registrationKey"), registrationKey(sourceEvent))); + event.put("vehicleKey", firstNonBlank(text(raw, "vehicleKey"), vehicleKey(sourceEvent))); + event.put("sourceKind", firstNonBlank(text(raw, "sourceKind"), sourceKind(sourceEvent))); + event.put("synthetic", booleanValue(raw, "synthetic", false)); + event.put("clippedToRequestedPeriod", booleanValue(raw, "clippedToRequestedPeriod", false)); + event.put("level", firstNonBlank(text(raw, "level"), "RAW_EVENT")); + return event; + } + + private static Map toVehicleUsagePointEvent(EventHubEventDto sourceEvent) { + if (sourceEvent == null + || sourceEvent.eventDomain() != EventDomain.DRIVER_CARD + || (sourceEvent.lifecycle() != EventLifecycle.INSERT && sourceEvent.lifecycle() != EventLifecycle.WITHDRAW) + || sourceEvent.occurredAt() == null) { + return null; + } + boolean supportedType = sourceEvent.eventType() == EventType.CARD_INSERTED + || sourceEvent.eventType() == EventType.CARD_WITHDRAWN; + if (!supportedType) { + return null; + } + JsonNode raw = rawPayload(sourceEvent); + String intervalId = firstNonBlank(text(raw, "intervalId"), text(raw, "sourceRowId"), sourceEvent.externalSourceEventId()); + String driverKey = firstNonBlank(text(raw, "driverKey"), driverKey(sourceEvent)); + if (driverKey == null || intervalId == null) { + return null; + } + Map event = new LinkedHashMap<>(); + event.put("sessionId", sessionId(raw, sourceEvent)); + event.put("driverKey", driverKey); + event.put("eventId", sourceEvent.externalSourceEventId()); + event.put("intervalId", intervalId); + event.put("sourceRowId", firstNonBlank(text(raw, "sourceRowId"), intervalId)); + event.put("sourceRowIds", stringList(raw, "sourceRowIds", intervalId)); + event.put("lifecycle", sourceEvent.lifecycle().name()); + event.put("occurredAt", sourceEvent.occurredAt()); + event.put("occurredAtEpochSecond", sourceEvent.occurredAt().toEpochSecond()); + event.put("registrationKey", firstNonBlank(text(raw, "registrationKey"), registrationKey(sourceEvent))); + event.put("vehicleKey", firstNonBlank(text(raw, "vehicleKey"), vehicleKey(sourceEvent))); + event.put("sourceKind", firstNonBlank(text(raw, "sourceKind"), sourceKind(sourceEvent))); + event.put("odometerKm", odometerKm(sourceEvent, raw)); + return event; + } + + private static Comparator> pointEventComparator() { + return Comparator + .comparing((Map event) -> (Long) event.get("occurredAtEpochSecond")) + .thenComparing(event -> lifecycleOrder(Objects.toString(event.get("lifecycle"), ""))) + .thenComparing(event -> Objects.toString(event.get("driverKey"), "")) + .thenComparing(event -> Objects.toString(event.get("intervalId"), "")) + .thenComparing(event -> Objects.toString(event.get("eventId"), "")); + } + + private static int lifecycleOrder(String lifecycle) { + return switch (lifecycle) { + case "INSERT", "START" -> 0; + case "WITHDRAW", "END" -> 1; + default -> 2; + }; + } + + private static JsonNode rawPayload(EventHubEventDto event) { + JsonNode payload = event.payload(); + if (payload == null || payload.isNull()) { + return null; + } + JsonNode raw = payload.get("raw"); + return raw == null || raw.isNull() ? payload : raw; + } + + private static JsonNode attributes(EventHubEventDto event) { + return event.eventDetails() == null ? null : event.eventDetails().attributes(); + } + + private static String text(JsonNode node, String field) { + if (node == null || field == null) { + return null; + } + JsonNode value = node.get(field); + if (value == null || value.isNull()) { + return null; + } + String text = value.asText(null); + return text == null || text.isBlank() ? null : text; + } + + private static boolean booleanValue(JsonNode node, String field, boolean fallback) { + if (node == null || field == null || node.get(field) == null || node.get(field).isNull()) { + return fallback; + } + return node.get(field).asBoolean(fallback); + } + + private static Long longValue(JsonNode node, String field) { + if (node == null || field == null || node.get(field) == null || node.get(field).isNull()) { + return null; + } + JsonNode value = node.get(field); + if (value.isNumber()) { + return value.asLong(); + } + try { + return Long.parseLong(value.asText()); + } catch (NumberFormatException ignored) { + return null; + } + } + + private static List stringList(JsonNode node, String field, String fallback) { + JsonNode value = node == null || field == null ? null : node.get(field); + if (value == null || value.isNull()) { + return fallback == null ? List.of() : List.of(fallback); + } + if (value.isArray()) { + List result = new ArrayList<>(); + value.forEach(item -> { + if (item != null && !item.isNull()) { + String text = item.asText(null); + if (text != null && !text.isBlank()) { + result.add(text); + } + } + }); + return result.isEmpty() && fallback != null ? List.of(fallback) : List.copyOf(result); + } + String text = value.asText(null); + return text == null || text.isBlank() ? (fallback == null ? List.of() : List.of(fallback)) : List.of(text); + } + + private static String firstNonBlank(String... values) { + if (values == null) { + return null; + } + for (String value : values) { + if (value != null && !value.isBlank()) { + return value.trim(); + } + } + return null; + } + + private static String eventTypeAsActivity(EventType eventType) { + if (eventType == null) { + return "UNKNOWN"; + } + return switch (eventType) { + case DRIVE -> "DRIVE"; + case WORK -> "WORK"; + case AVAILABILITY -> "AVAILABILITY"; + case BREAK_REST -> "BREAK_REST"; + default -> eventType.name(); + }; + } + + private static UUID sessionId(JsonNode raw, EventHubEventDto event) { + String rawSessionId = firstNonBlank( + text(raw, "sessionId"), + event.sourcePackageRef() == null ? null : event.sourcePackageRef().sourcePackageId() + ); + if (rawSessionId != null) { + try { + return UUID.fromString(rawSessionId); + } catch (IllegalArgumentException ignored) { + // DB-acquired source packages need not be UUID-based file sessions. + } + } + return new UUID(0L, 0L); + } + + private static String driverKey(EventHubEventDto event) { + if (event.driverRef() == null) { + return null; + } + if (event.driverRef().driverCard() != null && event.driverRef().driverCard().hasValue()) { + return event.driverRef().driverCard().stableKey(); + } + return event.driverRef().sourceEntityId(); + } + + private static String registrationKey(EventHubEventDto event) { + if (event.vehicleRef() == null || event.vehicleRef().vehicleRegistration() == null) { + return null; + } + return event.vehicleRef().vehicleRegistration().stableKey(); + } + + private static String vehicleKey(EventHubEventDto event) { + if (event.vehicleRef() == null) { + return null; + } + return firstNonBlank(event.vehicleRef().vin(), event.vehicleRef().sourceVehicleEntityId()); + } + + private static String sourceKind(EventHubEventDto event) { + return event.packageInfo() == null || event.packageInfo().eventSource() == null + ? null + : event.packageInfo().eventSource().sourceKind(); + } + + private static Long odometerKm(EventHubEventDto event, JsonNode raw) { + Long explicit = longValue(raw, event.lifecycle() == EventLifecycle.WITHDRAW ? "odometerEndKm" : "odometerBeginKm"); + if (explicit != null) { + return explicit; + } + explicit = longValue(raw, "odometerKm"); + if (explicit != null) { + return explicit; + } + return event.odometerM() == null ? null : event.odometerM() / 1_000L; + } + + private static List safeList(List values) { + return values == null ? List.of() : values; + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplInputEventStream.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplInputEventStream.java new file mode 100644 index 0000000..e24af30 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplInputEventStream.java @@ -0,0 +1,17 @@ +package at.procon.eventhub.processing.eventprocessing.module.epl; + +import java.util.List; +import java.util.Map; + +public record RuntimeEplInputEventStream( + String eventTypeName, + List> events +) { + public RuntimeEplInputEventStream { + if (eventTypeName == null || eventTypeName.isBlank()) { + throw new IllegalArgumentException("eventTypeName must not be blank"); + } + eventTypeName = eventTypeName.trim(); + events = events == null ? List.of() : List.copyOf(events); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModule.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModule.java new file mode 100644 index 0000000..62e470f --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModule.java @@ -0,0 +1,10 @@ +package at.procon.eventhub.processing.eventprocessing.module.epl; + +import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModule; + +public interface RuntimeEplModule extends RuntimeProcessingModule { + + default String engine() { + return "EPL"; + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModuleDefinition.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModuleDefinition.java new file mode 100644 index 0000000..b0ea60a --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModuleDefinition.java @@ -0,0 +1,31 @@ +package at.procon.eventhub.processing.eventprocessing.module.epl; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public record RuntimeEplModuleDefinition( + String moduleKey, + Map> eventTypes, + List eplResources, + List outputStatementNames, + List inputStreams +) { + public RuntimeEplModuleDefinition { + if (moduleKey == null || moduleKey.isBlank()) { + throw new IllegalArgumentException("moduleKey must not be blank"); + } + moduleKey = moduleKey.trim(); + eventTypes = eventTypes == null ? Map.of() : immutableNestedMap(eventTypes); + eplResources = eplResources == null ? List.of() : List.copyOf(eplResources); + outputStatementNames = outputStatementNames == null ? List.of() : List.copyOf(outputStatementNames); + inputStreams = inputStreams == null ? List.of() : List.copyOf(inputStreams); + } + + private static Map> immutableNestedMap(Map> source) { + LinkedHashMap> copy = new LinkedHashMap<>(); + source.forEach((key, value) -> copy.put(key, value == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(value)))); + return Collections.unmodifiableMap(copy); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModuleExecutionResult.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModuleExecutionResult.java new file mode 100644 index 0000000..ddd1be9 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModuleExecutionResult.java @@ -0,0 +1,26 @@ +package at.procon.eventhub.processing.eventprocessing.module.epl; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public record RuntimeEplModuleExecutionResult( + Map>> outputsByStatement, + Map metadata +) { + public RuntimeEplModuleExecutionResult { + outputsByStatement = outputsByStatement == null ? Map.of() : immutableOutputMap(outputsByStatement); + metadata = metadata == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(metadata)); + } + + public List> output(String statementName) { + return outputsByStatement.getOrDefault(statementName, List.of()); + } + + private static Map>> immutableOutputMap(Map>> source) { + LinkedHashMap>> copy = new LinkedHashMap<>(); + source.forEach((key, value) -> copy.put(key, value == null ? List.of() : List.copyOf(value))); + return Collections.unmodifiableMap(copy); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModuleExecutor.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModuleExecutor.java new file mode 100644 index 0000000..0557f72 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/module/epl/RuntimeEplModuleExecutor.java @@ -0,0 +1,120 @@ +package at.procon.eventhub.processing.eventprocessing.module.epl; + +import com.espertech.esper.common.client.EPCompiled; +import com.espertech.esper.common.client.EventBean; +import com.espertech.esper.common.client.configuration.Configuration; +import com.espertech.esper.compiler.client.CompilerArguments; +import com.espertech.esper.compiler.client.EPCompileException; +import com.espertech.esper.compiler.client.EPCompilerProvider; +import com.espertech.esper.runtime.client.EPDeployException; +import com.espertech.esper.runtime.client.EPDeployment; +import com.espertech.esper.runtime.client.EPRuntime; +import com.espertech.esper.runtime.client.EPRuntimeProvider; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; +import org.springframework.core.io.ClassPathResource; +import org.springframework.stereotype.Service; +import org.springframework.util.StreamUtils; + +@Service +public class RuntimeEplModuleExecutor { + + private static final AtomicLong RUNTIME_COUNTER = new AtomicLong(); + + public RuntimeEplModuleExecutionResult execute(RuntimeEplModuleDefinition definition) { + EPRuntime runtime = null; + try { + Configuration configuration = new Configuration(); + definition.eventTypes().forEach((eventTypeName, eventTypeDefinition) -> + configuration.getCommon().addEventType(eventTypeName, eventTypeDefinition)); + + String runtimeUri = "eventhub-runtime-epl-module-" + + definition.moduleKey().replaceAll("[^A-Za-z0-9_-]", "-") + + "-" + RUNTIME_COUNTER.incrementAndGet(); + runtime = EPRuntimeProvider.getRuntime(runtimeUri, configuration); + + EPCompiled compiled = EPCompilerProvider.getCompiler().compile(renderEpl(definition.eplResources()), new CompilerArguments(configuration)); + EPDeployment deployment = runtime.getDeploymentService().deploy(compiled); + + Map>> outputs = new LinkedHashMap<>(); + for (String statementName : definition.outputStatementNames()) { + outputs.put(statementName, new ArrayList<>()); + var statement = runtime.getDeploymentService().getStatement(deployment.getDeploymentId(), statementName); + if (statement == null) { + throw new IllegalStateException("EPL module " + definition.moduleKey() + + " did not deploy expected statement '" + statementName + "'."); + } + statement.addListener((newData, oldData, stmt, rt) -> collect(newData, outputs.get(statementName))); + } + + for (RuntimeEplInputEventStream inputStream : definition.inputStreams()) { + for (Map event : inputStream.events()) { + runtime.getEventService().sendEventMap(event, inputStream.eventTypeName()); + } + } + + Map metadata = new LinkedHashMap<>(); + metadata.put("engine", "EPL"); + metadata.put("eplResources", definition.eplResources()); + Map inputCounts = new LinkedHashMap<>(); + for (RuntimeEplInputEventStream inputStream : definition.inputStreams()) { + inputCounts.merge(inputStream.eventTypeName(), inputStream.events().size(), Integer::sum); + } + metadata.put("inputCounts", inputCounts); + Map outputCounts = new LinkedHashMap<>(); + outputs.forEach((statement, events) -> outputCounts.put(statement, events.size())); + metadata.put("outputCounts", outputCounts); + return new RuntimeEplModuleExecutionResult(outputs, metadata); + } catch (EPCompileException | EPDeployException e) { + throw new IllegalStateException("Cannot compile/deploy runtime EPL module " + definition.moduleKey(), e); + } finally { + if (runtime != null) { + runtime.destroy(); + } + } + } + + private String renderEpl(List resources) { + StringBuilder builder = new StringBuilder(); + for (String resource : resources) { + if (resource == null || resource.isBlank()) { + continue; + } + if (!builder.isEmpty()) { + builder.append("\n\n"); + } + builder.append(loadResource(resource.trim())); + } + return builder.toString(); + } + + private static String loadResource(String path) { + try { + ClassPathResource resource = new ClassPathResource(path); + return StreamUtils.copyToString(resource.getInputStream(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException("Cannot load EPL resource: " + path, e); + } + } + + private void collect(EventBean[] newData, List> target) { + if (newData == null || target == null) { + return; + } + for (EventBean eventBean : newData) { + LinkedHashMap values = new LinkedHashMap<>(); + for (String propertyName : eventBean.getEventType().getPropertyNames()) { + if (propertyName == null) { + continue; + } + values.put(propertyName, eventBean.get(propertyName)); + } + target.add(values); + } + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/DriverWorkingTimeRuntimeProcessingPlan.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/DriverWorkingTimeRuntimeProcessingPlan.java new file mode 100644 index 0000000..355f7a8 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/DriverWorkingTimeRuntimeProcessingPlan.java @@ -0,0 +1,480 @@ +package at.procon.eventhub.processing.eventprocessing.plan; + +import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto; +import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto; +import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest; +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest; +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto; +import at.procon.eventhub.processing.eventprocessing.module.DriverWorkingTimeModuleKeys; +import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleContext; +import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleResult; +import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleStatus; +import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingPipelineExecutor; +import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleRegistry; +import at.procon.eventhub.processing.eventprocessing.module.DriverActivityIntervalsModule; +import at.procon.eventhub.processing.eventprocessing.module.DriverVehicleUsageIntervalsModule; +import at.procon.eventhub.processing.eventprocessing.module.DriverVehicleUsageMergeModule; +import at.procon.eventhub.processing.eventprocessing.module.VehicleEvidenceAttachmentModule; +import at.procon.eventhub.processing.eventprocessing.module.SupportEvidenceNormalizationModule; +import at.procon.eventhub.processing.eventprocessing.module.DriverWorkingTimeDerivedProjectionsModule; +import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService; +import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Component +public class DriverWorkingTimeRuntimeProcessingPlan implements RuntimeProcessingPlan { + + public static final String PLAN_KEY = "driver-working-time-v1"; + public static final String LEGACY_PROFILE_ALIAS = "tachograph-driver-esper-v1"; + + private final RuntimeProcessingPipelineExecutor pipelineExecutor; + private final boolean includeRuntimeEventAssemblyModule; + + @Autowired + public DriverWorkingTimeRuntimeProcessingPlan(RuntimeProcessingPipelineExecutor pipelineExecutor) { + this.pipelineExecutor = pipelineExecutor; + this.includeRuntimeEventAssemblyModule = true; + } + + /** + * Compatibility constructor for older unit tests and legacy adapters that instantiated the + * driver-working-time plan directly from the old scope service. This constructor builds a + * small local module registry around the existing scope-processing adapter. It intentionally + * omits the runtime-event-assembly module because the scope-processing adapter performs that + * assembly internally. + */ + public DriverWorkingTimeRuntimeProcessingPlan( + RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService + ) { + this(new RuntimeProcessingPipelineExecutor(new RuntimeProcessingModuleRegistry(List.of( + new DriverActivityIntervalsModule(), + new DriverVehicleUsageIntervalsModule(), + new DriverVehicleUsageMergeModule(), + new VehicleEvidenceAttachmentModule(), + new SupportEvidenceNormalizationModule(), + new DriverWorkingTimeDerivedProjectionsModule(scopeProcessingService) + ))), false); + } + + private DriverWorkingTimeRuntimeProcessingPlan( + RuntimeProcessingPipelineExecutor pipelineExecutor, + boolean includeRuntimeEventAssemblyModule + ) { + this.pipelineExecutor = pipelineExecutor; + this.includeRuntimeEventAssemblyModule = includeRuntimeEventAssemblyModule; + } + + @Override + public String processingPlanKey() { + return PLAN_KEY; + } + + @Override + public Set aliases() { + return Set.of(LEGACY_PROFILE_ALIAS); + } + + @Override + public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() { + return RuntimeEventPartitioningStrategy.DRIVER; + } + + @Override + public String displayName() { + return "Driver working-time processing"; + } + + @Override + public String description() { + return "Loads canonical EventHub runtime events from selected sources, partitions them by driver, " + + "attaches vehicle-only evidence by vehicle/time, normalizes mixed-source support evidence, " + + "then executes source-neutral driver working-time processing modules for driving intervals, " + + "interruptions, rest candidates, overnight candidates, trips, vehicle usage, and card-absence coverage."; + } + + @Override + public List supportedPartitioningStrategies() { + return List.of(RuntimeEventPartitioningStrategy.DRIVER); + } + + @Override + public List modules() { + List descriptors = new java.util.ArrayList<>(); + if (includeRuntimeEventAssemblyModule) { + descriptors.add(new RuntimeProcessingModuleDescriptorDto( + DriverWorkingTimeModuleKeys.RUNTIME_EVENT_ASSEMBLY, + "Runtime event assembly", + "Loads and merges canonical runtime events from the selected source scope before plan-specific processing.", + "JAVA", + Set.of("UnifiedRuntimeEventBundle") + )); + } + descriptors.addAll(List.of( + new RuntimeProcessingModuleDescriptorDto( + DriverWorkingTimeModuleKeys.EVENT_TO_ACTIVITY_INTERVALS, + "Event to activity intervals", + "Pairs canonical DRIVER_ACTIVITY START/END point events into source-neutral activity intervals using a first-class EPL module.", + "ESPER", + Set.of("DriverActivityIntervalEvent") + ), + new RuntimeProcessingModuleDescriptorDto( + DriverWorkingTimeModuleKeys.EVENT_TO_VEHICLE_USAGE_INTERVALS, + "Event to vehicle usage intervals", + "Pairs canonical driver/card INSERT/WITHDRAW point events into source-neutral vehicle-usage intervals using a first-class EPL module.", + "ESPER", + Set.of("DriverVehicleUsageIntervalEvent") + ), + new RuntimeProcessingModuleDescriptorDto( + DriverWorkingTimeModuleKeys.VEHICLE_USAGE_MERGE, + "Vehicle usage merge", + "Merges adjacent same-driver/same-vehicle usage intervals, including 23:59:59 to 00:00:00 continuations. Currently delegated to the derived projections adapter.", + "JAVA/ESPER", + Set.of("DriverVehicleUsageIntervalEvent") + ), + new RuntimeProcessingModuleDescriptorDto( + DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT, + "Vehicle evidence attachment", + "Attaches vehicle-only events to driver partitions when they overlap driver vehicle-usage intervals.", + "JAVA", + Set.of("RuntimeDriverPartitionDebugDto") + ), + new RuntimeProcessingModuleDescriptorDto( + DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION, + "Support evidence normalization", + "Normalizes attached mixed-source support evidence for driver working-time processing.", + "JAVA", + Set.of("RuntimeSupportEvidenceNormalizationDebugDto") + ), + new RuntimeProcessingModuleDescriptorDto( + DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS, + "Driving-derived projections", + "Runs the shared driver working-time derived projection module for driving interruptions, rest candidates, trips, and overnight candidates.", + "ESPER+JAVA", + Set.of("DriverWorkingTimeProcessingResultDto") + ) + )); + return List.copyOf(descriptors); + } + + @Override + public Set optionalParameters() { + return Set.of( + "significantDrivingMinutes", + "minimumRestPeriodMinutes", + "attachVehicleOnlyEvents", + "vehicleEvidencePaddingMinutes", + "includePartitionDebug" + ); + } + + @Override + public RuntimeProcessingExecutionResultDto execute(RuntimeProcessingExecutionApiRequest request) { + boolean includePartitionDebug = booleanParameter( + request.parameters(), + "includePartitionDebug", + request.partitioning() != null && request.partitioning().includeDebugOrDefault() + ); + UnifiedRuntimeProcessingApiRequest scopeRequest = applyExecutionRequest( + request.sourceSelection(), + request.partitioning(), + request.parameters() + ); + + Map attributes = new LinkedHashMap<>(); + attributes.put("runtimeScopeApiRequest", scopeRequest); + attributes.put("includePartitionDebug", includePartitionDebug); + RuntimeProcessingModuleContext initialContext = new RuntimeProcessingModuleContext( + request, + List.of(), + attributes, + Map.of() + ); + + List executedModules = requestedOrDefaultModules(request.modules()); + Map moduleResults = pipelineExecutor.execute( + request, + executedModules, + initialContext + ); + UnifiedRuntimeDriverWorkingTimeScopeResultDto workingTimeResult = extractWorkingTimeResult(moduleResults); + + Map partitionResults = new LinkedHashMap<>(); + workingTimeResult.driverResults().forEach((driverKey, driverResult) -> { + Map metadata = new LinkedHashMap<>(); + metadata.put("projectionResultType", driverResult.projection() == null ? "NONE" : "DriverWorkingTimeProcessingResultDto"); + metadata.put("driverSeedEventCount", driverResult.driverSeedEventCount()); + metadata.put("expandedVehicleEventCount", driverResult.expandedVehicleEventCount()); + metadata.put("mergedEventCount", driverResult.mergedEventCount()); + if (driverResult.supportEvidenceNormalization() != null) { + metadata.put("supportEvidenceNormalization", driverResult.supportEvidenceNormalization()); + } + if (driverResult.partitionDebug() != null) { + metadata.put("partitionDebug", driverResult.partitionDebug()); + } + partitionResults.put( + driverKey, + new RuntimeEventProcessingPartitionResultDto( + "DRIVER", + driverKey, + "UnifiedRuntimeDerivedProjectionResultDto", + driverResult, + metadata, + partitionModuleResults(driverResult) + ) + ); + }); + + return new RuntimeProcessingExecutionResultDto( + processingPlanKey(), + executedModules, + RuntimeEventPartitioningStrategy.DRIVER, + workingTimeResult.request(), + workingTimeResult.inputEventCount(), + workingTimeResult.selectedDriverCount(), + workingTimeResult.discoveredVehicleCount(), + workingTimeResult.discoveredVehicles(), + sanitizeExecutionModuleResults(moduleResults), + partitionResults, + workingTimeResult.notes(), + workingTimeResult.warnings() + ); + } + + private UnifiedRuntimeDriverWorkingTimeScopeResultDto extractWorkingTimeResult( + Map moduleResults + ) { + RuntimeProcessingModuleResult finalResult = moduleResults.get(DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS); + if (finalResult == null) { + throw new IllegalArgumentException("Processing plan " + PLAN_KEY + + " requires module " + DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS + "."); + } + if (finalResult.status() == RuntimeProcessingModuleStatus.FAILED) { + throw new IllegalStateException("Processing module " + finalResult.moduleKey() + " failed: " + finalResult.warnings()); + } + Object output = finalResult.output(); + if (output instanceof UnifiedRuntimeDriverWorkingTimeScopeResultDto result) { + return result; + } + throw new IllegalStateException("Processing module " + DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS + + " did not return UnifiedRuntimeDriverWorkingTimeScopeResultDto. Actual: " + + (output == null ? "null" : output.getClass().getName())); + } + + private Map sanitizeExecutionModuleResults( + Map moduleResults + ) { + LinkedHashMap sanitized = new LinkedHashMap<>(); + for (Map.Entry entry : moduleResults.entrySet()) { + RuntimeProcessingModuleResult result = entry.getValue(); + sanitized.put(entry.getKey(), new RuntimeProcessingModuleResult( + result.moduleKey(), + result.status(), + null, + result.metadata(), + result.warnings() + )); + } + return sanitized; + } + + private Map partitionModuleResults( + UnifiedRuntimeDerivedProjectionResultDto driverResult + ) { + LinkedHashMap results = new LinkedHashMap<>(); + Map attachmentMetadata = new LinkedHashMap<>(); + attachmentMetadata.put("driverSeedEventCount", driverResult.driverSeedEventCount()); + attachmentMetadata.put("expandedVehicleEventCount", driverResult.expandedVehicleEventCount()); + attachmentMetadata.put("mergedEventCount", driverResult.mergedEventCount()); + results.put( + DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT, + new RuntimeProcessingModuleResult( + DriverWorkingTimeModuleKeys.VEHICLE_EVIDENCE_ATTACHMENT, + RuntimeProcessingModuleStatus.SUCCESS, + driverResult.partitionDebug(), + attachmentMetadata, + List.of() + ) + ); + Map normalizationMetadata = new LinkedHashMap<>(); + if (driverResult.supportEvidenceNormalization() != null) { + normalizationMetadata.put("inputEventCount", driverResult.supportEvidenceNormalization().inputEventCount()); + normalizationMetadata.put("normalizedSupportEvidenceEventCount", driverResult.supportEvidenceNormalization().normalizedSupportEvidenceEventCount()); + normalizationMetadata.put("unchangedEventCount", driverResult.supportEvidenceNormalization().unchangedEventCount()); + } + results.put( + DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION, + new RuntimeProcessingModuleResult( + DriverWorkingTimeModuleKeys.SUPPORT_EVIDENCE_NORMALIZATION, + RuntimeProcessingModuleStatus.SUCCESS, + driverResult.supportEvidenceNormalization(), + normalizationMetadata, + List.of() + ) + ); + results.put( + DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS, + new RuntimeProcessingModuleResult( + DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS, + RuntimeProcessingModuleStatus.SUCCESS, + driverResult.projection(), + Map.of( + "resultType", driverResult.projection() == null ? "NONE" : "DriverWorkingTimeProcessingResultDto", + "noteCount", driverResult.notes().size() + ), + List.of() + ) + ); + return results; + } + + public UnifiedRuntimeProcessingApiRequest applyExecutionRequest( + UnifiedRuntimeProcessingApiRequest sourceSelection, + RuntimeEventPartitioningApiRequest partitioning, + Map parameters + ) { + if (sourceSelection == null) { + throw new IllegalArgumentException("sourceSelection must not be null for processing plan " + PLAN_KEY + "."); + } + boolean includeAllDrivers = sourceSelection.includeAllDrivers() != null && sourceSelection.includeAllDrivers(); + java.util.Set driverKeys = sourceSelection.driverKeys(); + boolean includeAllVehicles = sourceSelection.includeAllVehicles() != null && sourceSelection.includeAllVehicles(); + java.util.Set vehicleKeys = sourceSelection.vehicleKeys(); + + if (partitioning != null) { + RuntimeEventPartitioningStrategy strategy = partitioning.strategy() == null + ? RuntimeEventPartitioningStrategy.DRIVER + : partitioning.strategy(); + if (strategy != RuntimeEventPartitioningStrategy.DRIVER + && strategy != RuntimeEventPartitioningStrategy.CUSTOM_PROFILE) { + throw new IllegalArgumentException("Processing plan " + PLAN_KEY + + " currently supports DRIVER partitioning only. Requested: " + strategy); + } + includeAllDrivers = includeAllDrivers || partitioning.allPartitions() || partitioning.allDrivers(); + if (!partitioning.driverKeys().isEmpty()) { + driverKeys = partitioning.driverKeys(); + } else if (!partitioning.partitionKeys().isEmpty()) { + driverKeys = partitioning.partitionKeys(); + } + includeAllVehicles = includeAllVehicles || partitioning.allVehicles(); + if (!partitioning.vehicleKeys().isEmpty()) { + vehicleKeys = partitioning.vehicleKeys(); + } + } + + Integer significantDrivingMinutes = integerParameter(parameters, "significantDrivingMinutes", sourceSelection.significantDrivingMinutes()); + Integer minimumRestPeriodMinutes = integerParameter(parameters, "minimumRestPeriodMinutes", sourceSelection.minimumRestPeriodMinutes()); + boolean attachVehicleOnlyEvents = booleanParameter(parameters, "attachVehicleOnlyEvents", + partitioning == null ? sourceSelection.expandVehicleEvents() == null || sourceSelection.expandVehicleEvents() : partitioning.attachVehicleEvidenceOrDefault()); + Integer vehicleEvidencePaddingMinutes = nonNegativeIntegerParameter(parameters, "vehicleEvidencePaddingMinutes", + partitioning == null + ? sourceSelection.vehicleExpansionPaddingMinutes() + : partitioning.vehicleEvidencePaddingMinutesOrDefault(sourceSelection.vehicleExpansionPaddingMinutes() == null ? 0 : sourceSelection.vehicleExpansionPaddingMinutes())); + + return new UnifiedRuntimeProcessingApiRequest( + sourceSelection.sessionId(), + sourceSelection.sessionIds(), + sourceSelection.compositeSessionId(), + sourceSelection.tenantKey(), + sourceSelection.sourceFamilies(), + sourceSelection.eventBackend(), + sourceSelection.driverKey(), + driverKeys, + includeAllDrivers, + vehicleKeys, + includeAllVehicles, + sourceSelection.driverSourceEntityId(), + sourceSelection.driverCardNation(), + sourceSelection.driverCardNumber(), + sourceSelection.occurredFrom(), + sourceSelection.occurredTo(), + attachVehicleOnlyEvents, + vehicleEvidencePaddingMinutes, + significantDrivingMinutes, + minimumRestPeriodMinutes + ); + } + + private List requestedOrDefaultModules(List requestedModules) { + if (requestedModules != null && !requestedModules.isEmpty()) { + LinkedHashMap requested = new LinkedHashMap<>(); + for (String module : requestedModules) { + if (module != null && !module.isBlank()) { + requested.put(module.trim(), module.trim()); + } + } + requested.putIfAbsent(DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS, + DriverWorkingTimeModuleKeys.DRIVING_DERIVED_PROJECTIONS); + return List.copyOf(requested.values()); + } + return modules().stream() + .map(RuntimeProcessingModuleDescriptorDto::moduleKey) + .toList(); + } + + private boolean booleanParameter(Map parameters, String key, boolean fallback) { + if (parameters == null || !parameters.containsKey(key)) { + return fallback; + } + Object value = parameters.get(key); + if (value == null) { + return fallback; + } + if (value instanceof Boolean booleanValue) { + return booleanValue; + } + String text = value.toString(); + if (text == null || text.isBlank()) { + return fallback; + } + return Boolean.parseBoolean(text.trim()); + } + + private Integer nonNegativeIntegerParameter(Map parameters, String key, Integer fallback) { + if (parameters == null || !parameters.containsKey(key)) { + return fallback; + } + Object value = parameters.get(key); + if (value == null) { + return fallback; + } + if (value instanceof Number number) { + return Math.max(0, number.intValue()); + } + String text = value.toString(); + if (text == null || text.isBlank()) { + return fallback; + } + try { + return Math.max(0, Integer.parseInt(text.trim())); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Parameter '" + key + "' must be an integer.", ex); + } + } + + private Integer integerParameter(Map parameters, String key, Integer fallback) { + if (parameters == null || !parameters.containsKey(key)) { + return fallback; + } + Object value = parameters.get(key); + if (value == null) { + return fallback; + } + if (value instanceof Number number) { + return Math.max(1, number.intValue()); + } + String text = value.toString(); + if (text == null || text.isBlank()) { + return fallback; + } + try { + return Math.max(1, Integer.parseInt(text.trim())); + } catch (NumberFormatException ex) { + throw new IllegalArgumentException("Parameter '" + key + "' must be an integer.", ex); + } + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingExecutionApiRequest.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingExecutionApiRequest.java new file mode 100644 index 0000000..71ce22c --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingExecutionApiRequest.java @@ -0,0 +1,59 @@ +package at.procon.eventhub.processing.eventprocessing.plan; + +import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest; +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest; +import com.fasterxml.jackson.annotation.JsonAlias; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public record RuntimeProcessingExecutionApiRequest( + String processingPlanKey, + @JsonAlias("scope") UnifiedRuntimeProcessingApiRequest sourceSelection, + RuntimeEventPartitioningApiRequest partitioning, + List modules, + Map parameters +) { + public RuntimeProcessingExecutionApiRequest { + if (processingPlanKey == null || processingPlanKey.isBlank()) { + throw new IllegalArgumentException("processingPlanKey must not be blank"); + } + processingPlanKey = processingPlanKey.trim(); + if (sourceSelection == null) { + throw new IllegalArgumentException("sourceSelection must not be null"); + } + partitioning = partitioning == null + ? new RuntimeEventPartitioningApiRequest(null, null, null, null, null, null, null, null, null, null) + : partitioning; + modules = modules == null ? List.of() : List.copyOf(modules); + parameters = parameters == null + ? Map.of() + : Collections.unmodifiableMap(new LinkedHashMap<>(parameters)); + } + + public UnifiedRuntimeProcessingApiRequest scope() { + return sourceSelection; + } + + public static RuntimeProcessingExecutionApiRequest driverWorkingTime(UnifiedRuntimeProcessingApiRequest sourceSelection) { + return new RuntimeProcessingExecutionApiRequest( + DriverWorkingTimeRuntimeProcessingPlan.PLAN_KEY, + sourceSelection, + new RuntimeEventPartitioningApiRequest( + at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy.DRIVER, + null, + sourceSelection != null ? sourceSelection.includeAllDrivers() : null, + sourceSelection != null ? sourceSelection.driverKeys() : null, + sourceSelection != null ? sourceSelection.includeAllDrivers() : null, + sourceSelection != null ? sourceSelection.vehicleKeys() : null, + sourceSelection != null ? sourceSelection.includeAllVehicles() : null, + null, + sourceSelection != null ? sourceSelection.vehicleExpansionPaddingMinutes() : null, + null + ), + List.of(), + Map.of() + ); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingExecutionResultDto.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingExecutionResultDto.java new file mode 100644 index 0000000..cef188f --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingExecutionResultDto.java @@ -0,0 +1,53 @@ +package at.procon.eventhub.processing.eventprocessing.plan; + +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto; +import at.procon.eventhub.processing.eventprocessing.module.RuntimeProcessingModuleResult; +import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy; +import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef; +import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public record RuntimeProcessingExecutionResultDto( + String processingPlanKey, + List executedModules, + RuntimeEventPartitioningStrategy partitioningStrategy, + UnifiedRuntimeProcessingRequest request, + int inputEventCount, + int selectedPartitionCount, + int discoveredVehicleCount, + List discoveredVehicles, + Map moduleResults, + Map partitionResults, + List notes, + List warnings +) { + public RuntimeProcessingExecutionResultDto { + executedModules = executedModules == null ? List.of() : List.copyOf(executedModules); + discoveredVehicles = discoveredVehicles == null ? List.of() : List.copyOf(discoveredVehicles); + moduleResults = moduleResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(moduleResults)); + partitionResults = partitionResults == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(partitionResults)); + notes = notes == null ? List.of() : List.copyOf(notes); + warnings = warnings == null ? List.of() : List.copyOf(warnings); + } + + public RuntimeProcessingExecutionResultDto( + String processingPlanKey, + List executedModules, + RuntimeEventPartitioningStrategy partitioningStrategy, + UnifiedRuntimeProcessingRequest request, + int inputEventCount, + int selectedPartitionCount, + int discoveredVehicleCount, + List discoveredVehicles, + Map partitionResults, + List notes, + List warnings + ) { + this(processingPlanKey, executedModules, partitioningStrategy, request, inputEventCount, + selectedPartitionCount, discoveredVehicleCount, discoveredVehicles, Map.of(), partitionResults, + notes, warnings); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingExecutionService.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingExecutionService.java new file mode 100644 index 0000000..4dfce32 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingExecutionService.java @@ -0,0 +1,24 @@ +package at.procon.eventhub.processing.eventprocessing.plan; + +import java.util.List; +import org.springframework.stereotype.Service; + +@Service +public class RuntimeProcessingExecutionService { + + private final RuntimeProcessingPlanRegistry planRegistry; + + public RuntimeProcessingExecutionService(RuntimeProcessingPlanRegistry planRegistry) { + this.planRegistry = planRegistry; + } + + public RuntimeProcessingExecutionResultDto execute(RuntimeProcessingExecutionApiRequest request) { + RuntimeProcessingPlan plan = planRegistry.require(request.processingPlanKey()); + plan.validatePartitioning(request.partitioning()); + return plan.execute(request); + } + + public List listPlans() { + return planRegistry.planDescriptors(); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingModuleDescriptorDto.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingModuleDescriptorDto.java new file mode 100644 index 0000000..e7b44cb --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingModuleDescriptorDto.java @@ -0,0 +1,22 @@ +package at.procon.eventhub.processing.eventprocessing.plan; + +import java.util.Set; + +public record RuntimeProcessingModuleDescriptorDto( + String moduleKey, + String displayName, + String description, + String engine, + Set producedStreams +) { + public RuntimeProcessingModuleDescriptorDto { + if (moduleKey == null || moduleKey.isBlank()) { + throw new IllegalArgumentException("moduleKey must not be blank"); + } + moduleKey = moduleKey.trim(); + displayName = displayName == null || displayName.isBlank() ? moduleKey : displayName.trim(); + description = description == null ? "" : description; + engine = engine == null || engine.isBlank() ? "UNKNOWN" : engine.trim(); + producedStreams = producedStreams == null ? Set.of() : Set.copyOf(producedStreams); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlan.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlan.java new file mode 100644 index 0000000..9ea6e19 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlan.java @@ -0,0 +1,55 @@ +package at.procon.eventhub.processing.eventprocessing.plan; + +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest; +import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy; +import java.util.List; +import java.util.Set; + +public interface RuntimeProcessingPlan { + + String processingPlanKey(); + + default Set aliases() { + return Set.of(); + } + + RuntimeEventPartitioningStrategy defaultPartitioningStrategy(); + + default String displayName() { + return processingPlanKey(); + } + + default String description() { + return ""; + } + + default List supportedPartitioningStrategies() { + RuntimeEventPartitioningStrategy defaultStrategy = defaultPartitioningStrategy(); + return defaultStrategy == null ? List.of() : List.of(defaultStrategy); + } + + default List modules() { + return List.of(); + } + + default Set requiredParameters() { + return Set.of(); + } + + default Set optionalParameters() { + return Set.of(); + } + + RuntimeProcessingExecutionResultDto execute(RuntimeProcessingExecutionApiRequest request); + + default void validatePartitioning(RuntimeEventPartitioningApiRequest partitioning) { + RuntimeEventPartitioningStrategy requested = partitioning == null || partitioning.strategy() == null + ? defaultPartitioningStrategy() + : partitioning.strategy(); + if (requested != null && !supportedPartitioningStrategies().contains(requested)) { + throw new IllegalArgumentException("Processing plan " + processingPlanKey() + + " does not support partitioning strategy " + requested + + ". Supported: " + supportedPartitioningStrategies()); + } + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlanDescriptorDto.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlanDescriptorDto.java new file mode 100644 index 0000000..97ee73d --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlanDescriptorDto.java @@ -0,0 +1,44 @@ +package at.procon.eventhub.processing.eventprocessing.plan; + +import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy; +import java.util.List; +import java.util.Set; + +public record RuntimeProcessingPlanDescriptorDto( + String processingPlanKey, + Set aliases, + String displayName, + String description, + RuntimeEventPartitioningStrategy defaultPartitioningStrategy, + List supportedPartitioningStrategies, + List modules, + Set requiredParameters, + Set optionalParameters +) { + public RuntimeProcessingPlanDescriptorDto { + supportedPartitioningStrategies = supportedPartitioningStrategies == null + ? List.of() + : List.copyOf(supportedPartitioningStrategies); + modules = modules == null ? List.of() : List.copyOf(modules); + aliases = aliases == null ? Set.of() : Set.copyOf(aliases); + requiredParameters = requiredParameters == null ? Set.of() : Set.copyOf(requiredParameters); + optionalParameters = optionalParameters == null ? Set.of() : Set.copyOf(optionalParameters); + } + + public static RuntimeProcessingPlanDescriptorDto from(RuntimeProcessingPlan plan) { + if (plan == null) { + throw new IllegalArgumentException("plan must not be null"); + } + return new RuntimeProcessingPlanDescriptorDto( + plan.processingPlanKey(), + plan.aliases(), + plan.displayName(), + plan.description(), + plan.defaultPartitioningStrategy(), + plan.supportedPartitioningStrategies(), + plan.modules(), + plan.requiredParameters(), + plan.optionalParameters() + ); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlanRegistry.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlanRegistry.java new file mode 100644 index 0000000..d922291 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlanRegistry.java @@ -0,0 +1,72 @@ +package at.procon.eventhub.processing.eventprocessing.plan; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import org.springframework.stereotype.Component; + +@Component +public class RuntimeProcessingPlanRegistry { + + private final Map plansByKey; + private final Map plansByKeyAndAlias; + + public RuntimeProcessingPlanRegistry(List plans) { + LinkedHashMap byKey = new LinkedHashMap<>(); + LinkedHashMap byKeyAndAlias = new LinkedHashMap<>(); + for (RuntimeProcessingPlan plan : plans == null ? List.of() : plans) { + String key = normalize(plan.processingPlanKey()); + if (key == null) { + throw new IllegalStateException("Runtime processing plan returned a blank processingPlanKey: " + plan.getClass().getName()); + } + RuntimeProcessingPlan previous = byKey.putIfAbsent(key, plan); + if (previous != null) { + throw new IllegalStateException("Duplicate runtime processingPlanKey: " + key); + } + putAlias(byKeyAndAlias, key, plan); + for (String alias : plan.aliases()) { + String normalizedAlias = normalize(alias); + if (normalizedAlias != null) { + putAlias(byKeyAndAlias, normalizedAlias, plan); + } + } + } + this.plansByKey = Collections.unmodifiableMap(byKey); + this.plansByKeyAndAlias = Collections.unmodifiableMap(byKeyAndAlias); + } + + public RuntimeProcessingPlan require(String processingPlanKey) { + String normalized = normalize(processingPlanKey); + RuntimeProcessingPlan plan = normalized == null ? null : plansByKeyAndAlias.get(normalized); + if (plan == null) { + throw new IllegalArgumentException("Unknown runtime processingPlanKey: " + processingPlanKey + + ". Available plans: " + plansByKey.keySet()); + } + return plan; + } + + public Map plansByKey() { + return plansByKey; + } + + public List planDescriptors() { + return plansByKey.values().stream() + .map(RuntimeProcessingPlanDescriptorDto::from) + .toList(); + } + + private void putAlias(Map target, String key, RuntimeProcessingPlan plan) { + RuntimeProcessingPlan previous = target.putIfAbsent(key, plan); + if (previous != null && previous != plan) { + throw new IllegalStateException("Duplicate runtime processing plan alias/key: " + key); + } + } + + private String normalize(String key) { + if (key == null || key.isBlank()) { + return null; + } + return key.trim(); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/TachographDriverWorkingTimeRuntimeProcessingPlan.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/TachographDriverWorkingTimeRuntimeProcessingPlan.java new file mode 100644 index 0000000..3f96e95 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/plan/TachographDriverWorkingTimeRuntimeProcessingPlan.java @@ -0,0 +1,16 @@ +package at.procon.eventhub.processing.eventprocessing.plan; + +import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService; + +/** + * @deprecated Compatibility adapter. Use {@link DriverWorkingTimeRuntimeProcessingPlan}. + */ +@Deprecated(forRemoval = false) +public class TachographDriverWorkingTimeRuntimeProcessingPlan extends DriverWorkingTimeRuntimeProcessingPlan { + + public TachographDriverWorkingTimeRuntimeProcessingPlan( + RuntimeDriverWorkingTimeScopeProcessingService driverWorkingTimeScopeProcessingService + ) { + super(driverWorkingTimeScopeProcessingService); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/profile/TachographDriverEsperRuntimeEventProcessingProfile.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/profile/TachographDriverEsperRuntimeEventProcessingProfile.java index 0cd5c23..0d923d9 100644 --- a/src/main/java/at/procon/eventhub/processing/eventprocessing/profile/TachographDriverEsperRuntimeEventProcessingProfile.java +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/profile/TachographDriverEsperRuntimeEventProcessingProfile.java @@ -1,30 +1,35 @@ package at.procon.eventhub.processing.eventprocessing.profile; -import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest; -import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto; -import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest; -import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto; import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy; -import at.procon.eventhub.processing.service.UnifiedRuntimeTachographEsperScopeProcessingService; -import java.util.LinkedHashMap; +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionApiRequest; +import at.procon.eventhub.processing.eventprocessing.plan.RuntimeProcessingExecutionResultDto; +import at.procon.eventhub.processing.eventprocessing.plan.DriverWorkingTimeRuntimeProcessingPlan; +import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService; import java.util.List; -import java.util.Map; import java.util.Set; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @Component public class TachographDriverEsperRuntimeEventProcessingProfile implements RuntimeEventProcessingProfile { - public static final String PROFILE_KEY = "tachograph-driver-esper-v1"; + /** + * Legacy compatibility key. New clients should use processingPlanKey=driver-working-time-v1 + * via /api/eventhub/runtime-processing/executions. + */ + public static final String PROFILE_KEY = DriverWorkingTimeRuntimeProcessingPlan.LEGACY_PROFILE_ALIAS; - private final UnifiedRuntimeTachographEsperScopeProcessingService tachographScopeProcessingService; + private final DriverWorkingTimeRuntimeProcessingPlan plan; - public TachographDriverEsperRuntimeEventProcessingProfile( - UnifiedRuntimeTachographEsperScopeProcessingService tachographScopeProcessingService - ) { - this.tachographScopeProcessingService = tachographScopeProcessingService; + @Autowired + public TachographDriverEsperRuntimeEventProcessingProfile(DriverWorkingTimeRuntimeProcessingPlan plan) { + this.plan = plan; + } + + public TachographDriverEsperRuntimeEventProcessingProfile(RuntimeDriverWorkingTimeScopeProcessingService scopeProcessingService) { + this(new DriverWorkingTimeRuntimeProcessingPlan(scopeProcessingService)); } @Override @@ -34,7 +39,7 @@ public class TachographDriverEsperRuntimeEventProcessingProfile implements Runti @Override public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() { - return RuntimeEventPartitioningStrategy.DRIVER; + return plan.defaultPartitioningStrategy(); } @Override @@ -44,202 +49,35 @@ public class TachographDriverEsperRuntimeEventProcessingProfile implements Runti @Override public String description() { - return "Runs the shared tachograph driver Esper processing pipeline over Runtime Processing event scopes. " - + "The profile partitions mixed runtime events by driver, attaches vehicle evidence by vehicle/time, " - + "normalizes mixed-source support evidence, and then invokes the event-input EPL pipeline."; + return "Compatibility adapter for legacy profileKey=" + PROFILE_KEY + + ". New clients should use processingPlanKey=" + plan.processingPlanKey() + ". " + + plan.description(); } @Override public List supportedPartitioningStrategies() { - return List.of(RuntimeEventPartitioningStrategy.DRIVER); + return plan.supportedPartitioningStrategies(); } @Override public Set optionalParameters() { - return Set.of( - "significantDrivingMinutes", - "minimumRestPeriodMinutes", - "attachVehicleOnlyEvents", - "vehicleEvidencePaddingMinutes", - "includePartitionDebug" - ); + return plan.optionalParameters(); + } + + @Override + public Set requiredParameters() { + return plan.requiredParameters(); } @Override public RuntimeEventProcessingResultDto process(RuntimeEventProcessingApiRequest request) { - boolean includePartitionDebug = booleanParameter( - request.parameters(), - "includePartitionDebug", - request.partitioning() != null && request.partitioning().includeDebugOrDefault() - ); - UnifiedRuntimeProcessingApiRequest tachographScopeRequest = applyGenericRequest(request.scope(), request.partitioning(), request.parameters()); - UnifiedRuntimeTachographEsperScopeResultDto tachographResult = tachographScopeProcessingService.processScope( - tachographScopeRequest, - includePartitionDebug - ); - - Map partitionResults = new LinkedHashMap<>(); - tachographResult.driverResults().forEach((driverKey, driverResult) -> { - Map metadata = new LinkedHashMap<>(); - metadata.put("projectionResultType", driverResult.projection() == null ? "NONE" : "TachographEsperDriverProcessingResultDto"); - metadata.put("driverSeedEventCount", driverResult.driverSeedEventCount()); - metadata.put("expandedVehicleEventCount", driverResult.expandedVehicleEventCount()); - metadata.put("mergedEventCount", driverResult.mergedEventCount()); - if (driverResult.partitionDebug() != null) { - metadata.put("partitionDebug", driverResult.partitionDebug()); - } - partitionResults.put( - driverKey, - new RuntimeEventProcessingPartitionResultDto( - "DRIVER", - driverKey, - "UnifiedRuntimeDerivedProjectionResultDto", - driverResult, - metadata - ) - ); - }); - - return new RuntimeEventProcessingResultDto( - profileKey(), - RuntimeEventPartitioningStrategy.DRIVER, - tachographResult.request(), - tachographResult.inputEventCount(), - tachographResult.selectedDriverCount(), - tachographResult.discoveredVehicleCount(), - tachographResult.discoveredVehicles(), - partitionResults, - tachographResult.notes(), - tachographResult.warnings() - ); - } - - private UnifiedRuntimeProcessingApiRequest applyGenericRequest( - UnifiedRuntimeProcessingApiRequest scope, - RuntimeEventPartitioningApiRequest partitioning, - Map parameters - ) { - if (scope == null) { - throw new IllegalArgumentException("scope must not be null for profile " + PROFILE_KEY + "."); - } - boolean includeAllDrivers = scope.includeAllDrivers() != null && scope.includeAllDrivers(); - java.util.Set driverKeys = scope.driverKeys(); - boolean includeAllVehicles = scope.includeAllVehicles() != null && scope.includeAllVehicles(); - java.util.Set vehicleKeys = scope.vehicleKeys(); - - if (partitioning != null) { - RuntimeEventPartitioningStrategy strategy = partitioning.strategy() == null - ? RuntimeEventPartitioningStrategy.DRIVER - : partitioning.strategy(); - if (strategy != RuntimeEventPartitioningStrategy.DRIVER - && strategy != RuntimeEventPartitioningStrategy.CUSTOM_PROFILE) { - throw new IllegalArgumentException("Profile " + PROFILE_KEY - + " currently supports DRIVER partitioning only. Requested: " + strategy); - } - includeAllDrivers = includeAllDrivers || partitioning.allPartitions() || partitioning.allDrivers(); - if (!partitioning.driverKeys().isEmpty()) { - driverKeys = partitioning.driverKeys(); - } else if (!partitioning.partitionKeys().isEmpty()) { - driverKeys = partitioning.partitionKeys(); - } - includeAllVehicles = includeAllVehicles || partitioning.allVehicles(); - if (!partitioning.vehicleKeys().isEmpty()) { - vehicleKeys = partitioning.vehicleKeys(); - } - } - - Integer significantDrivingMinutes = integerParameter(parameters, "significantDrivingMinutes", scope.significantDrivingMinutes()); - Integer minimumRestPeriodMinutes = integerParameter(parameters, "minimumRestPeriodMinutes", scope.minimumRestPeriodMinutes()); - boolean attachVehicleOnlyEvents = booleanParameter(parameters, "attachVehicleOnlyEvents", - partitioning == null ? scope.expandVehicleEvents() == null || scope.expandVehicleEvents() : partitioning.attachVehicleEvidenceOrDefault()); - Integer vehicleEvidencePaddingMinutes = nonNegativeIntegerParameter(parameters, "vehicleEvidencePaddingMinutes", - partitioning == null - ? scope.vehicleExpansionPaddingMinutes() - : partitioning.vehicleEvidencePaddingMinutesOrDefault(scope.vehicleExpansionPaddingMinutes() == null ? 0 : scope.vehicleExpansionPaddingMinutes())); - - return new UnifiedRuntimeProcessingApiRequest( - scope.sessionId(), - scope.sessionIds(), - scope.compositeSessionId(), - scope.tenantKey(), - scope.sourceFamilies(), - scope.eventBackend(), - scope.driverKey(), - driverKeys, - includeAllDrivers, - vehicleKeys, - includeAllVehicles, - scope.driverSourceEntityId(), - scope.driverCardNation(), - scope.driverCardNumber(), - scope.occurredFrom(), - scope.occurredTo(), - attachVehicleOnlyEvents, - vehicleEvidencePaddingMinutes, - significantDrivingMinutes, - minimumRestPeriodMinutes - ); - } - - private boolean booleanParameter(Map parameters, String key, boolean fallback) { - if (parameters == null || !parameters.containsKey(key)) { - return fallback; - } - Object value = parameters.get(key); - if (value == null) { - return fallback; - } - if (value instanceof Boolean booleanValue) { - return booleanValue; - } - String text = value.toString(); - if (text == null || text.isBlank()) { - return fallback; - } - return Boolean.parseBoolean(text.trim()); - } - - private Integer nonNegativeIntegerParameter(Map parameters, String key, Integer fallback) { - if (parameters == null || !parameters.containsKey(key)) { - return fallback; - } - Object value = parameters.get(key); - if (value == null) { - return fallback; - } - if (value instanceof Number number) { - return Math.max(0, number.intValue()); - } - String text = value.toString(); - if (text == null || text.isBlank()) { - return fallback; - } - try { - return Math.max(0, Integer.parseInt(text.trim())); - } catch (NumberFormatException ex) { - throw new IllegalArgumentException("Parameter '" + key + "' must be an integer.", ex); - } - } - - private Integer integerParameter(Map parameters, String key, Integer fallback) { - if (parameters == null || !parameters.containsKey(key)) { - return fallback; - } - Object value = parameters.get(key); - if (value == null) { - return fallback; - } - if (value instanceof Number number) { - return Math.max(1, number.intValue()); - } - String text = value.toString(); - if (text == null || text.isBlank()) { - return fallback; - } - try { - return Math.max(1, Integer.parseInt(text.trim())); - } catch (NumberFormatException ex) { - throw new IllegalArgumentException("Parameter '" + key + "' must be an integer.", ex); - } + RuntimeProcessingExecutionResultDto executionResult = plan.execute(new RuntimeProcessingExecutionApiRequest( + plan.processingPlanKey(), + request.scope(), + request.partitioning(), + List.of(), + request.parameters() + )); + return RuntimeEventProcessingResultDto.fromExecution(executionResult, profileKey()); } } diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidencePartitionValidationDto.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidencePartitionValidationDto.java new file mode 100644 index 0000000..4f66059 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidencePartitionValidationDto.java @@ -0,0 +1,23 @@ +package at.procon.eventhub.processing.eventprocessing.validation; + +import java.util.List; + +public record RuntimeMixedSourceEvidencePartitionValidationDto( + String partitionKey, + String status, + int directDriverEventCount, + int attachedVehicleEvidenceEventCount, + int ignoredVehicleEvidenceEventCount, + int mergedEventCount, + int normalizedSupportEvidenceEventCount, + int unchangedEventCount, + int supportGeoEventCount, + boolean partitionDebugPresent, + List notes, + List warnings +) { + public RuntimeMixedSourceEvidencePartitionValidationDto { + notes = notes == null ? List.of() : List.copyOf(notes); + warnings = warnings == null ? List.of() : List.copyOf(warnings); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationApiRequest.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationApiRequest.java new file mode 100644 index 0000000..8a2943e --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationApiRequest.java @@ -0,0 +1,37 @@ +package at.procon.eventhub.processing.eventprocessing.validation; + +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest; +import java.util.Set; + +public record RuntimeMixedSourceEvidenceValidationApiRequest( + RuntimeEventProcessingApiRequest processingRequest, + Set expectedPartitionKeys, + Integer minimumAttachedVehicleEvidenceEvents, + Integer minimumNormalizedSupportEvidenceEvents, + Boolean failWhenPartitionDebugMissing +) { + public RuntimeMixedSourceEvidenceValidationApiRequest { + if (processingRequest == null) { + throw new IllegalArgumentException("processingRequest must not be null."); + } + expectedPartitionKeys = expectedPartitionKeys == null ? Set.of() : Set.copyOf(expectedPartitionKeys); + if (minimumAttachedVehicleEvidenceEvents != null && minimumAttachedVehicleEvidenceEvents < 0) { + throw new IllegalArgumentException("minimumAttachedVehicleEvidenceEvents must not be negative."); + } + if (minimumNormalizedSupportEvidenceEvents != null && minimumNormalizedSupportEvidenceEvents < 0) { + throw new IllegalArgumentException("minimumNormalizedSupportEvidenceEvents must not be negative."); + } + } + + public int minimumAttachedVehicleEvidenceEventsOrDefault() { + return minimumAttachedVehicleEvidenceEvents == null ? 0 : Math.max(0, minimumAttachedVehicleEvidenceEvents); + } + + public int minimumNormalizedSupportEvidenceEventsOrDefault() { + return minimumNormalizedSupportEvidenceEvents == null ? 0 : Math.max(0, minimumNormalizedSupportEvidenceEvents); + } + + public boolean failWhenPartitionDebugMissingOrDefault() { + return failWhenPartitionDebugMissing == null || failWhenPartitionDebugMissing; + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationResultDto.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationResultDto.java new file mode 100644 index 0000000..41e6b9e --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationResultDto.java @@ -0,0 +1,25 @@ +package at.procon.eventhub.processing.eventprocessing.validation; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public record RuntimeMixedSourceEvidenceValidationResultDto( + String profileKey, + String status, + int partitionCount, + int totalAttachedVehicleEvidenceEventCount, + int totalIgnoredVehicleEvidenceEventCount, + int totalNormalizedSupportEvidenceEventCount, + int totalSupportGeoEventCount, + Map partitions, + List notes, + List warnings +) { + public RuntimeMixedSourceEvidenceValidationResultDto { + partitions = partitions == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(partitions)); + notes = notes == null ? List.of() : List.copyOf(notes); + warnings = warnings == null ? List.of() : List.copyOf(warnings); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationService.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationService.java new file mode 100644 index 0000000..6cd8af4 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationService.java @@ -0,0 +1,174 @@ +package at.procon.eventhub.processing.eventprocessing.validation; + +import at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto; +import at.procon.eventhub.processing.dto.RuntimeSupportEvidenceNormalizationDebugDto; +import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto; +import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService; +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest; +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest; +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto; +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.springframework.stereotype.Service; + +@Service +public class RuntimeMixedSourceEvidenceValidationService { + + private final RuntimeEventProcessingService runtimeEventProcessingService; + + public RuntimeMixedSourceEvidenceValidationService(RuntimeEventProcessingService runtimeEventProcessingService) { + this.runtimeEventProcessingService = runtimeEventProcessingService; + } + + public RuntimeMixedSourceEvidenceValidationResultDto validate(RuntimeMixedSourceEvidenceValidationApiRequest request) { + RuntimeEventProcessingApiRequest debugRequest = withDebugEnabled(request.processingRequest()); + RuntimeEventProcessingResultDto runtimeResult = runtimeEventProcessingService.process(debugRequest); + + LinkedHashMap partitionResults = new LinkedHashMap<>(); + List warnings = new ArrayList<>(runtimeResult.warnings()); + List notes = new ArrayList<>(runtimeResult.notes()); + notes.add("Mixed-source evidence validation executed runtime event processing with partition debug enabled."); + notes.add("Validation checks vehicle-only evidence attachment and support-evidence normalization counts; it does not compare legal tachograph result semantics."); + + int totalAttached = 0; + int totalIgnored = 0; + int totalNormalized = 0; + int totalSupportGeo = 0; + + for (Map.Entry entry : runtimeResult.partitionResults().entrySet()) { + RuntimeMixedSourceEvidencePartitionValidationDto partition = partitionValidation( + entry.getKey(), + entry.getValue(), + request.failWhenPartitionDebugMissingOrDefault() + ); + partitionResults.put(entry.getKey(), partition); + totalAttached += partition.attachedVehicleEvidenceEventCount(); + totalIgnored += partition.ignoredVehicleEvidenceEventCount(); + totalNormalized += partition.normalizedSupportEvidenceEventCount(); + totalSupportGeo += partition.supportGeoEventCount(); + warnings.addAll(partition.warnings()); + } + + Set missingPartitions = new LinkedHashSet<>(request.expectedPartitionKeys()); + missingPartitions.removeAll(partitionResults.keySet()); + for (String missingPartition : missingPartitions) { + warnings.add("Expected partition " + missingPartition + " was not returned by runtime event processing."); + } + if (totalAttached < request.minimumAttachedVehicleEvidenceEventsOrDefault()) { + warnings.add("Attached vehicle evidence count " + totalAttached + " is below expected minimum " + + request.minimumAttachedVehicleEvidenceEventsOrDefault() + "."); + } + if (totalNormalized < request.minimumNormalizedSupportEvidenceEventsOrDefault()) { + warnings.add("Normalized support evidence count " + totalNormalized + " is below expected minimum " + + request.minimumNormalizedSupportEvidenceEventsOrDefault() + "."); + } + + boolean failed = !missingPartitions.isEmpty() + || totalAttached < request.minimumAttachedVehicleEvidenceEventsOrDefault() + || totalNormalized < request.minimumNormalizedSupportEvidenceEventsOrDefault() + || partitionResults.values().stream().anyMatch(partition -> "FAILED".equals(partition.status())); + + return new RuntimeMixedSourceEvidenceValidationResultDto( + runtimeResult.profileKey(), + failed ? "FAILED" : "PASSED", + partitionResults.size(), + totalAttached, + totalIgnored, + totalNormalized, + totalSupportGeo, + partitionResults, + notes, + warnings + ); + } + + private RuntimeMixedSourceEvidencePartitionValidationDto partitionValidation( + String partitionKey, + RuntimeEventProcessingPartitionResultDto partitionResult, + boolean failWhenPartitionDebugMissing + ) { + UnifiedRuntimeDerivedProjectionResultDto result = partitionResult.result() instanceof UnifiedRuntimeDerivedProjectionResultDto derived + ? derived + : null; + RuntimeDriverPartitionDebugDto debug = result == null ? null : result.partitionDebug(); + RuntimeSupportEvidenceNormalizationDebugDto normalization = result == null ? null : result.supportEvidenceNormalization(); + + List notes = new ArrayList<>(); + List warnings = new ArrayList<>(); + if (result == null) { + warnings.add("Partition " + partitionKey + " did not return UnifiedRuntimeDerivedProjectionResultDto; resultType=" + + partitionResult.resultType() + "."); + } + if (debug == null) { + String message = "Partition " + partitionKey + " has no partition debug metadata."; + if (failWhenPartitionDebugMissing) { + warnings.add(message); + } else { + notes.add(message); + } + } else { + notes.addAll(debug.notes()); + warnings.addAll(debug.warnings()); + } + if (normalization == null) { + warnings.add("Partition " + partitionKey + " has no support-evidence normalization metadata."); + } else { + notes.addAll(normalization.notes()); + } + + int directDriverEventCount = debug == null ? (result == null ? 0 : result.driverSeedEventCount()) : debug.directDriverEventCount(); + int attached = debug == null ? (result == null ? 0 : result.expandedVehicleEventCount()) : debug.attachedVehicleEvidenceEventCount(); + int ignored = debug == null ? 0 : debug.ignoredVehicleEvidenceEventCount(); + int merged = debug == null ? (result == null ? 0 : result.mergedEventCount()) : debug.mergedEventCount(); + int normalized = normalization == null ? 0 : normalization.normalizedSupportEvidenceEventCount(); + int unchanged = normalization == null ? 0 : normalization.unchangedEventCount(); + int supportGeo = result == null || result.projection() == null ? 0 : result.projection().supportGeoEventCount(); + + String status = warnings.isEmpty() || (!failWhenPartitionDebugMissing && debug == null && warnings.size() == 1) + ? "PASSED" + : "FAILED"; + return new RuntimeMixedSourceEvidencePartitionValidationDto( + partitionKey, + status, + directDriverEventCount, + attached, + ignored, + merged, + normalized, + unchanged, + supportGeo, + debug != null, + notes, + warnings + ); + } + + private RuntimeEventProcessingApiRequest withDebugEnabled(RuntimeEventProcessingApiRequest request) { + RuntimeEventPartitioningApiRequest partitioning = request.partitioning(); + RuntimeEventPartitioningApiRequest debugPartitioning = new RuntimeEventPartitioningApiRequest( + partitioning.strategy(), + partitioning.partitionKeys(), + partitioning.includeAllPartitions(), + partitioning.driverKeys(), + partitioning.includeAllDrivers(), + partitioning.vehicleKeys(), + partitioning.includeAllVehicles(), + partitioning.attachVehicleEvidence(), + partitioning.vehicleEvidencePaddingMinutes(), + true + ); + Map parameters = new LinkedHashMap<>(request.parameters()); + parameters.put("includePartitionDebug", true); + return new RuntimeEventProcessingApiRequest( + request.profileKey(), + request.scope(), + debugPartitioning, + parameters + ); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeTachographParityValidationService.java b/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeTachographParityValidationService.java index 584099a..b4bea01 100644 --- a/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeTachographParityValidationService.java +++ b/src/main/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeTachographParityValidationService.java @@ -1,6 +1,7 @@ package at.procon.eventhub.processing.eventprocessing.validation; import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto; +import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto; import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest; import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest; @@ -347,6 +348,10 @@ public class RuntimeTachographParityValidationService { } static ProjectionCounts from(TachographEsperDriverProcessingResultDto projection) { + return projection == null ? empty() : from(projection.toDriverWorkingTime()); + } + + static ProjectionCounts from(DriverWorkingTimeProcessingResultDto projection) { if (projection == null) { return empty(); } diff --git a/src/main/java/at/procon/eventhub/processing/service/RuntimeDriverWorkingTimeScopeProcessingService.java b/src/main/java/at/procon/eventhub/processing/service/RuntimeDriverWorkingTimeScopeProcessingService.java new file mode 100644 index 0000000..8234a28 --- /dev/null +++ b/src/main/java/at/procon/eventhub/processing/service/RuntimeDriverWorkingTimeScopeProcessingService.java @@ -0,0 +1,319 @@ +package at.procon.eventhub.processing.service; + +import at.procon.eventhub.dto.DriverRefDto; +import at.procon.eventhub.dto.EventHubEventDto; +import at.procon.eventhub.dto.VehicleRefDto; +import at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto; +import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto; +import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest; +import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto; +import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef; +import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult; +import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle; +import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; +import com.fasterxml.jackson.databind.JsonNode; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import org.springframework.stereotype.Service; + +@Service +public class RuntimeDriverWorkingTimeScopeProcessingService { + + private final UnifiedRuntimeEventAssemblyService eventAssemblyService; + private final UnifiedRuntimeDerivedProjectionService derivedProjectionService; + private final RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService; + + public RuntimeDriverWorkingTimeScopeProcessingService( + UnifiedRuntimeEventAssemblyService eventAssemblyService, + UnifiedRuntimeDerivedProjectionService derivedProjectionService, + RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService + ) { + this.eventAssemblyService = eventAssemblyService; + this.derivedProjectionService = derivedProjectionService; + this.vehicleEvidenceAttachmentService = vehicleEvidenceAttachmentService; + } + + public UnifiedRuntimeDriverWorkingTimeScopeResultDto processScope(UnifiedRuntimeProcessingApiRequest apiRequest) { + return processScope(apiRequest, false); + } + + public UnifiedRuntimeDriverWorkingTimeScopeResultDto processScope( + UnifiedRuntimeProcessingApiRequest apiRequest, + boolean includePartitionDebug + ) { + UnifiedRuntimeProcessingRequest request = apiRequest.toRuntimeRequest(); + UnifiedRuntimeEventBundle broadBundle = eventAssemblyService.assembleDriverScopedEvents(request); + LinkedHashSet selectedDriverKeys = selectedDriverKeys(request, broadBundle.mergedEvents()); + if (selectedDriverKeys.isEmpty()) { + throw new IllegalArgumentException("No driver partitions could be resolved from the runtime event scope."); + } + + Map driverResults = new LinkedHashMap<>(); + Map partitionDebugByDriver = new LinkedHashMap<>(); + Map> attachedVehicleEvidenceByEvent = new LinkedHashMap<>(); + List warnings = new ArrayList<>(); + for (String driverKey : selectedDriverKeys) { + DriverPartition driverPartition = partitionForDriver(request, broadBundle, driverKey, includePartitionDebug); + UnifiedRuntimeEventBundle driverBundle = driverPartition.bundle(); + if (driverPartition.debug() != null) { + partitionDebugByDriver.put(driverKey, driverPartition.debug()); + } + for (EventHubEventDto attachedEvent : driverBundle.expandedVehicleEvents()) { + attachedVehicleEvidenceByEvent + .computeIfAbsent(dedupKey(attachedEvent), ignored -> new ArrayList<>()) + .add(driverKey); + } + driverBundle.notes().stream() + .filter(note -> note.startsWith("WARNING:")) + .map(note -> note.substring("WARNING:".length()).trim()) + .forEach(warnings::add); + if (driverBundle.mergedEvents().isEmpty()) { + warnings.add("No events remained after partitioning runtime scope for driver " + driverKey + "."); + continue; + } + UnifiedRuntimeProcessingRequest driverRequest = request.withDriverKey(driverKey); + UnifiedRuntimeDerivedProjectionResultDto driverResult = derivedProjectionService.buildDriverDerivedProjection( + apiRequest, + driverRequest, + driverBundle, + driverKey + ); + driverResults.put(driverKey, driverPartition.debug() == null ? driverResult : driverResult.withPartitionDebug(driverPartition.debug())); + } + attachedVehicleEvidenceByEvent.forEach((eventKey, drivers) -> { + if (drivers.size() > 1) { + warnings.add("Vehicle-only event " + eventKey + " was attached to multiple driver partitions " + + drivers + "; check overlapping vehicle-usage intervals."); + } + }); + + List notes = new ArrayList<>(broadBundle.notes()); + notes.add("Runtime driver working-time processing used Java-side driver partitioning before calling the common event-input processing pipeline."); + notes.add("Selected driver partitions: " + selectedDriverKeys.size() + "."); + if (!request.includeAllDrivers() && !request.driverKeys().isEmpty()) { + notes.add("The broad runtime event set was filtered to the requested driverKeys."); + } + if (!request.vehicleKeys().isEmpty() || request.includeAllVehicles()) { + notes.add("vehicleKeys/includeAllVehicles are accepted in the request model for source-neutral scopes; driver partitions are enriched only with vehicle evidence that overlaps reconstructed driver vehicle-usage intervals."); + } + notes.add("Vehicle-only evidence attachment is controlled by expandVehicleEvents/attachVehicleOnlyEvents and vehicleExpansionPaddingMinutes/vehicleEvidencePaddingMinutes."); + + return new UnifiedRuntimeDriverWorkingTimeScopeResultDto( + request, + broadBundle.mergedEvents().size(), + driverResults.size(), + broadBundle.discoveredVehicles().size(), + broadBundle.discoveredVehicles(), + driverResults, + partitionDebugByDriver, + notes, + warnings + ); + } + + private LinkedHashSet selectedDriverKeys( + UnifiedRuntimeProcessingRequest request, + List events + ) { + LinkedHashSet allDrivers = discoverDriverKeys(events); + if (request.includeAllDrivers()) { + return allDrivers; + } + LinkedHashSet requested = new LinkedHashSet<>(request.driverKeys()); + if (request.driverKey() != null) { + requested.add(request.driverKey()); + } + if (requested.isEmpty()) { + return allDrivers; + } + LinkedHashSet selected = new LinkedHashSet<>(); + for (String driverKey : allDrivers) { + if (requested.contains(driverKey)) { + selected.add(driverKey); + } + } + for (String driverKey : requested) { + selected.add(driverKey); + } + return selected; + } + + private DriverPartition partitionForDriver( + UnifiedRuntimeProcessingRequest request, + UnifiedRuntimeEventBundle broadBundle, + String driverKey, + boolean includePartitionDebug + ) { + List directDriverEvents = broadBundle.mergedEvents().stream() + .filter(event -> Objects.equals(driverKey(event), driverKey)) + .toList(); + RuntimeDriverVehicleEvidenceAttachmentResult attachmentResult = vehicleEvidenceAttachmentService.attachVehicleEvidence( + driverKey, + directDriverEvents, + broadBundle.mergedEvents(), + request.expandVehicleEvents(), + request.vehicleExpansionPaddingMinutes(), + includePartitionDebug + ); + List driverVehicles = discoverVehicles(attachmentResult.mergedEvents()); + List notes = new ArrayList<>(broadBundle.notes()); + notes.add("Partitioned mixed runtime event scope for driver " + driverKey + "."); + notes.add("Driver direct events: " + attachmentResult.directDriverEvents().size() + "."); + notes.add("Vehicle-only evidence events attached to driver partition: " + attachmentResult.attachedVehicleEvidenceEvents().size() + "."); + notes.add("Vehicle-usage intervals used for temporal evidence attachment: " + attachmentResult.vehicleUsageIntervalCount() + "."); + notes.addAll(attachmentResult.notes()); + attachmentResult.warnings().forEach(warning -> notes.add("WARNING: " + warning)); + UnifiedRuntimeEventBundle bundle = new UnifiedRuntimeEventBundle( + request.withDriverKey(driverKey), + attachmentResult.directDriverEvents(), + driverVehicles, + attachmentResult.attachedVehicleEvidenceEvents(), + attachmentResult.mergedEvents(), + notes + ); + return new DriverPartition( + bundle, + includePartitionDebug ? attachmentResult.toPartitionDebug() : null + ); + } + + private record DriverPartition( + UnifiedRuntimeEventBundle bundle, + RuntimeDriverPartitionDebugDto debug + ) { + } + + private LinkedHashSet discoverDriverKeys(List events) { + LinkedHashSet result = new LinkedHashSet<>(); + for (EventHubEventDto event : sort(events)) { + String driverKey = driverKey(event); + if (driverKey != null) { + result.add(driverKey); + } + } + return result; + } + + private List discoverVehicles(List events) { + List result = new ArrayList<>(); + for (EventHubEventDto event : events) { + UnifiedDiscoveredVehicleRef candidate = vehicleRef(event.vehicleRef()); + if (candidate == null || !candidate.hasAnyReference()) { + continue; + } + boolean merged = false; + for (int i = 0; i < result.size(); i++) { + UnifiedDiscoveredVehicleRef existing = result.get(i); + if (existing.matches(candidate)) { + result.set(i, existing.merge(candidate)); + merged = true; + break; + } + } + if (!merged) { + result.add(candidate); + } + } + result.sort(Comparator.comparing(UnifiedDiscoveredVehicleRef::stableKey)); + return List.copyOf(result); + } + + private boolean matchesAnyVehicle(VehicleRefDto vehicleRef, List vehicles) { + UnifiedDiscoveredVehicleRef candidate = vehicleRef(vehicleRef); + if (candidate == null || !candidate.hasAnyReference()) { + return false; + } + return vehicles.stream().anyMatch(vehicle -> vehicle.matches(candidate)); + } + + private UnifiedDiscoveredVehicleRef vehicleRef(VehicleRefDto vehicleRef) { + if (vehicleRef == null || !vehicleRef.hasAnyReference()) { + return null; + } + return new UnifiedDiscoveredVehicleRef( + vehicleRef.sourceVehicleEntityId(), + vehicleRef.vin(), + vehicleRef.vehicleRegistration() == null + ? null + : vehicleRef.vehicleRegistration().nationNumericCode() == null + ? vehicleRef.vehicleRegistration().nation() + : vehicleRef.vehicleRegistration().nationNumericCode().toString(), + vehicleRef.vehicleRegistration() == null ? null : vehicleRef.vehicleRegistration().number() + ); + } + + private List deduplicateAndSort( + List directDriverEvents, + List vehicleEvidenceEvents + ) { + LinkedHashMap byKey = new LinkedHashMap<>(); + appendDeduplicated(byKey, directDriverEvents); + appendDeduplicated(byKey, vehicleEvidenceEvents); + return sort(new ArrayList<>(byKey.values())); + } + + private void appendDeduplicated(LinkedHashMap byKey, List events) { + for (EventHubEventDto event : events) { + byKey.putIfAbsent(dedupKey(event), event); + } + } + + private String dedupKey(EventHubEventDto event) { + String sourceKey = event.packageInfo() != null && event.packageInfo().eventSource() != null + ? event.packageInfo().eventSource().stableKey() + : "NO_SOURCE"; + return sourceKey + "|" + event.externalSourceEventId(); + } + + private List sort(List events) { + return (events == null ? List.of() : events).stream() + .sorted(Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(event -> event.eventDomain() == null ? "" : event.eventDomain().name()) + .thenComparing(event -> event.eventType() == null ? "" : event.eventType().name()) + .thenComparing(event -> event.lifecycle() == null ? "" : event.lifecycle().name()) + .thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo))) + .toList(); + } + + private String driverKey(EventHubEventDto event) { + if (event == null) { + return null; + } + String rawDriverKey = text(rawPayload(event), "driverKey"); + if (rawDriverKey != null) { + return rawDriverKey; + } + DriverRefDto driverRef = event.driverRef(); + if (driverRef != null && driverRef.hasAnyReference()) { + return driverRef.stableKey(); + } + return null; + } + + private JsonNode rawPayload(EventHubEventDto event) { + JsonNode payload = event.payload(); + if (payload == null || payload.isNull()) { + return null; + } + JsonNode raw = payload.get("raw"); + return raw == null || raw.isNull() ? payload : raw; + } + + private String text(JsonNode node, String field) { + if (node == null || field == null) { + return null; + } + JsonNode value = node.get(field); + if (value == null || value.isNull()) { + return null; + } + String text = value.asText(null); + return text == null || text.isBlank() ? null : text.trim(); + } +} diff --git a/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeDerivedProjectionService.java b/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeDerivedProjectionService.java index f7bd7ad..3db9ea0 100644 --- a/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeDerivedProjectionService.java +++ b/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeDerivedProjectionService.java @@ -4,12 +4,13 @@ import at.procon.eventhub.config.EventHubProperties; import at.procon.eventhub.dto.DriverRefDto; import at.procon.eventhub.dto.EventHubEventDto; import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto; +import at.procon.eventhub.processing.dto.RuntimeSupportEvidenceNormalizationDebugDto; import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest; import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizationResult; import at.procon.eventhub.processing.eventprocessing.support.RuntimeSupportEvidenceNormalizer; import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle; import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; -import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; +import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto; import at.procon.eventhub.tachographfilesession.model.ExtractedSupportEvent; import at.procon.eventhub.tachographfilesession.model.ResolvedDriverTimeline; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; @@ -24,7 +25,7 @@ import at.procon.eventhub.tachographfilesession.model.TachographEsperVehicleUsag import at.procon.eventhub.tachographfilesession.model.TachographEsperVuCardAbsentIntervalEvent; import at.procon.eventhub.tachographfilesession.service.DriverTimelineBuilder; import at.procon.eventhub.tachographfilesession.service.DriverTimelineReusableProjectionBuilder; -import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingCore; +import at.procon.eventhub.processing.driverworkingtime.service.DriverWorkingTimeProcessingCore; import at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingInput; import java.time.Duration; import java.time.OffsetDateTime; @@ -44,7 +45,7 @@ public class UnifiedRuntimeDerivedProjectionService { private final UnifiedEventTimelineReconstructor timelineReconstructor; private final DriverTimelineBuilder driverTimelineBuilder; private final DriverTimelineReusableProjectionBuilder reusableProjectionBuilder; - private final TachographEsperProcessingCore esperProcessingCore; + private final DriverWorkingTimeProcessingCore workingTimeProcessingCore; private final RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer; private final EventHubProperties properties; @@ -62,7 +63,7 @@ public class UnifiedRuntimeDerivedProjectionService { driverTimelineBuilder, reusableProjectionBuilder, properties, - new TachographEsperProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties), + new DriverWorkingTimeProcessingCore(new at.procon.eventhub.tachographfilesession.service.TachographEsperProcessingCore(driverTimelineBuilder, reusableProjectionBuilder, properties)), supportEvidenceNormalizer ); } @@ -74,7 +75,7 @@ public class UnifiedRuntimeDerivedProjectionService { DriverTimelineBuilder driverTimelineBuilder, DriverTimelineReusableProjectionBuilder reusableProjectionBuilder, EventHubProperties properties, - TachographEsperProcessingCore esperProcessingCore, + DriverWorkingTimeProcessingCore workingTimeProcessingCore, RuntimeSupportEvidenceNormalizer supportEvidenceNormalizer ) { this.runtimeEventAssemblyService = runtimeEventAssemblyService; @@ -82,7 +83,7 @@ public class UnifiedRuntimeDerivedProjectionService { this.driverTimelineBuilder = driverTimelineBuilder; this.reusableProjectionBuilder = reusableProjectionBuilder; this.properties = properties; - this.esperProcessingCore = esperProcessingCore; + this.workingTimeProcessingCore = workingTimeProcessingCore; this.supportEvidenceNormalizer = supportEvidenceNormalizer; } @@ -133,14 +134,14 @@ public class UnifiedRuntimeDerivedProjectionService { List notes = new ArrayList<>(eventBundle.notes()); notes.addAll(normalizationResult.notes()); - notes.add("Runtime derived projections were evaluated from the unified merged event stream using normalized support evidence and the shared tachograph Esper processing core."); + notes.add("Runtime derived projections were evaluated from the unified merged event stream using normalized support evidence and the shared driver working-time processing core."); notes.add("Significant driving threshold minutes: " + significantDrivingMinutes + "."); notes.add("Minimum rest candidate period minutes: " + minimumRestPeriodMinutes + "."); if (request.occurredFrom() != null || request.occurredTo() != null) { notes.add("Projection results are filtered to the requested runtime window. For intervals crossing the boundary, include enough source-event padding in the request."); } - TachographEsperDriverProcessingResultDto projection = esperProcessingCore.process(TachographEsperProcessingInput.fromEvents( + DriverWorkingTimeProcessingResultDto projection = workingTimeProcessingCore.process(TachographEsperProcessingInput.fromEvents( runtimeSessionId(request), driverKey, timeline, @@ -153,6 +154,13 @@ public class UnifiedRuntimeDerivedProjectionService { )); notes = projection.notes(); + RuntimeSupportEvidenceNormalizationDebugDto normalizationDebug = new RuntimeSupportEvidenceNormalizationDebugDto( + normalizationResult.inputEventCount(), + normalizationResult.normalizedSupportEvidenceEventCount(), + normalizationResult.unchangedEventCount(), + normalizationResult.notes() + ); + return new UnifiedRuntimeDerivedProjectionResultDto( request, eventBundle.driverSeedEvents().size(), @@ -161,7 +169,8 @@ public class UnifiedRuntimeDerivedProjectionService { eventBundle.mergedEvents().size(), eventBundle.discoveredVehicles(), projection, - notes + notes, + normalizationDebug ); } diff --git a/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeEventAssemblyService.java b/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeEventAssemblyService.java index 3916abc..6f740e9 100644 --- a/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeEventAssemblyService.java +++ b/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeEventAssemblyService.java @@ -142,10 +142,10 @@ public class UnifiedRuntimeEventAssemblyService { appendDeduplicated(byKey, right); return byKey.values().stream() .sorted(Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder())) - .thenComparing(event -> event.eventDomain().name()) - .thenComparing(event -> event.eventType().name()) - .thenComparing(event -> event.lifecycle().name()) - .thenComparing(EventHubEventDto::externalSourceEventId)) + .thenComparing(event -> event.eventDomain() == null ? "" : event.eventDomain().name()) + .thenComparing(event -> event.eventType() == null ? "" : event.eventType().name()) + .thenComparing(event -> event.lifecycle() == null ? "" : event.lifecycle().name()) + .thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo))) .toList(); } diff --git a/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeTachographEsperScopeProcessingService.java b/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeTachographEsperScopeProcessingService.java index 152315e..da39cd3 100644 --- a/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeTachographEsperScopeProcessingService.java +++ b/src/main/java/at/procon/eventhub/processing/service/UnifiedRuntimeTachographEsperScopeProcessingService.java @@ -1,319 +1,48 @@ package at.procon.eventhub.processing.service; -import at.procon.eventhub.dto.DriverRefDto; -import at.procon.eventhub.dto.EventHubEventDto; -import at.procon.eventhub.dto.VehicleRefDto; -import at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto; -import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto; +import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto; import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest; import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto; -import at.procon.eventhub.processing.model.UnifiedDiscoveredVehicleRef; -import at.procon.eventhub.processing.model.RuntimeDriverVehicleEvidenceAttachmentResult; -import at.procon.eventhub.processing.model.UnifiedRuntimeEventBundle; -import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; -import com.fasterxml.jackson.databind.JsonNode; -import java.util.ArrayList; -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Set; -import org.springframework.stereotype.Service; -@Service +/** + * @deprecated Compatibility adapter. Use {@link RuntimeDriverWorkingTimeScopeProcessingService}; + * tachograph files/databases are only one possible source for the common driver working-time runtime plan. + */ +@Deprecated(forRemoval = false) public class UnifiedRuntimeTachographEsperScopeProcessingService { - private final UnifiedRuntimeEventAssemblyService eventAssemblyService; - private final UnifiedRuntimeDerivedProjectionService derivedProjectionService; - private final RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService; + private final RuntimeDriverWorkingTimeScopeProcessingService delegate; public UnifiedRuntimeTachographEsperScopeProcessingService( - UnifiedRuntimeEventAssemblyService eventAssemblyService, - UnifiedRuntimeDerivedProjectionService derivedProjectionService, - RuntimeDriverVehicleEvidenceAttachmentService vehicleEvidenceAttachmentService + RuntimeDriverWorkingTimeScopeProcessingService delegate ) { - this.eventAssemblyService = eventAssemblyService; - this.derivedProjectionService = derivedProjectionService; - this.vehicleEvidenceAttachmentService = vehicleEvidenceAttachmentService; + this.delegate = delegate; } public UnifiedRuntimeTachographEsperScopeResultDto processScope(UnifiedRuntimeProcessingApiRequest apiRequest) { - return processScope(apiRequest, false); + return toLegacy(delegate.processScope(apiRequest)); } public UnifiedRuntimeTachographEsperScopeResultDto processScope( UnifiedRuntimeProcessingApiRequest apiRequest, boolean includePartitionDebug ) { - UnifiedRuntimeProcessingRequest request = apiRequest.toRuntimeRequest(); - UnifiedRuntimeEventBundle broadBundle = eventAssemblyService.assembleDriverScopedEvents(request); - LinkedHashSet selectedDriverKeys = selectedDriverKeys(request, broadBundle.mergedEvents()); - if (selectedDriverKeys.isEmpty()) { - throw new IllegalArgumentException("No driver partitions could be resolved from the runtime event scope."); - } - - Map driverResults = new LinkedHashMap<>(); - Map partitionDebugByDriver = new LinkedHashMap<>(); - Map> attachedVehicleEvidenceByEvent = new LinkedHashMap<>(); - List warnings = new ArrayList<>(); - for (String driverKey : selectedDriverKeys) { - DriverPartition driverPartition = partitionForDriver(request, broadBundle, driverKey, includePartitionDebug); - UnifiedRuntimeEventBundle driverBundle = driverPartition.bundle(); - if (driverPartition.debug() != null) { - partitionDebugByDriver.put(driverKey, driverPartition.debug()); - } - for (EventHubEventDto attachedEvent : driverBundle.expandedVehicleEvents()) { - attachedVehicleEvidenceByEvent - .computeIfAbsent(dedupKey(attachedEvent), ignored -> new ArrayList<>()) - .add(driverKey); - } - driverBundle.notes().stream() - .filter(note -> note.startsWith("WARNING:")) - .map(note -> note.substring("WARNING:".length()).trim()) - .forEach(warnings::add); - if (driverBundle.mergedEvents().isEmpty()) { - warnings.add("No events remained after partitioning runtime scope for driver " + driverKey + "."); - continue; - } - UnifiedRuntimeProcessingRequest driverRequest = request.withDriverKey(driverKey); - UnifiedRuntimeDerivedProjectionResultDto driverResult = derivedProjectionService.buildDriverDerivedProjection( - apiRequest, - driverRequest, - driverBundle, - driverKey - ); - driverResults.put(driverKey, driverPartition.debug() == null ? driverResult : driverResult.withPartitionDebug(driverPartition.debug())); - } - attachedVehicleEvidenceByEvent.forEach((eventKey, drivers) -> { - if (drivers.size() > 1) { - warnings.add("Vehicle-only event " + eventKey + " was attached to multiple driver partitions " - + drivers + "; check overlapping vehicle-usage intervals."); - } - }); - - List notes = new ArrayList<>(broadBundle.notes()); - notes.add("Runtime tachograph Esper scope processing used Java-side driver partitioning before calling the common event-input Esper pipeline."); - notes.add("Selected driver partitions: " + selectedDriverKeys.size() + "."); - if (!request.includeAllDrivers() && !request.driverKeys().isEmpty()) { - notes.add("The broad runtime event set was filtered to the requested driverKeys."); - } - if (!request.vehicleKeys().isEmpty() || request.includeAllVehicles()) { - notes.add("vehicleKeys/includeAllVehicles are accepted in the request model for source-neutral scopes; driver partitions are enriched only with vehicle evidence that overlaps reconstructed driver vehicle-usage intervals."); - } - notes.add("Vehicle-only evidence attachment is controlled by expandVehicleEvents/attachVehicleOnlyEvents and vehicleExpansionPaddingMinutes/vehicleEvidencePaddingMinutes."); + return toLegacy(delegate.processScope(apiRequest, includePartitionDebug)); + } + private UnifiedRuntimeTachographEsperScopeResultDto toLegacy( + UnifiedRuntimeDriverWorkingTimeScopeResultDto result + ) { return new UnifiedRuntimeTachographEsperScopeResultDto( - request, - broadBundle.mergedEvents().size(), - driverResults.size(), - broadBundle.discoveredVehicles().size(), - broadBundle.discoveredVehicles(), - driverResults, - partitionDebugByDriver, - notes, - warnings + result.request(), + result.inputEventCount(), + result.selectedDriverCount(), + result.discoveredVehicleCount(), + result.discoveredVehicles(), + result.driverResults(), + result.partitionDebugByDriver(), + result.notes(), + result.warnings() ); } - - private LinkedHashSet selectedDriverKeys( - UnifiedRuntimeProcessingRequest request, - List events - ) { - LinkedHashSet allDrivers = discoverDriverKeys(events); - if (request.includeAllDrivers()) { - return allDrivers; - } - LinkedHashSet requested = new LinkedHashSet<>(request.driverKeys()); - if (request.driverKey() != null) { - requested.add(request.driverKey()); - } - if (requested.isEmpty()) { - return allDrivers; - } - LinkedHashSet selected = new LinkedHashSet<>(); - for (String driverKey : allDrivers) { - if (requested.contains(driverKey)) { - selected.add(driverKey); - } - } - for (String driverKey : requested) { - selected.add(driverKey); - } - return selected; - } - - private DriverPartition partitionForDriver( - UnifiedRuntimeProcessingRequest request, - UnifiedRuntimeEventBundle broadBundle, - String driverKey, - boolean includePartitionDebug - ) { - List directDriverEvents = broadBundle.mergedEvents().stream() - .filter(event -> Objects.equals(driverKey(event), driverKey)) - .toList(); - RuntimeDriverVehicleEvidenceAttachmentResult attachmentResult = vehicleEvidenceAttachmentService.attachVehicleEvidence( - driverKey, - directDriverEvents, - broadBundle.mergedEvents(), - request.expandVehicleEvents(), - request.vehicleExpansionPaddingMinutes(), - includePartitionDebug - ); - List driverVehicles = discoverVehicles(attachmentResult.mergedEvents()); - List notes = new ArrayList<>(broadBundle.notes()); - notes.add("Partitioned mixed runtime event scope for driver " + driverKey + "."); - notes.add("Driver direct events: " + attachmentResult.directDriverEvents().size() + "."); - notes.add("Vehicle-only evidence events attached to driver partition: " + attachmentResult.attachedVehicleEvidenceEvents().size() + "."); - notes.add("Vehicle-usage intervals used for temporal evidence attachment: " + attachmentResult.vehicleUsageIntervalCount() + "."); - notes.addAll(attachmentResult.notes()); - attachmentResult.warnings().forEach(warning -> notes.add("WARNING: " + warning)); - UnifiedRuntimeEventBundle bundle = new UnifiedRuntimeEventBundle( - request.withDriverKey(driverKey), - attachmentResult.directDriverEvents(), - driverVehicles, - attachmentResult.attachedVehicleEvidenceEvents(), - attachmentResult.mergedEvents(), - notes - ); - return new DriverPartition( - bundle, - includePartitionDebug ? attachmentResult.toPartitionDebug() : null - ); - } - - private record DriverPartition( - UnifiedRuntimeEventBundle bundle, - RuntimeDriverPartitionDebugDto debug - ) { - } - - private LinkedHashSet discoverDriverKeys(List events) { - LinkedHashSet result = new LinkedHashSet<>(); - for (EventHubEventDto event : sort(events)) { - String driverKey = driverKey(event); - if (driverKey != null) { - result.add(driverKey); - } - } - return result; - } - - private List discoverVehicles(List events) { - List result = new ArrayList<>(); - for (EventHubEventDto event : events) { - UnifiedDiscoveredVehicleRef candidate = vehicleRef(event.vehicleRef()); - if (candidate == null || !candidate.hasAnyReference()) { - continue; - } - boolean merged = false; - for (int i = 0; i < result.size(); i++) { - UnifiedDiscoveredVehicleRef existing = result.get(i); - if (existing.matches(candidate)) { - result.set(i, existing.merge(candidate)); - merged = true; - break; - } - } - if (!merged) { - result.add(candidate); - } - } - result.sort(Comparator.comparing(UnifiedDiscoveredVehicleRef::stableKey)); - return List.copyOf(result); - } - - private boolean matchesAnyVehicle(VehicleRefDto vehicleRef, List vehicles) { - UnifiedDiscoveredVehicleRef candidate = vehicleRef(vehicleRef); - if (candidate == null || !candidate.hasAnyReference()) { - return false; - } - return vehicles.stream().anyMatch(vehicle -> vehicle.matches(candidate)); - } - - private UnifiedDiscoveredVehicleRef vehicleRef(VehicleRefDto vehicleRef) { - if (vehicleRef == null || !vehicleRef.hasAnyReference()) { - return null; - } - return new UnifiedDiscoveredVehicleRef( - vehicleRef.sourceVehicleEntityId(), - vehicleRef.vin(), - vehicleRef.vehicleRegistration() == null - ? null - : vehicleRef.vehicleRegistration().nationNumericCode() == null - ? vehicleRef.vehicleRegistration().nation() - : vehicleRef.vehicleRegistration().nationNumericCode().toString(), - vehicleRef.vehicleRegistration() == null ? null : vehicleRef.vehicleRegistration().number() - ); - } - - private List deduplicateAndSort( - List directDriverEvents, - List vehicleEvidenceEvents - ) { - LinkedHashMap byKey = new LinkedHashMap<>(); - appendDeduplicated(byKey, directDriverEvents); - appendDeduplicated(byKey, vehicleEvidenceEvents); - return sort(new ArrayList<>(byKey.values())); - } - - private void appendDeduplicated(LinkedHashMap byKey, List events) { - for (EventHubEventDto event : events) { - byKey.putIfAbsent(dedupKey(event), event); - } - } - - private String dedupKey(EventHubEventDto event) { - String sourceKey = event.packageInfo() != null && event.packageInfo().eventSource() != null - ? event.packageInfo().eventSource().stableKey() - : "NO_SOURCE"; - return sourceKey + "|" + event.externalSourceEventId(); - } - - private List sort(List events) { - return (events == null ? List.of() : events).stream() - .sorted(Comparator.comparing(EventHubEventDto::occurredAt, Comparator.nullsLast(Comparator.naturalOrder())) - .thenComparing(event -> event.eventDomain() == null ? "" : event.eventDomain().name()) - .thenComparing(event -> event.eventType() == null ? "" : event.eventType().name()) - .thenComparing(event -> event.lifecycle() == null ? "" : event.lifecycle().name()) - .thenComparing(EventHubEventDto::externalSourceEventId, Comparator.nullsLast(String::compareTo))) - .toList(); - } - - private String driverKey(EventHubEventDto event) { - if (event == null) { - return null; - } - String rawDriverKey = text(rawPayload(event), "driverKey"); - if (rawDriverKey != null) { - return rawDriverKey; - } - DriverRefDto driverRef = event.driverRef(); - if (driverRef != null && driverRef.hasAnyReference()) { - return driverRef.stableKey(); - } - return null; - } - - private JsonNode rawPayload(EventHubEventDto event) { - JsonNode payload = event.payload(); - if (payload == null || payload.isNull()) { - return null; - } - JsonNode raw = payload.get("raw"); - return raw == null || raw.isNull() ? payload : raw; - } - - private String text(JsonNode node, String field) { - if (node == null || field == null) { - return null; - } - JsonNode value = node.get(field); - if (value == null || value.isNull()) { - return null; - } - String text = value.asText(null); - return text == null || text.isBlank() ? null : text.trim(); - } } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java index d499a37..f788d6d 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/dto/TachographEsperDriverProcessingResultDto.java @@ -1,5 +1,6 @@ package at.procon.eventhub.tachographfilesession.dto; +import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto; import at.procon.eventhub.tachographfilesession.model.TachographEsperActivityIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDailyWeeklyRestCandidateCoverageIntervalEvent; import at.procon.eventhub.tachographfilesession.model.TachographEsperDrivingInterruptionIntervalEvent; @@ -13,6 +14,7 @@ import java.time.OffsetDateTime; import java.util.List; import java.util.UUID; +@Deprecated(forRemoval = false) public record TachographEsperDriverProcessingResultDto( UUID sessionId, String driverKey, @@ -49,4 +51,86 @@ public record TachographEsperDriverProcessingResultDto( List supportGeoEvents, List notes ) { + public static TachographEsperDriverProcessingResultDto fromDriverWorkingTime( + DriverWorkingTimeProcessingResultDto result + ) { + if (result == null) { + return null; + } + return new TachographEsperDriverProcessingResultDto( + result.sessionId(), + result.driverKey(), + result.sourceKind(), + result.loadedFrom(), + result.loadedTo(), + result.requestedFrom(), + result.requestedTo(), + result.activityIntervalCount(), + result.drivingIntervalCount(), + result.drivingInterruptionIntervalCount(), + result.drivingInterruptionVehicleChangeIntervalCount(), + result.dailyWeeklyRestCandidateIntervalCount(), + result.dailyWeeklyRestCandidateCoverageIntervalCount(), + result.unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount(), + result.potentialHomeOvernightStayIntervalCount(), + result.potentialInVehicleOvernightStayIntervalCount(), + result.potentialInVehicleTripIntervalCount(), + result.vehicleUsageIntervalCount(), + result.vuCardAbsentIntervalCount(), + result.supportGeoEventCount(), + result.activityIntervals(), + result.drivingIntervals(), + result.drivingInterruptionIntervals(), + result.drivingInterruptionVehicleChangeIntervals(), + result.dailyWeeklyRestCandidateIntervals(), + result.dailyWeeklyRestCandidateCoverageIntervals(), + result.unclassifiedDailyWeeklyRestCandidateCoverageIntervals(), + result.potentialHomeOvernightStayIntervals(), + result.potentialInVehicleOvernightStayIntervals(), + result.potentialInVehicleTripIntervals(), + result.vehicleUsageIntervals(), + result.vuCardAbsentIntervals(), + result.supportGeoEvents(), + result.notes() + ); + } + + public DriverWorkingTimeProcessingResultDto toDriverWorkingTime() { + return new DriverWorkingTimeProcessingResultDto( + sessionId, + driverKey, + sourceKind, + loadedFrom, + loadedTo, + requestedFrom, + requestedTo, + activityIntervalCount, + drivingIntervalCount, + drivingInterruptionIntervalCount, + drivingInterruptionVehicleChangeIntervalCount, + dailyWeeklyRestCandidateIntervalCount, + dailyWeeklyRestCandidateCoverageIntervalCount, + unclassifiedDailyWeeklyRestCandidateCoverageIntervalCount, + potentialHomeOvernightStayIntervalCount, + potentialInVehicleOvernightStayIntervalCount, + potentialInVehicleTripIntervalCount, + vehicleUsageIntervalCount, + vuCardAbsentIntervalCount, + supportGeoEventCount, + activityIntervals, + drivingIntervals, + drivingInterruptionIntervals, + drivingInterruptionVehicleChangeIntervals, + dailyWeeklyRestCandidateIntervals, + dailyWeeklyRestCandidateCoverageIntervals, + unclassifiedDailyWeeklyRestCandidateCoverageIntervals, + potentialHomeOvernightStayIntervals, + potentialInVehicleOvernightStayIntervals, + potentialInVehicleTripIntervals, + vehicleUsageIntervals, + vuCardAbsentIntervals, + supportGeoEvents, + notes + ); + } } diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java index bab923e..7852ef2 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/DriverTimelineReusableProjectionBuilder.java @@ -55,9 +55,9 @@ public class DriverTimelineReusableProjectionBuilder { private static final AtomicLong RUNTIME_COUNTER = new AtomicLong(); private static final String DRIVING_DERIVED_PROJECTION_BUNDLE_EPL_TEMPLATE = - loadResource("esper/tachograph-driving-derived-projection-bundle.epl"); + loadResource("esper/driver-working-time-derived-projections.epl"); private static final String DRIVING_DERIVED_PROJECTION_EVENTS_PREPROCESSOR_EPL = - loadResource("esper/tachograph-driving-derived-projection-events-preprocessor.epl"); + loadResource("esper/runtime-driver-event-interval-preprocessor.epl"); private final DriverTimelineBuilder driverTimelineBuilder; private final RawSourceDriverTimelineEventBuilder rawSourceEventBuilder; diff --git a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingCore.java b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingCore.java index f71747c..5a3a73f 100644 --- a/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingCore.java +++ b/src/main/java/at/procon/eventhub/tachographfilesession/service/TachographEsperProcessingCore.java @@ -1,6 +1,7 @@ package at.procon.eventhub.tachographfilesession.service; import at.procon.eventhub.config.EventHubProperties; +import at.procon.eventhub.processing.driverworkingtime.dto.DriverWorkingTimeProcessingResultDto; import at.procon.eventhub.tachographfilesession.dto.TachographEsperDriverProcessingResultDto; import at.procon.eventhub.tachographfilesession.model.*; import java.time.Duration; @@ -12,6 +13,7 @@ import java.util.Objects; import org.springframework.stereotype.Service; @Service +@Deprecated(forRemoval = false) public class TachographEsperProcessingCore { private final DriverTimelineBuilder driverTimelineBuilder; @@ -29,6 +31,10 @@ public class TachographEsperProcessingCore { } public TachographEsperDriverProcessingResultDto process(TachographEsperProcessingInput input) { + return TachographEsperDriverProcessingResultDto.fromDriverWorkingTime(processDriverWorkingTime(input)); + } + + public DriverWorkingTimeProcessingResultDto processDriverWorkingTime(TachographEsperProcessingInput input) { Objects.requireNonNull(input, "input must not be null"); ResolvedDriverTimeline timeline = Objects.requireNonNull(input.timeline(), "timeline must not be null"); String driverKey = input.driverKey(); @@ -132,7 +138,7 @@ public class TachographEsperProcessingCore { requestedTo ); - return new TachographEsperDriverProcessingResultDto( + return new DriverWorkingTimeProcessingResultDto( input.sessionId(), driverKey, timeline.sourceKind(), diff --git a/src/main/resources/esper/driver-working-time-derived-projections.epl b/src/main/resources/esper/driver-working-time-derived-projections.epl new file mode 100644 index 0000000..c02e4ed --- /dev/null +++ b/src/main/resources/esper/driver-working-time-derived-projections.epl @@ -0,0 +1,1792 @@ +/* + * driver-working-time-derived-projections.epl + * + * Source-neutral driver working-time derived projection rules over canonical driver activity, + * vehicle-usage, and support-evidence interval streams. Tachograph is only one source + * that can produce these canonical input streams. + */ + +create schema SignificantDrivingInterval( + sessionId java.util.UUID, + driverKey string, + firstSourceIntervalId string, + lastSourceIntervalId string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + registrationKey string, + vehicleKey string +); + +create schema DrivingInterruptionInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create schema DailyWeeklyRestCandidateInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create schema DrivingInterruptionVehicleChangeInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create schema DrivingInterruptionVehicleNotChangedInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create schema DailyWeeklyRestCandidateCoverageUnknownResolvedInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + cardAbsentDurationSeconds long, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create schema DailyWeeklyRestCandidateCoverageCardResolvedInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + cardAbsentDurationSeconds long, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create schema DailyWeeklyRestCandidateCoverageInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + cardAbsentDurationSeconds long, + cardAbsentCoveragePercent double, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string, + beginBoundaryOdometerKm java.lang.Long, + endBoundaryOdometerKm java.lang.Long, + beginGeoEventId string, + beginGeoEventDomain string, + beginGeoOccurredAtEpochSecond java.lang.Long, + beginLatitude java.lang.Double, + beginLongitude java.lang.Double, + beginGeoDistanceSeconds java.lang.Long, + beginGeoOdometerKm java.lang.Long, + endGeoEventId string, + endGeoEventDomain string, + endGeoOccurredAtEpochSecond java.lang.Long, + endLatitude java.lang.Double, + endLongitude java.lang.Double, + endGeoDistanceSeconds java.lang.Long, + endGeoOdometerKm java.lang.Long, + geoEvidenceMovementMeters java.lang.Long, + geoEvidenceMovementCategory string +); + +create schema SupportGeoEvidence( + sessionId java.util.UUID, + driverKey string, + eventId string, + eventDomain string, + occurredAtEpochSecond long, + registrationKey string, + vehicleKey string, + latitude java.lang.Double, + longitude java.lang.Double, + odometerKm java.lang.Long, + priority int +); + +create schema DailyWeeklyRestCandidateBeginGeoEvidenceCandidate( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + boundaryOdometerKm java.lang.Long, + eventId string, + eventDomain string, + occurredAtEpochSecond long, + latitude java.lang.Double, + longitude java.lang.Double, + odometerKm java.lang.Long, + odometerDeltaKm java.lang.Long, + distanceSeconds long, + rankScore long +); + +create schema DailyWeeklyRestCandidateBeginGeoEvidenceBestScore( + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + bestRankScore long +); + +create schema DailyWeeklyRestCandidateBeginGeoEvidenceResolved( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + eventId string, + eventDomain string, + occurredAtEpochSecond long, + latitude java.lang.Double, + longitude java.lang.Double, + odometerKm java.lang.Long, + distanceSeconds long +); + +create schema DailyWeeklyRestCandidateEndGeoEvidenceCandidate( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + boundaryOdometerKm java.lang.Long, + eventId string, + eventDomain string, + occurredAtEpochSecond long, + latitude java.lang.Double, + longitude java.lang.Double, + odometerKm java.lang.Long, + odometerDeltaKm java.lang.Long, + distanceSeconds long, + rankScore long +); + +create schema DailyWeeklyRestCandidateBeginBoundaryOdometerCandidate( + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + odometerKm java.lang.Long, + distanceSeconds long, + rankScore long +); + +create schema DailyWeeklyRestCandidateBeginBoundaryOdometerBestScore( + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + bestRankScore long +); + +create schema DailyWeeklyRestCandidateBeginBoundaryOdometerResolved( + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + odometerKm java.lang.Long, + distanceSeconds long +); + +create schema DailyWeeklyRestCandidateEndBoundaryOdometerCandidate( + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + odometerKm java.lang.Long, + distanceSeconds long, + rankScore long +); + +create schema DailyWeeklyRestCandidateEndBoundaryOdometerBestScore( + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + bestRankScore long +); + +create schema DailyWeeklyRestCandidateEndBoundaryOdometerResolved( + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + odometerKm java.lang.Long, + distanceSeconds long +); + +create schema DailyWeeklyRestCandidateEndGeoEvidenceBestScore( + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + bestRankScore long +); + +create schema DailyWeeklyRestCandidateEndGeoEvidenceResolved( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + eventId string, + eventDomain string, + occurredAtEpochSecond long, + latitude java.lang.Double, + longitude java.lang.Double, + odometerKm java.lang.Long, + distanceSeconds long +); + +create schema DailyWeeklyRestCandidateCoverageFinalizationRequest( + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long +); + +create schema DailyWeeklyRestCandidateCoverageEmittedKey( + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long +); + +create context PerDriver partition by driverKey from TachographVehicleUsageIntervalInputEvent; + +create schema VuCardAbsentInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + previousUsageIntervalId string, + nextUsageIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string +); + +create schema PotentialHomeOvernightStayInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + cardAbsentDurationSeconds long, + cardAbsentCoveragePercent double, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string, + beginBoundaryOdometerKm java.lang.Long, + endBoundaryOdometerKm java.lang.Long, + beginGeoEventId string, + beginGeoEventDomain string, + beginGeoOccurredAtEpochSecond java.lang.Long, + beginLatitude java.lang.Double, + beginLongitude java.lang.Double, + beginGeoDistanceSeconds java.lang.Long, + beginGeoOdometerKm java.lang.Long, + endGeoEventId string, + endGeoEventDomain string, + endGeoOccurredAtEpochSecond java.lang.Long, + endLatitude java.lang.Double, + endLongitude java.lang.Double, + endGeoDistanceSeconds java.lang.Long, + endGeoOdometerKm java.lang.Long, + geoEvidenceMovementMeters java.lang.Long, + geoEvidenceMovementCategory string +); + +create schema PotentialInVehicleOvernightStayInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + cardAbsentDurationSeconds long, + cardAbsentCoveragePercent double, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string, + beginBoundaryOdometerKm java.lang.Long, + endBoundaryOdometerKm java.lang.Long, + beginGeoEventId string, + beginGeoEventDomain string, + beginGeoOccurredAtEpochSecond java.lang.Long, + beginLatitude java.lang.Double, + beginLongitude java.lang.Double, + beginGeoDistanceSeconds java.lang.Long, + beginGeoOdometerKm java.lang.Long, + endGeoEventId string, + endGeoEventDomain string, + endGeoOccurredAtEpochSecond java.lang.Long, + endLatitude java.lang.Double, + endLongitude java.lang.Double, + endGeoDistanceSeconds java.lang.Long, + endGeoOdometerKm java.lang.Long, + geoEvidenceMovementMeters java.lang.Long, + geoEvidenceMovementCategory string +); + +create schema UnclassifiedDailyWeeklyRestCandidateCoverageInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + cardAbsentDurationSeconds long, + cardAbsentCoveragePercent double, + previousDrivingSourceIntervalId string, + nextDrivingSourceIntervalId string, + previousRegistrationKey string, + nextRegistrationKey string, + previousVehicleKey string, + nextVehicleKey string, + beginBoundaryOdometerKm java.lang.Long, + endBoundaryOdometerKm java.lang.Long, + beginGeoEventId string, + beginGeoEventDomain string, + beginGeoOccurredAtEpochSecond java.lang.Long, + beginLatitude java.lang.Double, + beginLongitude java.lang.Double, + beginGeoDistanceSeconds java.lang.Long, + beginGeoOdometerKm java.lang.Long, + endGeoEventId string, + endGeoEventDomain string, + endGeoOccurredAtEpochSecond java.lang.Long, + endLatitude java.lang.Double, + endLongitude java.lang.Double, + endGeoDistanceSeconds java.lang.Long, + endGeoOdometerKm java.lang.Long, + geoEvidenceMovementMeters java.lang.Long, + geoEvidenceMovementCategory string +); + +create schema PotentialInVehicleTripState( + sessionId java.util.UUID, + driverKey string, + tripStartedAtEpochSecond long, + registrationKey string, + vehicleKey string, + containedPotentialInVehicleOvernightStayIntervalCount int, + containedPotentialInVehicleOvernightStayDurationSeconds long, + containedCardAbsentDurationSeconds long, + firstPotentialInVehicleOvernightStayStartedAtEpochSecond long, + lastPotentialInVehicleOvernightStayEndedAtEpochSecond long, + firstPreviousDrivingSourceIntervalId string, + lastNextDrivingSourceIntervalId string +); + +create schema PotentialInVehicleTripInterval( + sessionId java.util.UUID, + driverKey string, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + registrationKey string, + vehicleKey string, + containedPotentialInVehicleOvernightStayIntervalCount int, + containedPotentialInVehicleOvernightStayDurationSeconds long, + containedCardAbsentDurationSeconds long, + firstPotentialInVehicleOvernightStayStartedAtEpochSecond long, + lastPotentialInVehicleOvernightStayEndedAtEpochSecond long, + firstPreviousDrivingSourceIntervalId string, + lastNextDrivingSourceIntervalId string +); + +create window PreviousRestCandidateCoverageInterval#unique(driverKey) as DailyWeeklyRestCandidateCoverageInterval; + +create window OpenPotentialInVehicleTripState#unique(driverKey) as PotentialInVehicleTripState; + +create window SupportGeoEvidenceWindow#keepall as SupportGeoEvidence; + +create window DailyWeeklyRestCandidateBeginGeoEvidenceCandidateWindow#keepall as DailyWeeklyRestCandidateBeginGeoEvidenceCandidate; +create window DailyWeeklyRestCandidateBeginGeoEvidenceBestScoreWindow#unique(driverKey, startedAtEpochSecond, endedAtEpochSecond) as DailyWeeklyRestCandidateBeginGeoEvidenceBestScore; +create window DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow#unique(driverKey, startedAtEpochSecond, endedAtEpochSecond) as DailyWeeklyRestCandidateBeginGeoEvidenceResolved; + +create window DailyWeeklyRestCandidateEndGeoEvidenceCandidateWindow#keepall as DailyWeeklyRestCandidateEndGeoEvidenceCandidate; +create window DailyWeeklyRestCandidateEndGeoEvidenceBestScoreWindow#unique(driverKey, startedAtEpochSecond, endedAtEpochSecond) as DailyWeeklyRestCandidateEndGeoEvidenceBestScore; +create window DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow#unique(driverKey, startedAtEpochSecond, endedAtEpochSecond) as DailyWeeklyRestCandidateEndGeoEvidenceResolved; +create window DailyWeeklyRestCandidateBeginBoundaryOdometerCandidateWindow#keepall as DailyWeeklyRestCandidateBeginBoundaryOdometerCandidate; +create window DailyWeeklyRestCandidateBeginBoundaryOdometerBestScoreWindow#unique(driverKey, startedAtEpochSecond, endedAtEpochSecond) as DailyWeeklyRestCandidateBeginBoundaryOdometerBestScore; +create window DailyWeeklyRestCandidateBeginBoundaryOdometerResolvedWindow#unique(driverKey, startedAtEpochSecond, endedAtEpochSecond) as DailyWeeklyRestCandidateBeginBoundaryOdometerResolved; +create window DailyWeeklyRestCandidateEndBoundaryOdometerCandidateWindow#keepall as DailyWeeklyRestCandidateEndBoundaryOdometerCandidate; +create window DailyWeeklyRestCandidateEndBoundaryOdometerBestScoreWindow#unique(driverKey, startedAtEpochSecond, endedAtEpochSecond) as DailyWeeklyRestCandidateEndBoundaryOdometerBestScore; +create window DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow#unique(driverKey, startedAtEpochSecond, endedAtEpochSecond) as DailyWeeklyRestCandidateEndBoundaryOdometerResolved; +create window DailyWeeklyRestCandidateCoverageCardResolvedIntervalWindow#unique(driverKey, startedAtEpochSecond, endedAtEpochSecond) as DailyWeeklyRestCandidateCoverageCardResolvedInterval; +create window DailyWeeklyRestCandidateCoverageEmittedKeyWindow#unique(driverKey, startedAtEpochSecond, endedAtEpochSecond) as DailyWeeklyRestCandidateCoverageEmittedKey; + +insert into SupportGeoEvidenceWindow +select + sessionId, + driverKey, + eventId, + eventDomain, + occurredAtEpochSecond, + registrationKey, + vehicleKey, + latitude, + longitude, + odometerKm, + priority +from TachographSupportGeoEvidenceInputEvent; + +insert into SignificantDrivingInterval +select + sessionId, + driverKey, + firstSourceIntervalId, + lastSourceIntervalId, + startedAtEpochSecond, + endedAtEpochSecond, + durationSeconds, + registrationKey, + vehicleKey +from TachographActivityIntervalInputEvent(activityType = 'DRIVE', durationSeconds > ${SIGNIFICANT_DRIVING_THRESHOLD_SECONDS}); + +create window PreviousSignificantDrivingInterval#unique(driverKey) as SignificantDrivingInterval; + +on SignificantDrivingInterval as next +insert into DrivingInterruptionInterval +select + priorInterval.sessionId as sessionId, + priorInterval.driverKey as driverKey, + priorInterval.endedAtEpochSecond as startedAtEpochSecond, + next.startedAtEpochSecond as endedAtEpochSecond, + next.startedAtEpochSecond - priorInterval.endedAtEpochSecond as durationSeconds, + priorInterval.lastSourceIntervalId as previousDrivingSourceIntervalId, + next.firstSourceIntervalId as nextDrivingSourceIntervalId, + priorInterval.registrationKey as previousRegistrationKey, + next.registrationKey as nextRegistrationKey, + priorInterval.vehicleKey as previousVehicleKey, + next.vehicleKey as nextVehicleKey +from PreviousSignificantDrivingInterval as priorInterval +where priorInterval.driverKey = next.driverKey + and next.startedAtEpochSecond > priorInterval.endedAtEpochSecond; + +@Priority(20) +on SignificantDrivingInterval +delete from PreviousSignificantDrivingInterval; + +@Priority(10) +on SignificantDrivingInterval as current +insert into PreviousSignificantDrivingInterval +select *; + +insert into DailyWeeklyRestCandidateInterval +select * +from DrivingInterruptionInterval(durationSeconds > ${MINIMUM_REST_PERIOD_THRESHOLD_SECONDS}); + +insert into DrivingInterruptionVehicleChangeInterval +select * +from DailyWeeklyRestCandidateInterval( + previousRegistrationKey is not null, + nextRegistrationKey is not null, + previousRegistrationKey != nextRegistrationKey +); + +insert into DrivingInterruptionVehicleNotChangedInterval +select * +from DailyWeeklyRestCandidateInterval( + previousRegistrationKey is not null, + nextRegistrationKey is not null, + previousRegistrationKey = nextRegistrationKey +); + +insert into DailyWeeklyRestCandidateCoverageUnknownResolvedInterval +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.durationSeconds as durationSeconds, + sum( + case + when u.startedAtEpochSecond <= c.startedAtEpochSecond and u.endedAtEpochSecond >= c.endedAtEpochSecond + then c.durationSeconds + when u.startedAtEpochSecond <= c.startedAtEpochSecond + then u.endedAtEpochSecond - c.startedAtEpochSecond + when u.endedAtEpochSecond >= c.endedAtEpochSecond + then c.endedAtEpochSecond - u.startedAtEpochSecond + else u.endedAtEpochSecond - u.startedAtEpochSecond + end + ) as cardAbsentDurationSeconds, + c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, + c.previousRegistrationKey as previousRegistrationKey, + c.nextRegistrationKey as nextRegistrationKey, + c.previousVehicleKey as previousVehicleKey, + c.nextVehicleKey as nextVehicleKey +from DailyWeeklyRestCandidateInterval as c unidirectional, + VuCardAbsentInterval#keepall as u +where u.driverKey = c.driverKey + and u.startedAtEpochSecond < c.endedAtEpochSecond + and u.endedAtEpochSecond > c.startedAtEpochSecond +group by + c.sessionId, + c.driverKey, + c.startedAtEpochSecond, + c.endedAtEpochSecond, + c.durationSeconds, + c.previousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId, + c.previousRegistrationKey, + c.nextRegistrationKey, + c.previousVehicleKey, + c.nextVehicleKey; + +insert into DailyWeeklyRestCandidateCoverageUnknownResolvedInterval +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.durationSeconds as durationSeconds, + 0L as cardAbsentDurationSeconds, + c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, + c.previousRegistrationKey as previousRegistrationKey, + c.nextRegistrationKey as nextRegistrationKey, + c.previousVehicleKey as previousVehicleKey, + c.nextVehicleKey as nextVehicleKey +from DailyWeeklyRestCandidateInterval as c +where not exists ( + select * from VuCardAbsentInterval#keepall as u + where u.driverKey = c.driverKey + and u.startedAtEpochSecond < c.endedAtEpochSecond + and u.endedAtEpochSecond > c.startedAtEpochSecond +); + +insert into DailyWeeklyRestCandidateCoverageCardResolvedInterval +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.durationSeconds as durationSeconds, + c.cardAbsentDurationSeconds as cardAbsentDurationSeconds, + c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, + c.previousRegistrationKey as previousRegistrationKey, + c.nextRegistrationKey as nextRegistrationKey, + c.previousVehicleKey as previousVehicleKey, + c.nextVehicleKey as nextVehicleKey +from DailyWeeklyRestCandidateCoverageUnknownResolvedInterval as c; + +insert into DailyWeeklyRestCandidateCoverageCardResolvedIntervalWindow +select * +from DailyWeeklyRestCandidateCoverageCardResolvedInterval; + +@Priority(50) +insert into DailyWeeklyRestCandidateBeginBoundaryOdometerCandidateWindow +select + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + v.odometerBeginKm as odometerKm, + case + when v.startedAtEpochSecond >= c.startedAtEpochSecond + then v.startedAtEpochSecond - c.startedAtEpochSecond + else c.startedAtEpochSecond - v.startedAtEpochSecond + end as distanceSeconds, + ( + ( + case + when v.startedAtEpochSecond >= c.startedAtEpochSecond + then v.startedAtEpochSecond - c.startedAtEpochSecond + else c.startedAtEpochSecond - v.startedAtEpochSecond + end + ) * 10000L + ) + ( + case + when v.startedAtEpochSecond >= c.startedAtEpochSecond + then 0L + else 1000L + end + ) as rankScore +from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional, + TachographVehicleUsageIntervalInputEvent#keepall as v +where v.driverKey = c.driverKey + and v.odometerBeginKm is not null + and v.startedAtEpochSecond >= c.startedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS} + and v.startedAtEpochSecond <= c.startedAtEpochSecond + ${REST_GEO_LOOKAHEAD_SECONDS} + and ( + ( + c.previousVehicleKey is not null + and v.vehicleKey is not null + and c.previousVehicleKey = v.vehicleKey + ) + or ( + (c.previousVehicleKey is null or v.vehicleKey is null) + and c.previousRegistrationKey is not null + and v.registrationKey is not null + and c.previousRegistrationKey = v.registrationKey + ) + ); + +@Priority(50) +insert into DailyWeeklyRestCandidateBeginBoundaryOdometerCandidateWindow +select + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + v.odometerEndKm as odometerKm, + case + when v.endedAtEpochSecond >= c.startedAtEpochSecond + then v.endedAtEpochSecond - c.startedAtEpochSecond + else c.startedAtEpochSecond - v.endedAtEpochSecond + end as distanceSeconds, + ( + ( + case + when v.endedAtEpochSecond >= c.startedAtEpochSecond + then v.endedAtEpochSecond - c.startedAtEpochSecond + else c.startedAtEpochSecond - v.endedAtEpochSecond + end + ) * 10000L + ) + ( + case + when v.endedAtEpochSecond >= c.startedAtEpochSecond + then 0L + else 1000L + end + ) as rankScore +from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional, + TachographVehicleUsageIntervalInputEvent#keepall as v +where v.driverKey = c.driverKey + and v.endedAtEpochSecond is not null + and v.odometerEndKm is not null + and v.endedAtEpochSecond >= c.startedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS} + and v.endedAtEpochSecond <= c.startedAtEpochSecond + ${REST_GEO_LOOKAHEAD_SECONDS} + and ( + ( + c.previousVehicleKey is not null + and v.vehicleKey is not null + and c.previousVehicleKey = v.vehicleKey + ) + or ( + (c.previousVehicleKey is null or v.vehicleKey is null) + and c.previousRegistrationKey is not null + and v.registrationKey is not null + and c.previousRegistrationKey = v.registrationKey + ) + ); + +@Priority(45) +insert into DailyWeeklyRestCandidateBeginBoundaryOdometerBestScoreWindow +select + driverKey, + startedAtEpochSecond, + endedAtEpochSecond, + min(rankScore) as bestRankScore +from DailyWeeklyRestCandidateBeginBoundaryOdometerCandidateWindow +group by + driverKey, + startedAtEpochSecond, + endedAtEpochSecond; + +@Priority(40) +insert into DailyWeeklyRestCandidateBeginBoundaryOdometerResolvedWindow +select + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.odometerKm as odometerKm, + c.distanceSeconds as distanceSeconds +from DailyWeeklyRestCandidateBeginBoundaryOdometerCandidateWindow as c, + DailyWeeklyRestCandidateBeginBoundaryOdometerBestScoreWindow as best +where c.driverKey = best.driverKey + and c.startedAtEpochSecond = best.startedAtEpochSecond + and c.endedAtEpochSecond = best.endedAtEpochSecond + and c.rankScore = best.bestRankScore; + +@Priority(50) +insert into DailyWeeklyRestCandidateEndBoundaryOdometerCandidateWindow +select + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + v.odometerBeginKm as odometerKm, + case + when v.startedAtEpochSecond >= c.endedAtEpochSecond + then v.startedAtEpochSecond - c.endedAtEpochSecond + else c.endedAtEpochSecond - v.startedAtEpochSecond + end as distanceSeconds, + ( + ( + case + when v.startedAtEpochSecond >= c.endedAtEpochSecond + then v.startedAtEpochSecond - c.endedAtEpochSecond + else c.endedAtEpochSecond - v.startedAtEpochSecond + end + ) * 10000L + ) + ( + case + when v.startedAtEpochSecond <= c.endedAtEpochSecond + then 0L + else 1000L + end + ) as rankScore +from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional, + TachographVehicleUsageIntervalInputEvent#keepall as v +where v.driverKey = c.driverKey + and v.odometerBeginKm is not null + and v.startedAtEpochSecond >= c.endedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS} + and v.startedAtEpochSecond <= c.endedAtEpochSecond + ${REST_GEO_LOOKAHEAD_SECONDS} + and ( + ( + c.nextVehicleKey is not null + and v.vehicleKey is not null + and c.nextVehicleKey = v.vehicleKey + ) + or ( + (c.nextVehicleKey is null or v.vehicleKey is null) + and c.nextRegistrationKey is not null + and v.registrationKey is not null + and c.nextRegistrationKey = v.registrationKey + ) + ); + +@Priority(50) +insert into DailyWeeklyRestCandidateEndBoundaryOdometerCandidateWindow +select + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + v.odometerEndKm as odometerKm, + case + when v.endedAtEpochSecond >= c.endedAtEpochSecond + then v.endedAtEpochSecond - c.endedAtEpochSecond + else c.endedAtEpochSecond - v.endedAtEpochSecond + end as distanceSeconds, + ( + ( + case + when v.endedAtEpochSecond >= c.endedAtEpochSecond + then v.endedAtEpochSecond - c.endedAtEpochSecond + else c.endedAtEpochSecond - v.endedAtEpochSecond + end + ) * 10000L + ) + ( + case + when v.endedAtEpochSecond <= c.endedAtEpochSecond + then 0L + else 1000L + end + ) as rankScore +from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional, + TachographVehicleUsageIntervalInputEvent#keepall as v +where v.driverKey = c.driverKey + and v.endedAtEpochSecond is not null + and v.odometerEndKm is not null + and v.endedAtEpochSecond >= c.endedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS} + and v.endedAtEpochSecond <= c.endedAtEpochSecond + ${REST_GEO_LOOKAHEAD_SECONDS} + and ( + ( + c.nextVehicleKey is not null + and v.vehicleKey is not null + and c.nextVehicleKey = v.vehicleKey + ) + or ( + (c.nextVehicleKey is null or v.vehicleKey is null) + and c.nextRegistrationKey is not null + and v.registrationKey is not null + and c.nextRegistrationKey = v.registrationKey + ) + ); + +@Priority(45) +insert into DailyWeeklyRestCandidateEndBoundaryOdometerBestScoreWindow +select + driverKey, + startedAtEpochSecond, + endedAtEpochSecond, + min(rankScore) as bestRankScore +from DailyWeeklyRestCandidateEndBoundaryOdometerCandidateWindow +group by + driverKey, + startedAtEpochSecond, + endedAtEpochSecond; + +@Priority(40) +insert into DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow +select + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.odometerKm as odometerKm, + c.distanceSeconds as distanceSeconds +from DailyWeeklyRestCandidateEndBoundaryOdometerCandidateWindow as c, + DailyWeeklyRestCandidateEndBoundaryOdometerBestScoreWindow as best +where c.driverKey = best.driverKey + and c.startedAtEpochSecond = best.startedAtEpochSecond + and c.endedAtEpochSecond = best.endedAtEpochSecond + and c.rankScore = best.bestRankScore; + +@Priority(40) +insert into DailyWeeklyRestCandidateBeginGeoEvidenceCandidateWindow +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + (select odometerKm from DailyWeeklyRestCandidateBeginBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond) as boundaryOdometerKm, + g.eventId as eventId, + g.eventDomain as eventDomain, + g.occurredAtEpochSecond as occurredAtEpochSecond, + g.latitude as latitude, + g.longitude as longitude, + g.odometerKm as odometerKm, + case + when ( + select odometerKm from DailyWeeklyRestCandidateBeginBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) is null + or g.odometerKm is null + then null + when g.odometerKm >= ( + select odometerKm from DailyWeeklyRestCandidateBeginBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) + then g.odometerKm - ( + select odometerKm from DailyWeeklyRestCandidateBeginBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) + else ( + select odometerKm from DailyWeeklyRestCandidateBeginBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) - g.odometerKm + end as odometerDeltaKm, + case + when g.occurredAtEpochSecond >= c.startedAtEpochSecond + then g.occurredAtEpochSecond - c.startedAtEpochSecond + else c.startedAtEpochSecond - g.occurredAtEpochSecond + end as distanceSeconds, + ( + ( + case + when g.occurredAtEpochSecond >= c.startedAtEpochSecond + then g.occurredAtEpochSecond - c.startedAtEpochSecond + else c.startedAtEpochSecond - g.occurredAtEpochSecond + end + ) * 10000L + ) + ( + case + when ( + select odometerKm from DailyWeeklyRestCandidateBeginBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) is not null + and g.odometerKm is not null + then ( + case + when g.odometerKm >= ( + select odometerKm from DailyWeeklyRestCandidateBeginBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) + then g.odometerKm - ( + select odometerKm from DailyWeeklyRestCandidateBeginBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) + else ( + select odometerKm from DailyWeeklyRestCandidateBeginBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) - g.odometerKm + end + ) * 10L + else 9990L + end + ) + ( + case + when g.occurredAtEpochSecond >= c.startedAtEpochSecond + then 0L + else 1000L + end + ) + (1000L - g.priority) as rankScore +from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional, + SupportGeoEvidenceWindow as g +where g.driverKey = c.driverKey + and g.occurredAtEpochSecond >= c.startedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS} + and g.occurredAtEpochSecond <= c.startedAtEpochSecond + ${REST_GEO_LOOKAHEAD_SECONDS} + and ( + ( + c.previousVehicleKey is not null + and g.vehicleKey is not null + and c.previousVehicleKey = g.vehicleKey + ) + or ( + (c.previousVehicleKey is null or g.vehicleKey is null) + and c.previousRegistrationKey is not null + and g.registrationKey is not null + and c.previousRegistrationKey = g.registrationKey + ) + ); + +@Priority(35) +insert into DailyWeeklyRestCandidateBeginGeoEvidenceBestScoreWindow +select + driverKey, + startedAtEpochSecond, + endedAtEpochSecond, + min(rankScore) as bestRankScore +from DailyWeeklyRestCandidateBeginGeoEvidenceCandidateWindow +group by + driverKey, + startedAtEpochSecond, + endedAtEpochSecond; + +@Priority(30) +insert into DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.eventId as eventId, + c.eventDomain as eventDomain, + c.occurredAtEpochSecond as occurredAtEpochSecond, + c.latitude as latitude, + c.longitude as longitude, + c.odometerKm as odometerKm, + c.distanceSeconds as distanceSeconds +from DailyWeeklyRestCandidateBeginGeoEvidenceCandidateWindow as c, + DailyWeeklyRestCandidateBeginGeoEvidenceBestScoreWindow as best +where c.driverKey = best.driverKey + and c.startedAtEpochSecond = best.startedAtEpochSecond + and c.endedAtEpochSecond = best.endedAtEpochSecond + and c.rankScore = best.bestRankScore; + +@Priority(40) +insert into DailyWeeklyRestCandidateEndGeoEvidenceCandidateWindow +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + (select odometerKm from DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond) as boundaryOdometerKm, + g.eventId as eventId, + g.eventDomain as eventDomain, + g.occurredAtEpochSecond as occurredAtEpochSecond, + g.latitude as latitude, + g.longitude as longitude, + g.odometerKm as odometerKm, + case + when ( + select odometerKm from DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) is null + or g.odometerKm is null + then null + when g.odometerKm >= ( + select odometerKm from DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) + then g.odometerKm - ( + select odometerKm from DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) + else ( + select odometerKm from DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) - g.odometerKm + end as odometerDeltaKm, + case + when g.occurredAtEpochSecond >= c.endedAtEpochSecond + then g.occurredAtEpochSecond - c.endedAtEpochSecond + else c.endedAtEpochSecond - g.occurredAtEpochSecond + end as distanceSeconds, + ( + ( + case + when g.occurredAtEpochSecond >= c.endedAtEpochSecond + then g.occurredAtEpochSecond - c.endedAtEpochSecond + else c.endedAtEpochSecond - g.occurredAtEpochSecond + end + ) * 10000L + ) + ( + case + when ( + select odometerKm from DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) is not null + and g.odometerKm is not null + then ( + case + when g.odometerKm >= ( + select odometerKm from DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) + then g.odometerKm - ( + select odometerKm from DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) + else ( + select odometerKm from DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow as boundary + where boundary.driverKey = c.driverKey + and boundary.startedAtEpochSecond = c.startedAtEpochSecond + and boundary.endedAtEpochSecond = c.endedAtEpochSecond + ) - g.odometerKm + end + ) * 10L + else 9990L + end + ) + ( + case + when g.occurredAtEpochSecond <= c.endedAtEpochSecond + then 0L + else 1000L + end + ) + (1000L - g.priority) as rankScore +from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c unidirectional, + SupportGeoEvidenceWindow as g +where g.driverKey = c.driverKey + and g.occurredAtEpochSecond >= c.endedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS} + and g.occurredAtEpochSecond <= c.endedAtEpochSecond + ${REST_GEO_LOOKAHEAD_SECONDS} + and ( + ( + c.nextVehicleKey is not null + and g.vehicleKey is not null + and c.nextVehicleKey = g.vehicleKey + ) + or ( + (c.nextVehicleKey is null or g.vehicleKey is null) + and c.nextRegistrationKey is not null + and g.registrationKey is not null + and c.nextRegistrationKey = g.registrationKey + ) + ); + +@Priority(35) +insert into DailyWeeklyRestCandidateEndGeoEvidenceBestScoreWindow +select + driverKey, + startedAtEpochSecond, + endedAtEpochSecond, + min(rankScore) as bestRankScore +from DailyWeeklyRestCandidateEndGeoEvidenceCandidateWindow +group by + driverKey, + startedAtEpochSecond, + endedAtEpochSecond; + +@Priority(30) +insert into DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.eventId as eventId, + c.eventDomain as eventDomain, + c.occurredAtEpochSecond as occurredAtEpochSecond, + c.latitude as latitude, + c.longitude as longitude, + c.odometerKm as odometerKm, + c.distanceSeconds as distanceSeconds +from DailyWeeklyRestCandidateEndGeoEvidenceCandidateWindow as c, + DailyWeeklyRestCandidateEndGeoEvidenceBestScoreWindow as best +where c.driverKey = best.driverKey + and c.startedAtEpochSecond = best.startedAtEpochSecond + and c.endedAtEpochSecond = best.endedAtEpochSecond + and c.rankScore = best.bestRankScore; + +@Priority(20) +insert into DailyWeeklyRestCandidateCoverageFinalizationRequest +select + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond +from DailyWeeklyRestCandidateCoverageCardResolvedInterval as c +where not exists ( + select * from SupportGeoEvidenceWindow as g + where g.driverKey = c.driverKey + and g.occurredAtEpochSecond >= c.startedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS} + and g.occurredAtEpochSecond <= c.startedAtEpochSecond + ${REST_GEO_LOOKAHEAD_SECONDS} + and ( + ( + c.previousVehicleKey is not null + and g.vehicleKey is not null + and c.previousVehicleKey = g.vehicleKey + ) + or ( + (c.previousVehicleKey is null or g.vehicleKey is null) + and c.previousRegistrationKey is not null + and g.registrationKey is not null + and c.previousRegistrationKey = g.registrationKey + ) + ) +) +and not exists ( + select * from SupportGeoEvidenceWindow as g + where g.driverKey = c.driverKey + and g.occurredAtEpochSecond >= c.endedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS} + and g.occurredAtEpochSecond <= c.endedAtEpochSecond + ${REST_GEO_LOOKAHEAD_SECONDS} + and ( + ( + c.nextVehicleKey is not null + and g.vehicleKey is not null + and c.nextVehicleKey = g.vehicleKey + ) + or ( + (c.nextVehicleKey is null or g.vehicleKey is null) + and c.nextRegistrationKey is not null + and g.registrationKey is not null + and c.nextRegistrationKey = g.registrationKey + ) + ) +); + +@Priority(20) +on DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo +insert into DailyWeeklyRestCandidateCoverageFinalizationRequest +select + beginGeo.driverKey as driverKey, + beginGeo.startedAtEpochSecond as startedAtEpochSecond, + beginGeo.endedAtEpochSecond as endedAtEpochSecond +from DailyWeeklyRestCandidateCoverageCardResolvedIntervalWindow as c +where c.driverKey = beginGeo.driverKey + and c.startedAtEpochSecond = beginGeo.startedAtEpochSecond + and c.endedAtEpochSecond = beginGeo.endedAtEpochSecond + and not exists ( + select * from SupportGeoEvidenceWindow as g + where g.driverKey = c.driverKey + and g.occurredAtEpochSecond >= c.endedAtEpochSecond - ${REST_GEO_LOOKBACK_SECONDS} + and g.occurredAtEpochSecond <= c.endedAtEpochSecond + ${REST_GEO_LOOKAHEAD_SECONDS} + and ( + ( + c.nextVehicleKey is not null + and g.vehicleKey is not null + and c.nextVehicleKey = g.vehicleKey + ) + or ( + (c.nextVehicleKey is null or g.vehicleKey is null) + and c.nextRegistrationKey is not null + and g.registrationKey is not null + and c.nextRegistrationKey = g.registrationKey + ) + ) + ); + +@Priority(20) +on DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo +insert into DailyWeeklyRestCandidateCoverageFinalizationRequest +select + endGeo.driverKey as driverKey, + endGeo.startedAtEpochSecond as startedAtEpochSecond, + endGeo.endedAtEpochSecond as endedAtEpochSecond +from DailyWeeklyRestCandidateCoverageCardResolvedIntervalWindow as c +where c.driverKey = endGeo.driverKey + and c.startedAtEpochSecond = endGeo.startedAtEpochSecond + and c.endedAtEpochSecond = endGeo.endedAtEpochSecond; + +@Priority(10) +insert into DailyWeeklyRestCandidateCoverageInterval +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.durationSeconds as durationSeconds, + c.cardAbsentDurationSeconds as cardAbsentDurationSeconds, + (c.cardAbsentDurationSeconds * 100.0d) / c.durationSeconds as cardAbsentCoveragePercent, + c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, + c.previousRegistrationKey as previousRegistrationKey, + c.nextRegistrationKey as nextRegistrationKey, + c.previousVehicleKey as previousVehicleKey, + c.nextVehicleKey as nextVehicleKey, + (select odometerKm from DailyWeeklyRestCandidateBeginBoundaryOdometerResolvedWindow as beginBoundary + where beginBoundary.driverKey = c.driverKey + and beginBoundary.startedAtEpochSecond = c.startedAtEpochSecond + and beginBoundary.endedAtEpochSecond = c.endedAtEpochSecond) as beginBoundaryOdometerKm, + (select odometerKm from DailyWeeklyRestCandidateEndBoundaryOdometerResolvedWindow as endBoundary + where endBoundary.driverKey = c.driverKey + and endBoundary.startedAtEpochSecond = c.startedAtEpochSecond + and endBoundary.endedAtEpochSecond = c.endedAtEpochSecond) as endBoundaryOdometerKm, + (select eventId from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) as beginGeoEventId, + (select eventDomain from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) as beginGeoEventDomain, + (select occurredAtEpochSecond from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) as beginGeoOccurredAtEpochSecond, + (select latitude from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) as beginLatitude, + (select longitude from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) as beginLongitude, + (select distanceSeconds from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) as beginGeoDistanceSeconds, + (select odometerKm from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) as beginGeoOdometerKm, + (select eventId from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) as endGeoEventId, + (select eventDomain from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) as endGeoEventDomain, + (select occurredAtEpochSecond from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) as endGeoOccurredAtEpochSecond, + (select latitude from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) as endLatitude, + (select longitude from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) as endLongitude, + (select distanceSeconds from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) as endGeoDistanceSeconds, + (select odometerKm from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) as endGeoOdometerKm, + case + when (select odometerKm from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) is null + or (select odometerKm from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) is null + then null + when (select odometerKm from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) + >= + (select odometerKm from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) + then ( + (select odometerKm from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) + - + (select odometerKm from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) + ) * 1000L + else ( + (select odometerKm from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) + - + (select odometerKm from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) + ) * 1000L + end as geoEvidenceMovementMeters, + case + when (select odometerKm from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) is null + or (select odometerKm from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) is null + then 'UNKNOWN' + when ( + case + when (select odometerKm from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) + >= + (select odometerKm from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) + then ( + (select odometerKm from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) + - + (select odometerKm from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) + ) * 1000L + else ( + (select odometerKm from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) + - + (select odometerKm from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) + ) * 1000L + end + ) <= ${REST_GEO_STATIONARY_MAX_METERS} + then 'STATIONARY' + when ( + case + when (select odometerKm from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) + >= + (select odometerKm from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) + then ( + (select odometerKm from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) + - + (select odometerKm from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) + ) * 1000L + else ( + (select odometerKm from DailyWeeklyRestCandidateBeginGeoEvidenceResolvedWindow as beginGeo + where beginGeo.driverKey = c.driverKey + and beginGeo.startedAtEpochSecond = c.startedAtEpochSecond + and beginGeo.endedAtEpochSecond = c.endedAtEpochSecond) + - + (select odometerKm from DailyWeeklyRestCandidateEndGeoEvidenceResolvedWindow as endGeo + where endGeo.driverKey = c.driverKey + and endGeo.startedAtEpochSecond = c.startedAtEpochSecond + and endGeo.endedAtEpochSecond = c.endedAtEpochSecond) + ) * 1000L + end + ) <= ${REST_GEO_MINOR_MOVEMENT_MAX_METERS} + then 'MINOR' + else 'MOVED' + end as geoEvidenceMovementCategory +from DailyWeeklyRestCandidateCoverageFinalizationRequest as request unidirectional, + DailyWeeklyRestCandidateCoverageCardResolvedIntervalWindow as c +where c.driverKey = request.driverKey + and c.startedAtEpochSecond = request.startedAtEpochSecond + and c.endedAtEpochSecond = request.endedAtEpochSecond + and not exists ( + select * from DailyWeeklyRestCandidateCoverageEmittedKeyWindow as emitted + where emitted.driverKey = request.driverKey + and emitted.startedAtEpochSecond = request.startedAtEpochSecond + and emitted.endedAtEpochSecond = request.endedAtEpochSecond + ); + +insert into DailyWeeklyRestCandidateCoverageEmittedKeyWindow +select + driverKey, + startedAtEpochSecond, + endedAtEpochSecond +from DailyWeeklyRestCandidateCoverageInterval; + +context PerDriver +create window PreviousVehicleUsageInterval#lastevent as TachographVehicleUsageIntervalInputEvent; + +@Priority(30) +context PerDriver +on TachographVehicleUsageIntervalInputEvent as next +insert into VuCardAbsentInterval +select + priorInterval.sessionId as sessionId, + priorInterval.driverKey as driverKey, + priorInterval.endedAtEpochSecond + 1L as startedAtEpochSecond, + next.startedAtEpochSecond as endedAtEpochSecond, + next.startedAtEpochSecond - (priorInterval.endedAtEpochSecond + 1L) as durationSeconds, + priorInterval.lastSourceIntervalId as previousUsageIntervalId, + next.firstSourceIntervalId as nextUsageIntervalId, + priorInterval.registrationKey as previousRegistrationKey, + next.registrationKey as nextRegistrationKey, + priorInterval.vehicleKey as previousVehicleKey, + next.vehicleKey as nextVehicleKey +from PreviousVehicleUsageInterval as priorInterval +where priorInterval.endedAt is not null + and next.startedAt is not null + and next.startedAtEpochSecond > priorInterval.endedAtEpochSecond + 1L; + +@Priority(20) +context PerDriver +on TachographVehicleUsageIntervalInputEvent +delete from PreviousVehicleUsageInterval; + +@Priority(10) +context PerDriver +on TachographVehicleUsageIntervalInputEvent as current +insert into PreviousVehicleUsageInterval +select *; + +insert into PotentialHomeOvernightStayInterval +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.durationSeconds as durationSeconds, + c.cardAbsentDurationSeconds as cardAbsentDurationSeconds, + c.cardAbsentCoveragePercent as cardAbsentCoveragePercent, + c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, + c.previousRegistrationKey as previousRegistrationKey, + c.nextRegistrationKey as nextRegistrationKey, + c.previousVehicleKey as previousVehicleKey, + c.nextVehicleKey as nextVehicleKey, + c.beginBoundaryOdometerKm as beginBoundaryOdometerKm, + c.endBoundaryOdometerKm as endBoundaryOdometerKm, + c.beginGeoEventId as beginGeoEventId, + c.beginGeoEventDomain as beginGeoEventDomain, + c.beginGeoOccurredAtEpochSecond as beginGeoOccurredAtEpochSecond, + c.beginLatitude as beginLatitude, + c.beginLongitude as beginLongitude, + c.beginGeoDistanceSeconds as beginGeoDistanceSeconds, + c.beginGeoOdometerKm as beginGeoOdometerKm, + c.endGeoEventId as endGeoEventId, + c.endGeoEventDomain as endGeoEventDomain, + c.endGeoOccurredAtEpochSecond as endGeoOccurredAtEpochSecond, + c.endLatitude as endLatitude, + c.endLongitude as endLongitude, + c.endGeoDistanceSeconds as endGeoDistanceSeconds, + c.endGeoOdometerKm as endGeoOdometerKm, + c.geoEvidenceMovementMeters as geoEvidenceMovementMeters, + c.geoEvidenceMovementCategory as geoEvidenceMovementCategory +from DailyWeeklyRestCandidateCoverageInterval as c +where c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey != c.nextRegistrationKey + and c.cardAbsentDurationSeconds * 100L >= c.durationSeconds * 95L; + +insert into PotentialInVehicleOvernightStayInterval +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.durationSeconds as durationSeconds, + c.cardAbsentDurationSeconds as cardAbsentDurationSeconds, + c.cardAbsentCoveragePercent as cardAbsentCoveragePercent, + c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, + c.previousRegistrationKey as previousRegistrationKey, + c.nextRegistrationKey as nextRegistrationKey, + c.previousVehicleKey as previousVehicleKey, + c.nextVehicleKey as nextVehicleKey, + c.beginBoundaryOdometerKm as beginBoundaryOdometerKm, + c.endBoundaryOdometerKm as endBoundaryOdometerKm, + c.beginGeoEventId as beginGeoEventId, + c.beginGeoEventDomain as beginGeoEventDomain, + c.beginGeoOccurredAtEpochSecond as beginGeoOccurredAtEpochSecond, + c.beginLatitude as beginLatitude, + c.beginLongitude as beginLongitude, + c.beginGeoDistanceSeconds as beginGeoDistanceSeconds, + c.beginGeoOdometerKm as beginGeoOdometerKm, + c.endGeoEventId as endGeoEventId, + c.endGeoEventDomain as endGeoEventDomain, + c.endGeoOccurredAtEpochSecond as endGeoOccurredAtEpochSecond, + c.endLatitude as endLatitude, + c.endLongitude as endLongitude, + c.endGeoDistanceSeconds as endGeoDistanceSeconds, + c.endGeoOdometerKm as endGeoOdometerKm, + c.geoEvidenceMovementMeters as geoEvidenceMovementMeters, + c.geoEvidenceMovementCategory as geoEvidenceMovementCategory +from DailyWeeklyRestCandidateCoverageInterval as c +where c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey = c.nextRegistrationKey + and c.cardAbsentDurationSeconds = 0L; + +insert into UnclassifiedDailyWeeklyRestCandidateCoverageInterval +select + c.sessionId as sessionId, + c.driverKey as driverKey, + c.startedAtEpochSecond as startedAtEpochSecond, + c.endedAtEpochSecond as endedAtEpochSecond, + c.durationSeconds as durationSeconds, + c.cardAbsentDurationSeconds as cardAbsentDurationSeconds, + c.cardAbsentCoveragePercent as cardAbsentCoveragePercent, + c.previousDrivingSourceIntervalId as previousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId as nextDrivingSourceIntervalId, + c.previousRegistrationKey as previousRegistrationKey, + c.nextRegistrationKey as nextRegistrationKey, + c.previousVehicleKey as previousVehicleKey, + c.nextVehicleKey as nextVehicleKey, + c.beginBoundaryOdometerKm as beginBoundaryOdometerKm, + c.endBoundaryOdometerKm as endBoundaryOdometerKm, + c.beginGeoEventId as beginGeoEventId, + c.beginGeoEventDomain as beginGeoEventDomain, + c.beginGeoOccurredAtEpochSecond as beginGeoOccurredAtEpochSecond, + c.beginLatitude as beginLatitude, + c.beginLongitude as beginLongitude, + c.beginGeoDistanceSeconds as beginGeoDistanceSeconds, + c.beginGeoOdometerKm as beginGeoOdometerKm, + c.endGeoEventId as endGeoEventId, + c.endGeoEventDomain as endGeoEventDomain, + c.endGeoOccurredAtEpochSecond as endGeoOccurredAtEpochSecond, + c.endLatitude as endLatitude, + c.endLongitude as endLongitude, + c.endGeoDistanceSeconds as endGeoDistanceSeconds, + c.endGeoOdometerKm as endGeoOdometerKm, + c.geoEvidenceMovementMeters as geoEvidenceMovementMeters, + c.geoEvidenceMovementCategory as geoEvidenceMovementCategory +from DailyWeeklyRestCandidateCoverageInterval as c +where not ( + c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey != c.nextRegistrationKey + and c.cardAbsentDurationSeconds * 100L >= c.durationSeconds * 95L +) + and not ( + c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey = c.nextRegistrationKey + and c.cardAbsentDurationSeconds = 0L +); + +@Priority(40) +on DailyWeeklyRestCandidateCoverageInterval as c +insert into PotentialInVehicleTripInterval +select + s.sessionId as sessionId, + s.driverKey as driverKey, + s.tripStartedAtEpochSecond as startedAtEpochSecond, + c.startedAtEpochSecond as endedAtEpochSecond, + c.startedAtEpochSecond - s.tripStartedAtEpochSecond as durationSeconds, + s.registrationKey as registrationKey, + s.vehicleKey as vehicleKey, + s.containedPotentialInVehicleOvernightStayIntervalCount as containedPotentialInVehicleOvernightStayIntervalCount, + s.containedPotentialInVehicleOvernightStayDurationSeconds as containedPotentialInVehicleOvernightStayDurationSeconds, + s.containedCardAbsentDurationSeconds as containedCardAbsentDurationSeconds, + s.firstPotentialInVehicleOvernightStayStartedAtEpochSecond as firstPotentialInVehicleOvernightStayStartedAtEpochSecond, + s.lastPotentialInVehicleOvernightStayEndedAtEpochSecond as lastPotentialInVehicleOvernightStayEndedAtEpochSecond, + s.firstPreviousDrivingSourceIntervalId as firstPreviousDrivingSourceIntervalId, + s.lastNextDrivingSourceIntervalId as lastNextDrivingSourceIntervalId +from OpenPotentialInVehicleTripState as s +where s.driverKey = c.driverKey + and c.startedAtEpochSecond > s.tripStartedAtEpochSecond + and ( + not ( + c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey = c.nextRegistrationKey + and c.cardAbsentDurationSeconds = 0L + ) + or s.registrationKey != c.previousRegistrationKey + or ( + s.vehicleKey is not null + and c.previousVehicleKey is not null + and s.vehicleKey != c.previousVehicleKey + ) + ); + +@Priority(35) +on DailyWeeklyRestCandidateCoverageInterval as c +delete from OpenPotentialInVehicleTripState as s +where s.driverKey = c.driverKey + and not ( + c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey = c.nextRegistrationKey + and c.cardAbsentDurationSeconds = 0L + ); + +@Priority(30) +on DailyWeeklyRestCandidateCoverageInterval as c +insert into OpenPotentialInVehicleTripState +select + s.sessionId as sessionId, + s.driverKey as driverKey, + s.tripStartedAtEpochSecond as tripStartedAtEpochSecond, + s.registrationKey as registrationKey, + s.vehicleKey as vehicleKey, + s.containedPotentialInVehicleOvernightStayIntervalCount + 1 as containedPotentialInVehicleOvernightStayIntervalCount, + s.containedPotentialInVehicleOvernightStayDurationSeconds + c.durationSeconds as containedPotentialInVehicleOvernightStayDurationSeconds, + s.containedCardAbsentDurationSeconds + c.cardAbsentDurationSeconds as containedCardAbsentDurationSeconds, + s.firstPotentialInVehicleOvernightStayStartedAtEpochSecond as firstPotentialInVehicleOvernightStayStartedAtEpochSecond, + c.endedAtEpochSecond as lastPotentialInVehicleOvernightStayEndedAtEpochSecond, + s.firstPreviousDrivingSourceIntervalId as firstPreviousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId as lastNextDrivingSourceIntervalId +from OpenPotentialInVehicleTripState as s +where s.driverKey = c.driverKey + and c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey = c.nextRegistrationKey + and c.cardAbsentDurationSeconds = 0L + and s.registrationKey = c.previousRegistrationKey + and ( + s.vehicleKey is null + or c.previousVehicleKey is null + or s.vehicleKey = c.previousVehicleKey + ); + +@Priority(20) +on DailyWeeklyRestCandidateCoverageInterval as c +insert into OpenPotentialInVehicleTripState +select + c.sessionId as sessionId, + c.driverKey as driverKey, + priorCoverage.endedAtEpochSecond as tripStartedAtEpochSecond, + c.previousRegistrationKey as registrationKey, + case + when c.previousVehicleKey is not null then c.previousVehicleKey + else c.nextVehicleKey + end as vehicleKey, + 1 as containedPotentialInVehicleOvernightStayIntervalCount, + c.durationSeconds as containedPotentialInVehicleOvernightStayDurationSeconds, + c.cardAbsentDurationSeconds as containedCardAbsentDurationSeconds, + c.startedAtEpochSecond as firstPotentialInVehicleOvernightStayStartedAtEpochSecond, + c.endedAtEpochSecond as lastPotentialInVehicleOvernightStayEndedAtEpochSecond, + c.previousDrivingSourceIntervalId as firstPreviousDrivingSourceIntervalId, + c.nextDrivingSourceIntervalId as lastNextDrivingSourceIntervalId +from PreviousRestCandidateCoverageInterval as priorCoverage +where priorCoverage.driverKey = c.driverKey + and c.previousRegistrationKey is not null + and c.nextRegistrationKey is not null + and c.previousRegistrationKey = c.nextRegistrationKey + and c.cardAbsentDurationSeconds = 0L + and not exists ( + select * from OpenPotentialInVehicleTripState as s + where s.driverKey = c.driverKey + and s.registrationKey = c.previousRegistrationKey + and ( + s.vehicleKey is null + or c.previousVehicleKey is null + or s.vehicleKey = c.previousVehicleKey + ) + ); + +@Priority(10) +on DailyWeeklyRestCandidateCoverageInterval +delete from PreviousRestCandidateCoverageInterval; + +@Priority(5) +on DailyWeeklyRestCandidateCoverageInterval as current +insert into PreviousRestCandidateCoverageInterval +select *; + +@name('drivingInterruptionIntervals') +select * from DrivingInterruptionInterval; + +@name('dailyWeeklyRestCandidateIntervals') +select * from DailyWeeklyRestCandidateInterval; + +@name('dailyWeeklyRestCandidateCoverageIntervals') +select * from DailyWeeklyRestCandidateCoverageInterval; + +@name('drivingInterruptionVehicleChangeIntervals') +select * from DrivingInterruptionVehicleChangeInterval; + +@name('vuCardAbsentIntervals') +select * from VuCardAbsentInterval; + +@name('potentialHomeOvernightStayIntervals') +select * from PotentialHomeOvernightStayInterval; + +@name('potentialInVehicleOvernightStayIntervals') +select * from PotentialInVehicleOvernightStayInterval; + +@name('potentialInVehicleTripIntervals') +select * from PotentialInVehicleTripInterval; + +@name('unclassifiedDailyWeeklyRestCandidateCoverageIntervals') +select * from UnclassifiedDailyWeeklyRestCandidateCoverageInterval; diff --git a/src/main/resources/esper/runtime-driver-activity-intervals.epl b/src/main/resources/esper/runtime-driver-activity-intervals.epl new file mode 100644 index 0000000..b41eae8 --- /dev/null +++ b/src/main/resources/esper/runtime-driver-activity-intervals.epl @@ -0,0 +1,74 @@ +/* + * Source-neutral EPL module: DriverActivityPointEvent -> DriverActivityIntervalEvent. + * This module is intended for Runtime Processing plans and is independent of any concrete source provider. + */ + +create schema DriverActivityIntervalEvent( + sessionId java.util.UUID, + driverKey string, + intervalId string, + activityType string, + cardSlot string, + cardStatus string, + drivingStatus string, + registrationKey string, + vehicleKey string, + sourceKind string, + firstSourceIntervalId string, + lastSourceIntervalId string, + startedAt java.time.OffsetDateTime, + endedAt java.time.OffsetDateTime, + startedAtEpochSecond long, + endedAtEpochSecond long, + durationSeconds long, + sourceIntervalIds java.util.List, + synthetic boolean, + clippedToRequestedPeriod boolean, + level string +); + +create window OpenDriverActivityPoint#unique(driverKey, intervalId) as DriverActivityPointEvent; + +insert into OpenDriverActivityPoint +select * +from DriverActivityPointEvent(lifecycle = 'START'); + +@Priority(20) +on DriverActivityPointEvent(lifecycle = 'END') as endEvent +insert into DriverActivityIntervalEvent +select + startEvent.sessionId as sessionId, + startEvent.driverKey as driverKey, + startEvent.intervalId as intervalId, + startEvent.activityType as activityType, + startEvent.cardSlot as cardSlot, + startEvent.cardStatus as cardStatus, + startEvent.drivingStatus as drivingStatus, + startEvent.registrationKey as registrationKey, + startEvent.vehicleKey as vehicleKey, + startEvent.sourceKind as sourceKind, + startEvent.sourceRowId as firstSourceIntervalId, + endEvent.sourceRowId as lastSourceIntervalId, + startEvent.occurredAt as startedAt, + endEvent.occurredAt as endedAt, + startEvent.occurredAtEpochSecond as startedAtEpochSecond, + endEvent.occurredAtEpochSecond as endedAtEpochSecond, + endEvent.occurredAtEpochSecond - startEvent.occurredAtEpochSecond as durationSeconds, + startEvent.sourceRowIds as sourceIntervalIds, + startEvent.synthetic as synthetic, + startEvent.clippedToRequestedPeriod as clippedToRequestedPeriod, + startEvent.level as level +from OpenDriverActivityPoint as startEvent +where startEvent.driverKey = endEvent.driverKey + and startEvent.intervalId = endEvent.intervalId + and endEvent.occurredAtEpochSecond > startEvent.occurredAtEpochSecond; + +@Priority(10) +on DriverActivityPointEvent(lifecycle = 'END') as endEvent +delete from OpenDriverActivityPoint as openEvent +where openEvent.driverKey = endEvent.driverKey + and openEvent.intervalId = endEvent.intervalId; + +@name('driverActivityIntervals') +select * +from DriverActivityIntervalEvent; diff --git a/src/main/resources/esper/runtime-driver-event-interval-preprocessor.epl b/src/main/resources/esper/runtime-driver-event-interval-preprocessor.epl new file mode 100644 index 0000000..cb11eea --- /dev/null +++ b/src/main/resources/esper/runtime-driver-event-interval-preprocessor.epl @@ -0,0 +1,348 @@ +/* + * Source-neutral event-input adapter for driver-working-time-derived-projections.epl. + * + * The old bundle consumes resolved interval input streams. This preprocessor lets the same + * derived rules consume EventHub point events by pairing START/END activity events and + * INSERT/WITHDRAW card-vehicle usage events inside Esper. + * + * Vehicle-usage intervals are additionally coalesced in EPL before they are forwarded to + * the old interval bundle. This keeps event mode equivalent to DriverTimelineBuilder's + * Java merge behavior for consecutive same-vehicle CardVehiclesUsed rows, including the + * common midnight continuation 23:59:59 -> 00:00:00 on the next day. + */ + +create window OpenActivityPoint#unique(driverKey, intervalId) as TachographActivityPointInputEvent; + +insert into OpenActivityPoint +select * +from TachographActivityPointInputEvent(lifecycle = 'START'); + +@Priority(20) +on TachographActivityPointInputEvent(lifecycle = 'END') as endEvent +insert into TachographActivityIntervalInputEvent +select + startEvent.sessionId as sessionId, + startEvent.driverKey as driverKey, + startEvent.intervalId as intervalId, + startEvent.activityType as activityType, + startEvent.cardSlot as cardSlot, + startEvent.cardStatus as cardStatus, + startEvent.drivingStatus as drivingStatus, + startEvent.registrationKey as registrationKey, + startEvent.vehicleKey as vehicleKey, + startEvent.sourceKind as sourceKind, + startEvent.sourceRowId as firstSourceIntervalId, + endEvent.sourceRowId as lastSourceIntervalId, + startEvent.occurredAt as startedAt, + endEvent.occurredAt as endedAt, + startEvent.occurredAtEpochSecond as startedAtEpochSecond, + endEvent.occurredAtEpochSecond as endedAtEpochSecond, + endEvent.occurredAtEpochSecond - startEvent.occurredAtEpochSecond as durationSeconds, + startEvent.sourceRowIds as sourceIntervalIds, + startEvent.synthetic as synthetic, + startEvent.clippedToRequestedPeriod as clippedToRequestedPeriod, + startEvent.level as level +from OpenActivityPoint as startEvent +where startEvent.driverKey = endEvent.driverKey + and startEvent.intervalId = endEvent.intervalId + and endEvent.occurredAtEpochSecond > startEvent.occurredAtEpochSecond; + +@Priority(10) +on TachographActivityPointInputEvent(lifecycle = 'END') as endEvent +delete from OpenActivityPoint as openEvent +where openEvent.driverKey = endEvent.driverKey + and openEvent.intervalId = endEvent.intervalId; + +create schema RawVehicleUsageInterval( + sessionId java.util.UUID, + driverKey string, + intervalId string, + firstSourceIntervalId string, + lastSourceIntervalId string, + startedAt java.time.OffsetDateTime, + endedAt java.time.OffsetDateTime, + startedAtEpochSecond long, + endedAtEpochSecond java.lang.Long, + durationSeconds long, + odometerBeginKm java.lang.Long, + odometerEndKm java.lang.Long, + registrationKey string, + vehicleKey string, + sourceKind string, + sourceIntervalIds java.util.List +); + +create window OpenVehicleUsagePoint#unique(driverKey, intervalId) as TachographVehicleUsagePointInputEvent; + +insert into OpenVehicleUsagePoint +select * +from TachographVehicleUsagePointInputEvent(lifecycle = 'INSERT'); + +@Priority(20) +on TachographVehicleUsagePointInputEvent(lifecycle = 'WITHDRAW') as withdrawEvent +insert into RawVehicleUsageInterval +select + insertEvent.sessionId as sessionId, + insertEvent.driverKey as driverKey, + insertEvent.intervalId as intervalId, + insertEvent.sourceRowId as firstSourceIntervalId, + withdrawEvent.sourceRowId as lastSourceIntervalId, + insertEvent.occurredAt as startedAt, + withdrawEvent.occurredAt as endedAt, + insertEvent.occurredAtEpochSecond as startedAtEpochSecond, + withdrawEvent.occurredAtEpochSecond as endedAtEpochSecond, + withdrawEvent.occurredAtEpochSecond - insertEvent.occurredAtEpochSecond as durationSeconds, + insertEvent.odometerKm as odometerBeginKm, + withdrawEvent.odometerKm as odometerEndKm, + insertEvent.registrationKey as registrationKey, + insertEvent.vehicleKey as vehicleKey, + insertEvent.sourceKind as sourceKind, + insertEvent.sourceRowIds as sourceIntervalIds +from OpenVehicleUsagePoint as insertEvent +where insertEvent.driverKey = withdrawEvent.driverKey + and insertEvent.intervalId = withdrawEvent.intervalId + and withdrawEvent.occurredAtEpochSecond > insertEvent.occurredAtEpochSecond; + +@Priority(10) +on TachographVehicleUsagePointInputEvent(lifecycle = 'WITHDRAW') as withdrawEvent +delete from OpenVehicleUsagePoint as openEvent +where openEvent.driverKey = withdrawEvent.driverKey + and openEvent.intervalId = withdrawEvent.intervalId; + +create window MergedVehicleUsageAccumulator#unique(driverKey) as RawVehicleUsageInterval; +create window MergedVehicleUsageNext#unique(driverKey) as RawVehicleUsageInterval; + +/* + * Case 1: first vehicle-usage interval for this driver. Start a new accumulator. + */ +@Priority(70) +on RawVehicleUsageInterval as next +insert into MergedVehicleUsageNext +select + next.sessionId as sessionId, + next.driverKey as driverKey, + next.intervalId as intervalId, + next.firstSourceIntervalId as firstSourceIntervalId, + next.lastSourceIntervalId as lastSourceIntervalId, + next.startedAt as startedAt, + next.endedAt as endedAt, + next.startedAtEpochSecond as startedAtEpochSecond, + next.endedAtEpochSecond as endedAtEpochSecond, + next.durationSeconds as durationSeconds, + next.odometerBeginKm as odometerBeginKm, + next.odometerEndKm as odometerEndKm, + next.registrationKey as registrationKey, + next.vehicleKey as vehicleKey, + next.sourceKind as sourceKind, + next.sourceIntervalIds as sourceIntervalIds +where not exists ( + select * from MergedVehicleUsageAccumulator as current + where current.driverKey = next.driverKey +); + +/* + * Case 2: same vehicle and registration, and the next interval starts at or before + * current.to + 1 second. This includes 23:59:59 -> 00:00:00. Keep one accumulated + * interval and extend its end/last-source/ending odometer. + */ +@Priority(70) +on RawVehicleUsageInterval as next +insert into MergedVehicleUsageNext +select + current.sessionId as sessionId, + current.driverKey as driverKey, + current.intervalId as intervalId, + current.firstSourceIntervalId as firstSourceIntervalId, + next.lastSourceIntervalId as lastSourceIntervalId, + current.startedAt as startedAt, + next.endedAt as endedAt, + current.startedAtEpochSecond as startedAtEpochSecond, + next.endedAtEpochSecond as endedAtEpochSecond, + next.endedAtEpochSecond - current.startedAtEpochSecond as durationSeconds, + current.odometerBeginKm as odometerBeginKm, + next.odometerEndKm as odometerEndKm, + current.registrationKey as registrationKey, + current.vehicleKey as vehicleKey, + current.sourceKind as sourceKind, + current.sourceIntervalIds as sourceIntervalIds +from MergedVehicleUsageAccumulator as current +where current.driverKey = next.driverKey + and current.endedAtEpochSecond is not null + and next.startedAtEpochSecond <= current.endedAtEpochSecond + 1L + and ( + (current.registrationKey is null and next.registrationKey is null) + or ( + current.registrationKey is not null + and next.registrationKey is not null + and current.registrationKey = next.registrationKey + ) + ) + and ( + (current.vehicleKey is null and next.vehicleKey is null) + or ( + current.vehicleKey is not null + and next.vehicleKey is not null + and current.vehicleKey = next.vehicleKey + ) + ); + +/* + * Case 3a: next interval is not mergeable. Emit the accumulated interval to the old + * interval-based bundle. + */ +@Priority(70) +on RawVehicleUsageInterval as next +insert into TachographVehicleUsageIntervalInputEvent +select + current.sessionId as sessionId, + current.driverKey as driverKey, + current.intervalId as intervalId, + current.firstSourceIntervalId as firstSourceIntervalId, + current.lastSourceIntervalId as lastSourceIntervalId, + current.startedAt as startedAt, + current.endedAt as endedAt, + current.startedAtEpochSecond as startedAtEpochSecond, + current.endedAtEpochSecond as endedAtEpochSecond, + current.durationSeconds as durationSeconds, + current.odometerBeginKm as odometerBeginKm, + current.odometerEndKm as odometerEndKm, + current.registrationKey as registrationKey, + current.vehicleKey as vehicleKey, + current.sourceKind as sourceKind, + current.sourceIntervalIds as sourceIntervalIds +from MergedVehicleUsageAccumulator as current +where current.driverKey = next.driverKey + and not ( + current.endedAtEpochSecond is not null + and next.startedAtEpochSecond <= current.endedAtEpochSecond + 1L + and ( + (current.registrationKey is null and next.registrationKey is null) + or ( + current.registrationKey is not null + and next.registrationKey is not null + and current.registrationKey = next.registrationKey + ) + ) + and ( + (current.vehicleKey is null and next.vehicleKey is null) + or ( + current.vehicleKey is not null + and next.vehicleKey is not null + and current.vehicleKey = next.vehicleKey + ) + ) + ); + +/* + * Case 3b: next interval is not mergeable. Start a new accumulator with it. + */ +@Priority(70) +on RawVehicleUsageInterval as next +insert into MergedVehicleUsageNext +select + next.sessionId as sessionId, + next.driverKey as driverKey, + next.intervalId as intervalId, + next.firstSourceIntervalId as firstSourceIntervalId, + next.lastSourceIntervalId as lastSourceIntervalId, + next.startedAt as startedAt, + next.endedAt as endedAt, + next.startedAtEpochSecond as startedAtEpochSecond, + next.endedAtEpochSecond as endedAtEpochSecond, + next.durationSeconds as durationSeconds, + next.odometerBeginKm as odometerBeginKm, + next.odometerEndKm as odometerEndKm, + next.registrationKey as registrationKey, + next.vehicleKey as vehicleKey, + next.sourceKind as sourceKind, + next.sourceIntervalIds as sourceIntervalIds +where exists ( + select * from MergedVehicleUsageAccumulator as current + where current.driverKey = next.driverKey + and not ( + current.endedAtEpochSecond is not null + and next.startedAtEpochSecond <= current.endedAtEpochSecond + 1L + and ( + (current.registrationKey is null and next.registrationKey is null) + or ( + current.registrationKey is not null + and next.registrationKey is not null + and current.registrationKey = next.registrationKey + ) + ) + and ( + (current.vehicleKey is null and next.vehicleKey is null) + or ( + current.vehicleKey is not null + and next.vehicleKey is not null + and current.vehicleKey = next.vehicleKey + ) + ) + ) +); + +@Priority(60) +on RawVehicleUsageInterval as next +delete from MergedVehicleUsageAccumulator as current +where current.driverKey = next.driverKey; + +@Priority(50) +on RawVehicleUsageInterval as next +insert into MergedVehicleUsageAccumulator +select + candidate.sessionId as sessionId, + candidate.driverKey as driverKey, + candidate.intervalId as intervalId, + candidate.firstSourceIntervalId as firstSourceIntervalId, + candidate.lastSourceIntervalId as lastSourceIntervalId, + candidate.startedAt as startedAt, + candidate.endedAt as endedAt, + candidate.startedAtEpochSecond as startedAtEpochSecond, + candidate.endedAtEpochSecond as endedAtEpochSecond, + candidate.durationSeconds as durationSeconds, + candidate.odometerBeginKm as odometerBeginKm, + candidate.odometerEndKm as odometerEndKm, + candidate.registrationKey as registrationKey, + candidate.vehicleKey as vehicleKey, + candidate.sourceKind as sourceKind, + candidate.sourceIntervalIds as sourceIntervalIds +from MergedVehicleUsageNext as candidate +where candidate.driverKey = next.driverKey; + +@Priority(40) +on RawVehicleUsageInterval as next +delete from MergedVehicleUsageNext as candidate +where candidate.driverKey = next.driverKey; + +/* + * The last accumulated interval cannot be emitted by looking at the next interval, so Java + * sends one TachographProjectionFinalizeEvent per driver after all vehicle-usage point + * events and before activity point events. + */ +@Priority(20) +on TachographProjectionFinalizeEvent as finalizeEvent +insert into TachographVehicleUsageIntervalInputEvent +select + current.sessionId as sessionId, + current.driverKey as driverKey, + current.intervalId as intervalId, + current.firstSourceIntervalId as firstSourceIntervalId, + current.lastSourceIntervalId as lastSourceIntervalId, + current.startedAt as startedAt, + current.endedAt as endedAt, + current.startedAtEpochSecond as startedAtEpochSecond, + current.endedAtEpochSecond as endedAtEpochSecond, + current.durationSeconds as durationSeconds, + current.odometerBeginKm as odometerBeginKm, + current.odometerEndKm as odometerEndKm, + current.registrationKey as registrationKey, + current.vehicleKey as vehicleKey, + current.sourceKind as sourceKind, + current.sourceIntervalIds as sourceIntervalIds +from MergedVehicleUsageAccumulator as current +where current.driverKey = finalizeEvent.driverKey; + +@Priority(10) +on TachographProjectionFinalizeEvent as finalizeEvent +delete from MergedVehicleUsageAccumulator as current +where current.driverKey = finalizeEvent.driverKey; diff --git a/src/main/resources/esper/runtime-driver-vehicle-usage-intervals.epl b/src/main/resources/esper/runtime-driver-vehicle-usage-intervals.epl new file mode 100644 index 0000000..19d105a --- /dev/null +++ b/src/main/resources/esper/runtime-driver-vehicle-usage-intervals.epl @@ -0,0 +1,64 @@ +/* + * Source-neutral EPL module: DriverVehicleUsagePointEvent -> DriverVehicleUsageIntervalEvent. + * Pairs INSERT/WITHDRAW point events for the same driver and interval id. + */ + +create schema DriverVehicleUsageIntervalEvent( + sessionId java.util.UUID, + driverKey string, + intervalId string, + firstSourceIntervalId string, + lastSourceIntervalId string, + startedAt java.time.OffsetDateTime, + endedAt java.time.OffsetDateTime, + startedAtEpochSecond long, + endedAtEpochSecond java.lang.Long, + durationSeconds long, + odometerBeginKm java.lang.Long, + odometerEndKm java.lang.Long, + registrationKey string, + vehicleKey string, + sourceKind string, + sourceIntervalIds java.util.List +); + +create window OpenDriverVehicleUsagePoint#unique(driverKey, intervalId) as DriverVehicleUsagePointEvent; + +insert into OpenDriverVehicleUsagePoint +select * +from DriverVehicleUsagePointEvent(lifecycle = 'INSERT'); + +@Priority(20) +on DriverVehicleUsagePointEvent(lifecycle = 'WITHDRAW') as withdrawEvent +insert into DriverVehicleUsageIntervalEvent +select + insertEvent.sessionId as sessionId, + insertEvent.driverKey as driverKey, + insertEvent.intervalId as intervalId, + insertEvent.sourceRowId as firstSourceIntervalId, + withdrawEvent.sourceRowId as lastSourceIntervalId, + insertEvent.occurredAt as startedAt, + withdrawEvent.occurredAt as endedAt, + insertEvent.occurredAtEpochSecond as startedAtEpochSecond, + withdrawEvent.occurredAtEpochSecond as endedAtEpochSecond, + withdrawEvent.occurredAtEpochSecond - insertEvent.occurredAtEpochSecond as durationSeconds, + insertEvent.odometerKm as odometerBeginKm, + withdrawEvent.odometerKm as odometerEndKm, + insertEvent.registrationKey as registrationKey, + insertEvent.vehicleKey as vehicleKey, + insertEvent.sourceKind as sourceKind, + insertEvent.sourceRowIds as sourceIntervalIds +from OpenDriverVehicleUsagePoint as insertEvent +where insertEvent.driverKey = withdrawEvent.driverKey + and insertEvent.intervalId = withdrawEvent.intervalId + and withdrawEvent.occurredAtEpochSecond >= insertEvent.occurredAtEpochSecond; + +@Priority(10) +on DriverVehicleUsagePointEvent(lifecycle = 'WITHDRAW') as withdrawEvent +delete from OpenDriverVehicleUsagePoint as openEvent +where openEvent.driverKey = withdrawEvent.driverKey + and openEvent.intervalId = withdrawEvent.intervalId; + +@name('driverVehicleUsageIntervals') +select * +from DriverVehicleUsageIntervalEvent; diff --git a/src/test/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingControllerTest.java b/src/test/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingControllerTest.java index e4dfbfc..4559b91 100644 --- a/src/test/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingControllerTest.java +++ b/src/test/java/at/procon/eventhub/processing/api/UnifiedRuntimeProcessingControllerTest.java @@ -244,7 +244,7 @@ class UnifiedRuntimeProcessingControllerTest { 1, 3, List.of(new UnifiedDiscoveredVehicleRef("VEH-1", "VIN-1", "12", "REG-1")), - projection, + projection.toDriverWorkingTime(), List.of("runtime derived") )); @@ -296,7 +296,7 @@ class UnifiedRuntimeProcessingControllerTest { .thenReturn(List.of(new RuntimeProcessingPlanDescriptorDto( "driver-working-time-v1", Set.of("tachograph-driver-esper-v1"), - "Driver working-time and tachograph-derived processing", + "Driver working-time processing", "Runs common runtime event processing modules.", RuntimeEventPartitioningStrategy.DRIVER, List.of(RuntimeEventPartitioningStrategy.DRIVER), @@ -576,9 +576,7 @@ class UnifiedRuntimeProcessingControllerTest { derivedProjectionService, null, runtimeEventProcessingService, - parityValidationService, - null, - null + parityValidationService )) .setMessageConverters(new MappingJackson2HttpMessageConverter(objectMapper)) .setControllerAdvice(new UnifiedRuntimeProcessingExceptionHandler()) diff --git a/src/test/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlanRegistryTest.java b/src/test/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlanRegistryTest.java new file mode 100644 index 0000000..75e8a57 --- /dev/null +++ b/src/test/java/at/procon/eventhub/processing/eventprocessing/plan/RuntimeProcessingPlanRegistryTest.java @@ -0,0 +1,60 @@ +package at.procon.eventhub.processing.eventprocessing.plan; + +import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy; +import java.util.List; +import java.util.Set; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class RuntimeProcessingPlanRegistryTest { + + @Test + void resolvesPlansByPrimaryKeyAndAlias() { + RuntimeProcessingPlanRegistry registry = new RuntimeProcessingPlanRegistry(List.of(new TestPlan("plan-a", Set.of("legacy-a")))); + + assertThat(registry.require("plan-a").processingPlanKey()).isEqualTo("plan-a"); + assertThat(registry.require("legacy-a").processingPlanKey()).isEqualTo("plan-a"); + assertThat(registry.planDescriptors()).hasSize(1); + assertThat(registry.planDescriptors().get(0).aliases()).contains("legacy-a"); + } + + @Test + void rejectsDuplicatePlanKeys() { + assertThatThrownBy(() -> new RuntimeProcessingPlanRegistry(List.of( + new TestPlan("plan-a", Set.of()), + new TestPlan("plan-a", Set.of()) + ))) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Duplicate runtime processingPlanKey"); + } + + @Test + void rejectsDuplicateAliasesPointingToDifferentPlans() { + assertThatThrownBy(() -> new RuntimeProcessingPlanRegistry(List.of( + new TestPlan("plan-a", Set.of("alias")), + new TestPlan("plan-b", Set.of("alias")) + ))) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("Duplicate runtime processing plan alias/key"); + } + + private record TestPlan(String key, Set aliases) implements RuntimeProcessingPlan { + + @Override + public String processingPlanKey() { + return key; + } + + @Override + public RuntimeEventPartitioningStrategy defaultPartitioningStrategy() { + return RuntimeEventPartitioningStrategy.DRIVER; + } + + @Override + public RuntimeProcessingExecutionResultDto execute(RuntimeProcessingExecutionApiRequest request) { + throw new UnsupportedOperationException("not needed"); + } + } +} diff --git a/src/test/java/at/procon/eventhub/processing/eventprocessing/profile/TachographDriverEsperRuntimeEventProcessingProfileTest.java b/src/test/java/at/procon/eventhub/processing/eventprocessing/profile/TachographDriverEsperRuntimeEventProcessingProfileTest.java index 493e202..47dc511 100644 --- a/src/test/java/at/procon/eventhub/processing/eventprocessing/profile/TachographDriverEsperRuntimeEventProcessingProfileTest.java +++ b/src/test/java/at/procon/eventhub/processing/eventprocessing/profile/TachographDriverEsperRuntimeEventProcessingProfileTest.java @@ -8,13 +8,13 @@ import static org.mockito.Mockito.when; import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto; import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest; -import at.procon.eventhub.processing.dto.UnifiedRuntimeTachographEsperScopeResultDto; +import at.procon.eventhub.processing.dto.UnifiedRuntimeDriverWorkingTimeScopeResultDto; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest; import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest; import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy; import at.procon.eventhub.processing.model.UnifiedEventSourceFamily; import at.procon.eventhub.processing.model.UnifiedRuntimeProcessingRequest; -import at.procon.eventhub.processing.service.UnifiedRuntimeTachographEsperScopeProcessingService; +import at.procon.eventhub.processing.service.RuntimeDriverWorkingTimeScopeProcessingService; import java.time.OffsetDateTime; import java.util.List; import java.util.Map; @@ -28,7 +28,7 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest { @Test void exposesDiscoveryMetadata() { TachographDriverEsperRuntimeEventProcessingProfile profile = new TachographDriverEsperRuntimeEventProcessingProfile( - org.mockito.Mockito.mock(UnifiedRuntimeTachographEsperScopeProcessingService.class) + org.mockito.Mockito.mock(RuntimeDriverWorkingTimeScopeProcessingService.class) ); assertThat(profile.profileKey()).isEqualTo("tachograph-driver-esper-v1"); @@ -46,7 +46,7 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest { @Test void delegatesToTachographScopeServiceAndMapsPartitionResults() { - UnifiedRuntimeTachographEsperScopeProcessingService scopeService = org.mockito.Mockito.mock(UnifiedRuntimeTachographEsperScopeProcessingService.class); + RuntimeDriverWorkingTimeScopeProcessingService scopeService = org.mockito.Mockito.mock(RuntimeDriverWorkingTimeScopeProcessingService.class); TachographDriverEsperRuntimeEventProcessingProfile profile = new TachographDriverEsperRuntimeEventProcessingProfile(scopeService); UUID sessionId = UUID.randomUUID(); @@ -127,7 +127,7 @@ class TachographDriverEsperRuntimeEventProcessingProfileTest { List.of("driver processed") ); when(scopeService.processScope(any(), anyBoolean())) - .thenReturn(new UnifiedRuntimeTachographEsperScopeResultDto( + .thenReturn(new UnifiedRuntimeDriverWorkingTimeScopeResultDto( processedRequest, 5, 1, diff --git a/src/test/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationServiceTest.java b/src/test/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationServiceTest.java new file mode 100644 index 0000000..2ab2092 --- /dev/null +++ b/src/test/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeMixedSourceEvidenceValidationServiceTest.java @@ -0,0 +1,135 @@ +package at.procon.eventhub.processing.eventprocessing.validation; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import at.procon.eventhub.processing.dto.RuntimeDriverPartitionDebugDto; +import at.procon.eventhub.processing.dto.RuntimeSupportEvidenceNormalizationDebugDto; +import at.procon.eventhub.processing.dto.UnifiedRuntimeDerivedProjectionResultDto; +import at.procon.eventhub.processing.eventprocessing.RuntimeEventProcessingService; +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventPartitioningApiRequest; +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingApiRequest; +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingPartitionResultDto; +import at.procon.eventhub.processing.eventprocessing.dto.RuntimeEventProcessingResultDto; +import at.procon.eventhub.processing.eventprocessing.partition.RuntimeEventPartitioningStrategy; +import at.procon.eventhub.processing.eventprocessing.profile.TachographDriverEsperRuntimeEventProcessingProfile; +import at.procon.eventhub.processing.model.UnifiedEventSourceFamily; +import at.procon.eventhub.processing.model.UnifiedRuntimeEventBackend; +import at.procon.eventhub.processing.dto.UnifiedRuntimeProcessingApiRequest; +import java.time.OffsetDateTime; +import java.util.List; +import java.util.Map; +import java.util.Set; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mockito; + +class RuntimeMixedSourceEvidenceValidationServiceTest { + + @Test + void validatesAttachedAndNormalizedEvidenceCounts() { + RuntimeEventProcessingService processingService = Mockito.mock(RuntimeEventProcessingService.class); + RuntimeMixedSourceEvidenceValidationService service = new RuntimeMixedSourceEvidenceValidationService(processingService); + + UnifiedRuntimeDerivedProjectionResultDto driverResult = new UnifiedRuntimeDerivedProjectionResultDto( + null, + 2, + 1, + 1, + 3, + List.of(), + null, + List.of("projection note"), + new RuntimeSupportEvidenceNormalizationDebugDto(3, 1, 2, List.of("normalization note")), + new RuntimeDriverPartitionDebugDto( + "DRIVER:1", + 2, + 1, + 1, + 1, + 0, + 3, + List.of(), + List.of(), + List.of("debug note"), + List.of() + ) + ); + when(processingService.process(any())).thenReturn(new RuntimeEventProcessingResultDto( + TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY, + RuntimeEventPartitioningStrategy.DRIVER, + null, + 3, + 1, + 1, + List.of(), + Map.of("DRIVER:1", new RuntimeEventProcessingPartitionResultDto( + "DRIVER", + "DRIVER:1", + "UnifiedRuntimeDerivedProjectionResultDto", + driverResult, + Map.of() + )), + List.of("runtime note"), + List.of() + )); + + RuntimeMixedSourceEvidenceValidationResultDto result = service.validate(new RuntimeMixedSourceEvidenceValidationApiRequest( + new RuntimeEventProcessingApiRequest( + TachographDriverEsperRuntimeEventProcessingProfile.PROFILE_KEY, + new UnifiedRuntimeProcessingApiRequest( + null, + List.of(), + null, + "default", + Set.of(UnifiedEventSourceFamily.TACHOGRAPH_FILE_SESSION), + UnifiedRuntimeEventBackend.SOURCE_DB, + "DRIVER:1", + Set.of(), + false, + Set.of(), + false, + null, + null, + null, + OffsetDateTime.parse("2026-05-01T00:00:00Z"), + OffsetDateTime.parse("2026-05-02T00:00:00Z"), + true, + 15, + 3, + 720 + ), + new RuntimeEventPartitioningApiRequest( + RuntimeEventPartitioningStrategy.DRIVER, + null, + null, + Set.of("DRIVER:1"), + null, + null, + null, + true, + 15, + false + ), + Map.of() + ), + Set.of("DRIVER:1"), + 1, + 1, + true + )); + + assertThat(result.status()).isEqualTo("PASSED"); + assertThat(result.totalAttachedVehicleEvidenceEventCount()).isEqualTo(1); + assertThat(result.totalNormalizedSupportEvidenceEventCount()).isEqualTo(1); + assertThat(result.partitions().get("DRIVER:1").partitionDebugPresent()).isTrue(); + assertThat(result.partitions().get("DRIVER:1").normalizedSupportEvidenceEventCount()).isEqualTo(1); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RuntimeEventProcessingApiRequest.class); + verify(processingService).process(captor.capture()); + assertThat(captor.getValue().partitioning().includeDebugOrDefault()).isTrue(); + assertThat(captor.getValue().parameters()).containsEntry("includePartitionDebug", true); + } +} diff --git a/src/test/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeTachographParityValidationServiceTest.java b/src/test/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeTachographParityValidationServiceTest.java index 49a129f..73c011f 100644 --- a/src/test/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeTachographParityValidationServiceTest.java +++ b/src/test/java/at/procon/eventhub/processing/eventprocessing/validation/RuntimeTachographParityValidationServiceTest.java @@ -65,7 +65,7 @@ class RuntimeTachographParityValidationServiceTest { 0, 4, List.of(), - projection(sessionId, driverKey, 2, 1, 1), + projection(sessionId, driverKey, 2, 1, 1).toDriverWorkingTime(), List.of() ); when(runtimeEventProcessingService.process(any()))