diff --git a/src/main/java/at/procon/eventhub/service/AbstractTachographActivityRowMapper.java b/src/main/java/at/procon/eventhub/service/AbstractTachographActivityRowMapper.java index 4df29e2..cd6f0f6 100644 --- a/src/main/java/at/procon/eventhub/service/AbstractTachographActivityRowMapper.java +++ b/src/main/java/at/procon/eventhub/service/AbstractTachographActivityRowMapper.java @@ -267,6 +267,15 @@ abstract class AbstractTachographActivityRowMapper implements TachographExtracti return fallback; } String normalized = value.trim().toUpperCase(Locale.ROOT).replace('-', '_').replace(' ', '_'); + if ("CODRIVER".equals(normalized)) { + normalized = "CO_DRIVER"; + } else if ("DRIVING".equals(normalized)) { + normalized = "DRIVE"; + } else if ("BREAK/REST".equals(normalized) || "REST".equals(normalized)) { + normalized = "BREAK_REST"; + } else if ("AVAILABLE".equals(normalized)) { + normalized = "AVAILABILITY"; + } try { return Enum.valueOf(type, normalized); } catch (IllegalArgumentException ignored) { diff --git a/src/main/java/at/procon/eventhub/service/JdbcTachographExtractionBatchExecutor.java b/src/main/java/at/procon/eventhub/service/JdbcTachographExtractionBatchExecutor.java index 75b5947..bdb9a47 100644 --- a/src/main/java/at/procon/eventhub/service/JdbcTachographExtractionBatchExecutor.java +++ b/src/main/java/at/procon/eventhub/service/JdbcTachographExtractionBatchExecutor.java @@ -105,7 +105,11 @@ public class JdbcTachographExtractionBatchExecutor implements TachographExtracti .filter(event -> event.sourcePackageRef() != null && event.sourcePackageRef().importedIntoSourceAt() != null) .max((left, right) -> left.sourcePackageRef().importedIntoSourceAt().compareTo(right.sourcePackageRef().importedIntoSourceAt())) .map(event -> event.sourcePackageRef().sourcePackageId()) - .orElse(cursor == null ? null : cursor.lastSourcePackageId()); + .orElseGet(() -> events.stream() + .map(event -> event.sourcePackageRef() == null ? null : event.sourcePackageRef().sourcePackageId()) + .filter(value -> value != null && !value.isBlank()) + .max(this::compareSourcePackageId) + .orElse(cursor == null ? null : cursor.lastSourcePackageId())); return new TachographExtractionBatchResultDto( packageId, @@ -174,4 +178,24 @@ public class JdbcTachographExtractionBatchExecutor implements TachographExtracti throw new IllegalStateException("Cannot load tachograph extraction SQL resource " + location, e); } } + + private int compareSourcePackageId(String left, String right) { + Integer leftInt = parseInteger(left); + Integer rightInt = parseInteger(right); + if (leftInt != null && rightInt != null) { + return leftInt.compareTo(rightInt); + } + return left.compareTo(right); + } + + private Integer parseInteger(String value) { + if (value == null || value.isBlank()) { + return null; + } + try { + return Integer.parseInt(value.trim()); + } catch (NumberFormatException ignored) { + return null; + } + } } diff --git a/src/main/resources/sql/tachograph/card-activity.sql b/src/main/resources/sql/tachograph/card-activity.sql index 0eee2a9..0c65438 100644 --- a/src/main/resources/sql/tachograph/card-activity.sql +++ b/src/main/resources/sql/tachograph/card-activity.sql @@ -1,50 +1,83 @@ /* - * CardActivity DRIVER_ACTIVITY extraction. + * CardActivity DRIVER_ACTIVITY extraction for the bytebar tachograph schema. * - * Adapt table and column names to the concrete tachograph SQL Server schema. - * Keep the selected aliases stable; CardActivityRowMapper consumes this alias contract. + * Real join path: + * CardActivity -> CardDailyActivity -> Card -> Driver/Nation + * + * CardActivity itself has no direct vehicle reference. The OUTER APPLY resolves + * the best matching CardVehiclesUsed row for the activity timestamp when one is + * available. */ select - cast(ca.Id as varchar(128)) as source_row_id, - cast(ca.Id as varchar(128)) as card_activity_id, - concat('TACHOGRAPH:CARD_ACTIVITY:', ca.Id) as external_source_event_id, + cast(ca.ID as varchar(128)) as source_row_id, + cast(ca.ID as varchar(128)) as card_activity_id, + concat('TACHOGRAPH:CARD_ACTIVITY:', ca.ID) as external_source_event_id, - ca.ActivityTime as occurred_at, - ca.ReceivedAt as received_partner_at, + ca.BeginTime as occurred_at, + cast(null as datetime) as received_partner_at, ca.Activity as activity_code, - ca.ActivityText as activity_text, - ca.EventType as event_type, - ca.Lifecycle as lifecycle, - ca.CardSlot as card_slot, + ca.Activity as activity_text, + case upper(coalesce(ca.Activity, '')) + when 'DRIVING' then 'DRIVE' + when 'DRIVE' then 'DRIVE' + when 'WORK' then 'WORK' + when 'AVAILABILITY' then 'AVAILABILITY' + when 'AVAILABLE' then 'AVAILABILITY' + when 'BREAK_REST' then 'BREAK_REST' + when 'BREAK/REST' then 'BREAK_REST' + when 'REST' then 'BREAK_REST' + else 'UNKNOWN_ACTIVITY' + end as event_type, + 'SNAPSHOT' as lifecycle, + ca.Slot as card_slot, ca.CardStatus as card_status, ca.DrivingStatus as driving_status, - ca.OdometerM as odometer_m, + cast(null as bigint) as odometer_m, - cast(ca.DriverId as varchar(128)) as driver_source_entity_id, - ca.DriverCardNation as driver_card_nation, - ca.DriverCardNumber as driver_card_number, + cast(d.ID as varchar(128)) as driver_source_entity_id, + cn.AlphaCode as driver_card_nation, + c.CardNumber as driver_card_number, - cast(ca.VehicleId as varchar(128)) as vehicle_source_entity_id, - ca.VehicleVin as vehicle_vin, - ca.VehicleRegistrationNation as vehicle_registration_nation, - ca.VehicleRegistrationNumber as vehicle_registration_number, + cast(coalesce(cvu.ID_Vehicle, v.ID) as varchar(128)) as vehicle_source_entity_id, + coalesce(cvu.VIN, vi.VIN) as vehicle_vin, + vn.AlphaCode as vehicle_registration_nation, + v.VRN as vehicle_registration_number, 'DRIVER_CARD' as source_package_kind, - cast(ca.SourcePackageId as varchar(128)) as source_package_id, - cast(ca.DriverId as varchar(128)) as source_package_entity_id, - ca.SourcePackagePeriodFrom as source_package_period_from, - ca.SourcePackagePeriodTo as source_package_period_to, - ca.SourcePackageImportedAt as source_package_imported_at -from CardActivity ca -where (:occurredFrom is null or ca.ActivityTime >= :occurredFrom) - and (:occurredTo is null or ca.ActivityTime < :occurredTo) + cast(coalesce(ca.ID_FileLog, cda.ID_FileLog, c.ID_FileLog) as varchar(128)) as source_package_id, + cast(c.ID as varchar(128)) as source_package_entity_id, + cda.RecordDate as source_package_period_from, + coalesce(cda.RecordDateTo, dateadd(day, 1, cda.RecordDate)) as source_package_period_to, + cast(null as datetime) as source_package_imported_at +from dbo.CardActivity ca +join dbo.CardDailyActivity cda on cda.ID = ca.ID_DailyActivity +join dbo.Card c on c.ID = cda.ID_Card +left join dbo.Driver d on d.ID = c.ID_Driver +left join dbo.Nation cn on cn.ID = c.ID_Nation +outer apply ( + select top 1 used.ID_Vehicle, + used.VIN, + used.OdoBegin, + used.ID_VUInstallation + from dbo.CardVehiclesUsed used + where used.ID_Card = c.ID + and (used.FirstUse is null or used.FirstUse <= ca.BeginTime) + and (used.LastUse is null or used.LastUse >= ca.BeginTime) + order by + case when used.FirstUse is null then 1 else 0 end, + used.FirstUse desc, + used.ID desc +) cvu +left join dbo.Vehicle v on v.ID = cvu.ID_Vehicle +left join dbo.VehicleIdentification vi on vi.ID = v.ID_VehicleIdentification +left join dbo.Nation vn on vn.ID = v.ID_Nation +where (:occurredFrom is null or ca.BeginTime >= :occurredFrom) + and (:occurredTo is null or ca.BeginTime < :occurredTo) and ( - :lastSourcePackageImportedAt is null - or ca.SourcePackageImportedAt > :lastSourcePackageImportedAt - or (ca.SourcePackageImportedAt = :lastSourcePackageImportedAt and cast(ca.SourcePackageId as varchar(128)) > :lastSourcePackageId) + :lastSourcePackageId is null + or coalesce(ca.ID_FileLog, cda.ID_FileLog, c.ID_FileLog, ca.ID) > try_convert(int, :lastSourcePackageId) ) /* - * Organisation filtering is schema-specific. Once stable joins are known, add: - * and (:rootOrganisationId is null or ...) - * using :includeChildren to decide whether to match only root or descendants. + * Organisation filtering can be added through Driver_I_90021 / Vehicle_I_90021 + * once the exact organisation subtree semantics are confirmed. */ diff --git a/src/main/resources/sql/tachograph/vu-activity.sql b/src/main/resources/sql/tachograph/vu-activity.sql index d32d391..f19a584 100644 --- a/src/main/resources/sql/tachograph/vu-activity.sql +++ b/src/main/resources/sql/tachograph/vu-activity.sql @@ -1,50 +1,80 @@ /* - * VUActivity DRIVER_ACTIVITY extraction. + * VUActivity DRIVER_ACTIVITY extraction for the bytebar tachograph schema. * - * Adapt table and column names to the concrete tachograph SQL Server schema. - * Keep the selected aliases stable; VuActivityRowMapper consumes this alias contract. + * Real join path: + * VUActivity -> VUDailyActivity -> VUInstallation -> VehicleIdentification + * Optional driver/card context comes from VUActivity.ID_IWCycle -> IWCycle -> Card. */ select - cast(va.Id as varchar(128)) as source_row_id, - cast(va.Id as varchar(128)) as vu_activity_id, - concat('TACHOGRAPH:VU_ACTIVITY:', va.Id) as external_source_event_id, + cast(va.ID as varchar(128)) as source_row_id, + cast(va.ID as varchar(128)) as vu_activity_id, + concat('TACHOGRAPH:VU_ACTIVITY:', va.ID) as external_source_event_id, - va.ActivityTime as occurred_at, - va.ReceivedAt as received_partner_at, + va.BeginTime as occurred_at, + cast(null as datetime) as received_partner_at, va.Activity as activity_code, - va.ActivityText as activity_text, - va.EventType as event_type, - va.Lifecycle as lifecycle, - va.CardSlot as card_slot, + va.Activity as activity_text, + case upper(coalesce(va.Activity, '')) + when 'DRIVING' then 'DRIVE' + when 'DRIVE' then 'DRIVE' + when 'WORK' then 'WORK' + when 'AVAILABILITY' then 'AVAILABILITY' + when 'AVAILABLE' then 'AVAILABILITY' + when 'BREAK_REST' then 'BREAK_REST' + when 'BREAK/REST' then 'BREAK_REST' + when 'REST' then 'BREAK_REST' + else 'UNKNOWN_ACTIVITY' + end as event_type, + 'SNAPSHOT' as lifecycle, + va.Slot as card_slot, va.CardStatus as card_status, va.DrivingStatus as driving_status, - va.OdometerM as odometer_m, + cast(iw.OdoBegin as bigint) as odometer_m, - cast(va.DriverId as varchar(128)) as driver_source_entity_id, - va.DriverCardNation as driver_card_nation, - va.DriverCardNumber as driver_card_number, + cast(d.ID as varchar(128)) as driver_source_entity_id, + cn.AlphaCode as driver_card_nation, + c.CardNumber as driver_card_number, - cast(va.VehicleId as varchar(128)) as vehicle_source_entity_id, - va.VehicleVin as vehicle_vin, - va.VehicleRegistrationNation as vehicle_registration_nation, - va.VehicleRegistrationNumber as vehicle_registration_number, + cast(v.ID as varchar(128)) as vehicle_source_entity_id, + vi.VIN as vehicle_vin, + vn.AlphaCode as vehicle_registration_nation, + v.VRN as vehicle_registration_number, 'VEHICLE_UNIT' as source_package_kind, - cast(va.SourcePackageId as varchar(128)) as source_package_id, - cast(va.VehicleId as varchar(128)) as source_package_entity_id, - va.SourcePackagePeriodFrom as source_package_period_from, - va.SourcePackagePeriodTo as source_package_period_to, - va.SourcePackageImportedAt as source_package_imported_at -from VUActivity va -where (:occurredFrom is null or va.ActivityTime >= :occurredFrom) - and (:occurredTo is null or va.ActivityTime < :occurredTo) + cast(coalesce(va.ID_FileLog, vda.ID_FileLog, vui.ID_FileLog) as varchar(128)) as source_package_id, + cast(vui.ID_VehicleIdentification as varchar(128)) as source_package_entity_id, + vda.RecordDate as source_package_period_from, + dateadd(day, 1, vda.RecordDate) as source_package_period_to, + cast(null as datetime) as source_package_imported_at +from dbo.VUActivity va +join dbo.VUDailyActivity vda on vda.ID = va.ID_VUDailyActivity +join dbo.VUInstallation vui on vui.ID = vda.ID_VUInstallation +join dbo.VehicleIdentification vi on vi.ID = vui.ID_VehicleIdentification +outer apply ( + select top 1 vehicle.ID, + vehicle.VRN, + vehicle.ID_Nation + from dbo.Vehicle vehicle + where vehicle.ID_VehicleIdentification = vi.ID + and (vehicle.ValidFrom is null or vehicle.ValidFrom <= va.BeginTime) + and (vehicle.ValidTo is null or vehicle.ValidTo > va.BeginTime) + order by + case when vehicle.ValidFrom is null then 1 else 0 end, + vehicle.ValidFrom desc, + vehicle.ID desc +) v +left join dbo.Nation vn on vn.ID = v.ID_Nation +left join dbo.IWCycle iw on iw.ID = va.ID_IWCycle +left join dbo.Card c on c.ID = iw.ID_Card +left join dbo.Driver d on d.ID = c.ID_Driver +left join dbo.Nation cn on cn.ID = c.ID_Nation +where (:occurredFrom is null or va.BeginTime >= :occurredFrom) + and (:occurredTo is null or va.BeginTime < :occurredTo) and ( - :lastSourcePackageImportedAt is null - or va.SourcePackageImportedAt > :lastSourcePackageImportedAt - or (va.SourcePackageImportedAt = :lastSourcePackageImportedAt and cast(va.SourcePackageId as varchar(128)) > :lastSourcePackageId) + :lastSourcePackageId is null + or coalesce(va.ID_FileLog, vda.ID_FileLog, vui.ID_FileLog, va.ID) > try_convert(int, :lastSourcePackageId) ) /* - * Organisation filtering is schema-specific. Once stable joins are known, add: - * and (:rootOrganisationId is null or ...) - * using :includeChildren to decide whether to match only root or descendants. + * Organisation filtering can be added through Vehicle_I_90021 / Driver_I_90021 + * once the exact organisation subtree semantics are confirmed. */