Fix tachograph ingestion backpressure and vehicle projection
This commit is contained in:
parent
ec533bb24f
commit
866e275691
|
|
@ -0,0 +1,317 @@
|
||||||
|
create extension if not exists pgcrypto;
|
||||||
|
create extension if not exists postgis;
|
||||||
|
|
||||||
|
create schema if not exists eventhub;
|
||||||
|
|
||||||
|
create table if not exists eventhub.event_source (
|
||||||
|
id integer generated always as identity primary key,
|
||||||
|
tenant_key text not null,
|
||||||
|
provider_key text not null,
|
||||||
|
source_kind text not null,
|
||||||
|
source_key text not null,
|
||||||
|
source_instance_key text not null default 'default',
|
||||||
|
tenant_provider_setting_key text,
|
||||||
|
external_fleet_key text,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
constraint ux_event_source unique (tenant_key, provider_key, source_kind, source_key, source_instance_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists eventhub.import_run (
|
||||||
|
id uuid primary key,
|
||||||
|
tenant_key text not null,
|
||||||
|
event_source_id integer not null references eventhub.event_source(id),
|
||||||
|
mode text not null,
|
||||||
|
status text not null,
|
||||||
|
refresh_master_data_first boolean not null default true,
|
||||||
|
source_group_type text,
|
||||||
|
source_group_entity_id text,
|
||||||
|
source_group_code text,
|
||||||
|
source_group_name text,
|
||||||
|
import_scope_type text not null,
|
||||||
|
root_source_org_entity_id text,
|
||||||
|
root_source_org_code text,
|
||||||
|
root_source_org_name text,
|
||||||
|
include_children boolean not null default false,
|
||||||
|
occurred_from timestamptz,
|
||||||
|
occurred_to timestamptz,
|
||||||
|
requested_event_families text[] not null default '{}',
|
||||||
|
acquisition_strategy text,
|
||||||
|
metadata jsonb not null default '{}'::jsonb,
|
||||||
|
planned_package_count integer not null default 0,
|
||||||
|
started_at timestamptz not null default now(),
|
||||||
|
finished_at timestamptz,
|
||||||
|
error_message text,
|
||||||
|
constraint chk_import_run_occ_time_order check (occurred_from is null or occurred_to is null or occurred_from < occurred_to)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists eventhub.import_cursor (
|
||||||
|
id uuid primary key,
|
||||||
|
tenant_key text not null,
|
||||||
|
event_source_id integer not null references eventhub.event_source(id),
|
||||||
|
scope_hash text not null,
|
||||||
|
event_family text not null,
|
||||||
|
source_kind text not null,
|
||||||
|
cursor_type text not null,
|
||||||
|
last_source_package_imported_at timestamptz,
|
||||||
|
last_source_package_id text,
|
||||||
|
last_source_row_updated_at timestamptz,
|
||||||
|
last_occurred_to timestamptz,
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint ux_import_cursor unique (tenant_key, event_source_id, scope_hash, event_family, source_kind, cursor_type)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists eventhub.data_package (
|
||||||
|
id uuid primary key,
|
||||||
|
event_source_id integer not null references eventhub.event_source(id),
|
||||||
|
import_run_id uuid references eventhub.import_run(id),
|
||||||
|
tenant_key text not null,
|
||||||
|
package_key text not null,
|
||||||
|
package_type text not null,
|
||||||
|
status text not null,
|
||||||
|
source_group_type text,
|
||||||
|
source_group_entity_id text,
|
||||||
|
source_group_code text,
|
||||||
|
source_group_name text,
|
||||||
|
import_scope_type text,
|
||||||
|
root_source_org_entity_id text,
|
||||||
|
root_source_org_code text,
|
||||||
|
root_source_org_name text,
|
||||||
|
include_children boolean not null default false,
|
||||||
|
occurred_from timestamptz,
|
||||||
|
occurred_to timestamptz,
|
||||||
|
event_family text,
|
||||||
|
business_date date,
|
||||||
|
external_package_id text,
|
||||||
|
extraction_code text,
|
||||||
|
extraction_source_kind text,
|
||||||
|
entity_axis text,
|
||||||
|
batch_no integer,
|
||||||
|
chunk_from timestamptz,
|
||||||
|
chunk_to timestamptz,
|
||||||
|
source_package_kind text,
|
||||||
|
source_package_id text,
|
||||||
|
source_package_entity_id text,
|
||||||
|
source_package_period_from timestamptz,
|
||||||
|
source_package_period_to timestamptz,
|
||||||
|
source_package_imported_at timestamptz,
|
||||||
|
received_at timestamptz not null default now(),
|
||||||
|
completed_at timestamptz,
|
||||||
|
event_count integer not null default 0,
|
||||||
|
metadata jsonb not null default '{}'::jsonb,
|
||||||
|
error_message text,
|
||||||
|
constraint ux_data_package_package_key unique (tenant_key, event_source_id, package_key),
|
||||||
|
constraint chk_data_package_occ_time_order check (occurred_from is null or occurred_to is null or occurred_from < occurred_to),
|
||||||
|
constraint chk_data_package_chunk_time_order check (chunk_from is null or chunk_to is null or chunk_from < chunk_to)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists eventhub.source_master_entity (
|
||||||
|
id uuid primary key,
|
||||||
|
tenant_key text not null,
|
||||||
|
event_source_id integer not null references eventhub.event_source(id),
|
||||||
|
entity_type text not null,
|
||||||
|
source_entity_id text not null,
|
||||||
|
source_external_key text,
|
||||||
|
display_name text,
|
||||||
|
active boolean,
|
||||||
|
valid_from timestamptz,
|
||||||
|
valid_to timestamptz,
|
||||||
|
source_updated_at timestamptz,
|
||||||
|
payload jsonb not null default '{}'::jsonb,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint ux_source_master_entity unique (tenant_key, event_source_id, entity_type, source_entity_id),
|
||||||
|
constraint chk_source_master_entity_valid_time_order check (valid_from is null or valid_to is null or valid_from <= valid_to)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists eventhub.source_master_relation (
|
||||||
|
id uuid primary key,
|
||||||
|
tenant_key text not null,
|
||||||
|
event_source_id integer not null references eventhub.event_source(id),
|
||||||
|
relation_key text not null,
|
||||||
|
relation_type text not null,
|
||||||
|
from_entity_type text not null,
|
||||||
|
from_source_entity_id text not null,
|
||||||
|
to_entity_type text not null,
|
||||||
|
to_source_entity_id text not null,
|
||||||
|
valid_from timestamptz,
|
||||||
|
valid_to timestamptz,
|
||||||
|
source_updated_at timestamptz,
|
||||||
|
payload jsonb not null default '{}'::jsonb,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint ux_source_master_relation unique (tenant_key, event_source_id, relation_key),
|
||||||
|
constraint chk_source_master_relation_valid_time_order check (valid_from is null or valid_to is null or valid_from <= valid_to)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists eventhub.vehicle (
|
||||||
|
id uuid primary key,
|
||||||
|
tenant_key text not null,
|
||||||
|
event_source_id integer not null references eventhub.event_source(id),
|
||||||
|
source_vehicle_entity_id text,
|
||||||
|
vin text,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists eventhub.vehicle_registration (
|
||||||
|
id uuid primary key,
|
||||||
|
tenant_key text not null,
|
||||||
|
event_source_id integer not null references eventhub.event_source(id),
|
||||||
|
source_registration_entity_id text,
|
||||||
|
nation text not null,
|
||||||
|
registration_number text not null,
|
||||||
|
source_updated_at timestamptz,
|
||||||
|
payload jsonb not null default '{}'::jsonb,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists eventhub.vehicle_registration_assignment (
|
||||||
|
id uuid primary key,
|
||||||
|
tenant_key text not null,
|
||||||
|
event_source_id integer not null references eventhub.event_source(id),
|
||||||
|
vehicle_registration_id uuid not null references eventhub.vehicle_registration(id) on delete cascade,
|
||||||
|
vehicle_id uuid not null references eventhub.vehicle(id) on delete cascade,
|
||||||
|
valid_from timestamptz,
|
||||||
|
valid_to timestamptz,
|
||||||
|
source_updated_at timestamptz,
|
||||||
|
payload jsonb not null default '{}'::jsonb,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint chk_vehicle_registration_assignment_valid_time_order check (valid_from is null or valid_to is null or valid_from <= valid_to)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists eventhub.event (
|
||||||
|
id uuid not null,
|
||||||
|
event_source_id integer not null references eventhub.event_source(id),
|
||||||
|
data_package_id uuid not null references eventhub.data_package(id),
|
||||||
|
external_source_event_id text not null,
|
||||||
|
driver_entity_id uuid references eventhub.source_master_entity(id),
|
||||||
|
vehicle_id uuid references eventhub.vehicle(id),
|
||||||
|
vehicle_registration_id uuid references eventhub.vehicle_registration(id),
|
||||||
|
source_package_entity_id uuid references eventhub.source_master_entity(id),
|
||||||
|
occurred_at timestamptz not null,
|
||||||
|
received_partner_at timestamptz,
|
||||||
|
received_hub_at timestamptz not null default now(),
|
||||||
|
event_domain text not null,
|
||||||
|
event_type text not null,
|
||||||
|
lifecycle text not null,
|
||||||
|
odometer_m bigint,
|
||||||
|
position geography(Point, 4326),
|
||||||
|
payload jsonb not null default '{}'::jsonb,
|
||||||
|
manual_entry boolean not null default false,
|
||||||
|
source_record_key_hash text not null,
|
||||||
|
event_signature_hash text,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
constraint pk_event primary key (occurred_at, id),
|
||||||
|
constraint chk_event_driver_or_vehicle_ref check (
|
||||||
|
driver_entity_id is not null
|
||||||
|
or vehicle_id is not null
|
||||||
|
or vehicle_registration_id is not null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists eventhub.event_detail (
|
||||||
|
event_occurred_at timestamptz not null,
|
||||||
|
event_id uuid not null,
|
||||||
|
detail_type text not null,
|
||||||
|
attributes jsonb not null default '{}'::jsonb,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
constraint pk_event_detail primary key (event_occurred_at, event_id, detail_type),
|
||||||
|
constraint fk_event_detail_event foreign key (event_occurred_at, event_id)
|
||||||
|
references eventhub.event(occurred_at, id)
|
||||||
|
on delete cascade
|
||||||
|
);
|
||||||
|
|
||||||
|
create unique index if not exists ux_event_source_record
|
||||||
|
on eventhub.event(source_record_key_hash);
|
||||||
|
|
||||||
|
create index if not exists idx_event_signature
|
||||||
|
on eventhub.event(event_signature_hash)
|
||||||
|
where event_signature_hash is not null;
|
||||||
|
|
||||||
|
create index if not exists idx_event_source_time
|
||||||
|
on eventhub.event(event_source_id, occurred_at desc);
|
||||||
|
|
||||||
|
create index if not exists idx_event_package_time
|
||||||
|
on eventhub.event(data_package_id, occurred_at desc);
|
||||||
|
|
||||||
|
create index if not exists idx_event_domain_type_time
|
||||||
|
on eventhub.event(event_domain, event_type, occurred_at desc);
|
||||||
|
|
||||||
|
create index if not exists idx_event_driver_time
|
||||||
|
on eventhub.event(driver_entity_id, occurred_at desc)
|
||||||
|
where driver_entity_id is not null;
|
||||||
|
|
||||||
|
create index if not exists idx_event_vehicle_time
|
||||||
|
on eventhub.event(vehicle_id, occurred_at desc)
|
||||||
|
where vehicle_id is not null;
|
||||||
|
|
||||||
|
create index if not exists idx_event_vehicle_registration_time
|
||||||
|
on eventhub.event(vehicle_registration_id, occurred_at desc)
|
||||||
|
where vehicle_registration_id is not null;
|
||||||
|
|
||||||
|
create index if not exists idx_event_position_gist
|
||||||
|
on eventhub.event using gist(position)
|
||||||
|
where position is not null;
|
||||||
|
|
||||||
|
create index if not exists idx_event_payload_gin
|
||||||
|
on eventhub.event using gin(payload);
|
||||||
|
|
||||||
|
create index if not exists idx_event_detail_type
|
||||||
|
on eventhub.event_detail(detail_type);
|
||||||
|
|
||||||
|
create index if not exists idx_event_detail_attributes_gin
|
||||||
|
on eventhub.event_detail using gin(attributes);
|
||||||
|
|
||||||
|
create index if not exists idx_source_master_entity_type_key
|
||||||
|
on eventhub.source_master_entity(tenant_key, event_source_id, entity_type, source_external_key)
|
||||||
|
where source_external_key is not null;
|
||||||
|
|
||||||
|
create index if not exists idx_source_master_entity_payload_gin
|
||||||
|
on eventhub.source_master_entity using gin(payload);
|
||||||
|
|
||||||
|
create index if not exists idx_source_master_relation_from
|
||||||
|
on eventhub.source_master_relation(tenant_key, event_source_id, from_entity_type, from_source_entity_id, relation_type);
|
||||||
|
|
||||||
|
create index if not exists idx_source_master_relation_to
|
||||||
|
on eventhub.source_master_relation(tenant_key, event_source_id, to_entity_type, to_source_entity_id, relation_type);
|
||||||
|
|
||||||
|
create index if not exists idx_source_master_relation_payload_gin
|
||||||
|
on eventhub.source_master_relation using gin(payload);
|
||||||
|
|
||||||
|
create index if not exists idx_vehicle_lookup_ctx
|
||||||
|
on eventhub.vehicle(tenant_key, event_source_id, updated_at desc);
|
||||||
|
|
||||||
|
create index if not exists idx_vehicle_source_entity
|
||||||
|
on eventhub.vehicle(tenant_key, event_source_id, source_vehicle_entity_id)
|
||||||
|
where source_vehicle_entity_id is not null;
|
||||||
|
|
||||||
|
create index if not exists idx_vehicle_vin
|
||||||
|
on eventhub.vehicle(tenant_key, event_source_id, vin)
|
||||||
|
where vin is not null;
|
||||||
|
|
||||||
|
create index if not exists idx_vehicle_registration_source_entity
|
||||||
|
on eventhub.vehicle_registration(tenant_key, event_source_id, source_registration_entity_id)
|
||||||
|
where source_registration_entity_id is not null;
|
||||||
|
|
||||||
|
create index if not exists idx_vehicle_registration_plate
|
||||||
|
on eventhub.vehicle_registration(tenant_key, event_source_id, nation, registration_number);
|
||||||
|
|
||||||
|
create index if not exists idx_vehicle_registration_assignment_registration_time
|
||||||
|
on eventhub.vehicle_registration_assignment(vehicle_registration_id, valid_from desc, valid_to);
|
||||||
|
|
||||||
|
create index if not exists idx_vehicle_registration_assignment_vehicle_time
|
||||||
|
on eventhub.vehicle_registration_assignment(vehicle_id, valid_from desc, valid_to);
|
||||||
|
|
||||||
|
create index if not exists idx_data_package_source_time
|
||||||
|
on eventhub.data_package(tenant_key, event_source_id, received_at desc);
|
||||||
|
|
||||||
|
create index if not exists idx_data_package_scope
|
||||||
|
on eventhub.data_package(tenant_key, import_scope_type, root_source_org_entity_id, occurred_from, occurred_to);
|
||||||
|
|
||||||
|
create index if not exists idx_data_package_extraction
|
||||||
|
on eventhub.data_package(tenant_key, event_source_id, import_run_id, event_family, extraction_source_kind, extraction_code, batch_no);
|
||||||
|
|
||||||
|
create index if not exists idx_import_run_source_status
|
||||||
|
on eventhub.import_run(tenant_key, event_source_id, status, started_at desc);
|
||||||
|
|
@ -8,6 +8,8 @@ import org.springframework.stereotype.Component;
|
||||||
@Component
|
@Component
|
||||||
public class EventHubCommonIngestionRoute extends RouteBuilder {
|
public class EventHubCommonIngestionRoute extends RouteBuilder {
|
||||||
|
|
||||||
|
private static final String BATCH_INPUT_QUEUE = "seda:eventhub-batch-input";
|
||||||
|
|
||||||
private final EventHubProperties properties;
|
private final EventHubProperties properties;
|
||||||
private final EventHubEventValidationProcessor validationProcessor;
|
private final EventHubEventValidationProcessor validationProcessor;
|
||||||
private final EventHubPackageKeyProcessor packageKeyProcessor;
|
private final EventHubPackageKeyProcessor packageKeyProcessor;
|
||||||
|
|
@ -33,13 +35,15 @@ public class EventHubCommonIngestionRoute extends RouteBuilder {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void configure() {
|
public void configure() {
|
||||||
|
String batchInputUri = batchInputUri();
|
||||||
|
|
||||||
from("direct:eventhub-normalized-input")
|
from("direct:eventhub-normalized-input")
|
||||||
.routeId("eventhub-normalized-input-route")
|
.routeId("eventhub-normalized-input-route")
|
||||||
.process(validationProcessor)
|
.process(validationProcessor)
|
||||||
.process(packageKeyProcessor)
|
.process(packageKeyProcessor)
|
||||||
.to("seda:eventhub-batch-input");
|
.to(batchInputUri);
|
||||||
|
|
||||||
from("seda:eventhub-batch-input")
|
from(batchInputUri)
|
||||||
.routeId("eventhub-batch-and-persist-route")
|
.routeId("eventhub-batch-and-persist-route")
|
||||||
.aggregate(header(EventHubHeaders.PACKAGE_KEY), aggregationStrategy)
|
.aggregate(header(EventHubHeaders.PACKAGE_KEY), aggregationStrategy)
|
||||||
.completionSize(properties.getBatch().getCompletionSize())
|
.completionSize(properties.getBatch().getCompletionSize())
|
||||||
|
|
@ -48,4 +52,12 @@ public class EventHubCommonIngestionRoute extends RouteBuilder {
|
||||||
.process(batchBuildProcessor)
|
.process(batchBuildProcessor)
|
||||||
.bean(ingestionService, "ingest");
|
.bean(ingestionService, "ingest");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String batchInputUri() {
|
||||||
|
EventHubProperties.Batch batch = properties.getBatch();
|
||||||
|
return BATCH_INPUT_QUEUE
|
||||||
|
+ "?size=" + batch.getQueueSize()
|
||||||
|
+ "&blockWhenFull=" + batch.isBlockWhenFull()
|
||||||
|
+ "&offerTimeout=" + batch.getQueueOfferTimeout().toMillis();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,15 @@ public class EventHubProperties {
|
||||||
/** Maximum time to wait for more events belonging to the same package key. */
|
/** Maximum time to wait for more events belonging to the same package key. */
|
||||||
private Duration completionTimeout = Duration.ofSeconds(5);
|
private Duration completionTimeout = Duration.ofSeconds(5);
|
||||||
|
|
||||||
|
/** Maximum number of normalized events buffered before producers apply backpressure. */
|
||||||
|
private int queueSize = 10000;
|
||||||
|
|
||||||
|
/** Whether producers should wait for queue capacity instead of failing immediately. */
|
||||||
|
private boolean blockWhenFull = true;
|
||||||
|
|
||||||
|
/** Maximum time a producer waits for queue capacity before failing the import. */
|
||||||
|
private Duration queueOfferTimeout = Duration.ofMinutes(5);
|
||||||
|
|
||||||
public int getCompletionSize() {
|
public int getCompletionSize() {
|
||||||
return completionSize;
|
return completionSize;
|
||||||
}
|
}
|
||||||
|
|
@ -52,6 +61,32 @@ public class EventHubProperties {
|
||||||
public void setCompletionTimeout(Duration completionTimeout) {
|
public void setCompletionTimeout(Duration completionTimeout) {
|
||||||
this.completionTimeout = completionTimeout;
|
this.completionTimeout = completionTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int getQueueSize() {
|
||||||
|
return queueSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQueueSize(int queueSize) {
|
||||||
|
this.queueSize = Math.max(1, queueSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBlockWhenFull() {
|
||||||
|
return blockWhenFull;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setBlockWhenFull(boolean blockWhenFull) {
|
||||||
|
this.blockWhenFull = blockWhenFull;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Duration getQueueOfferTimeout() {
|
||||||
|
return queueOfferTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setQueueOfferTimeout(Duration queueOfferTimeout) {
|
||||||
|
if (queueOfferTimeout != null && !queueOfferTimeout.isNegative()) {
|
||||||
|
this.queueOfferTimeout = queueOfferTimeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class Tachograph {
|
public static class Tachograph {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
package at.procon.eventhub.dto;
|
package at.procon.eventhub.dto;
|
||||||
|
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
@ -15,6 +17,6 @@ public record EventHubEventBatchDto(
|
||||||
) {
|
) {
|
||||||
public EventHubEventBatchDto {
|
public EventHubEventBatchDto {
|
||||||
events = events == null ? List.of() : List.copyOf(events);
|
events = events == null ? List.of() : List.copyOf(events);
|
||||||
metadata = metadata == null ? Map.of() : Map.copyOf(metadata);
|
metadata = metadata == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(metadata));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,36 +3,56 @@ package at.procon.eventhub.dto;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Source-side vehicle reference. VIN can be missing for driver-card-only data;
|
* Source-side vehicle reference. A physical tachograph vehicle is identified by
|
||||||
* VRN/registration is nation-scoped and can be resolved to VIN later.
|
* VehicleIdentification.ID/VIN. A registration/plate is identified separately by
|
||||||
|
* Vehicle.ID/VRN because plates can move between vehicles over time.
|
||||||
*
|
*
|
||||||
* Organisation assignment is intentionally not stored on the event. Vehicle ↔
|
* Organisation assignment is intentionally not stored on the event. Vehicle and
|
||||||
* organisation relation belongs to master data and can be resolved by
|
* registration relations belong to master data and can be resolved historically.
|
||||||
* sourceEntityId/VIN/VRN + occurredAt when needed.
|
|
||||||
*/
|
*/
|
||||||
public record VehicleRefDto(
|
public record VehicleRefDto(
|
||||||
String sourceEntityId,
|
String sourceVehicleEntityId,
|
||||||
String vin,
|
String vin,
|
||||||
@Valid VehicleRegistrationRefDto vehicleRegistration
|
String sourceRegistrationEntityId,
|
||||||
|
@Valid at.procon.eventhub.dto.VehicleRegistrationRefDto vehicleRegistration
|
||||||
) {
|
) {
|
||||||
|
public VehicleRefDto(
|
||||||
|
String sourceEntityId,
|
||||||
|
String vin,
|
||||||
|
at.procon.eventhub.dto.VehicleRegistrationRefDto vehicleRegistration
|
||||||
|
) {
|
||||||
|
this(sourceEntityId, vin, null, vehicleRegistration);
|
||||||
|
}
|
||||||
|
|
||||||
public VehicleRefDto {
|
public VehicleRefDto {
|
||||||
sourceEntityId = normalizeNullable(sourceEntityId);
|
sourceVehicleEntityId = normalizeNullable(sourceVehicleEntityId);
|
||||||
vin = normalizeVin(vin);
|
vin = normalizeVin(vin);
|
||||||
|
sourceRegistrationEntityId = normalizeNullable(sourceRegistrationEntityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean hasAnyReference() {
|
public boolean hasAnyReference() {
|
||||||
return (sourceEntityId != null && !sourceEntityId.isBlank())
|
return (sourceVehicleEntityId != null && !sourceVehicleEntityId.isBlank())
|
||||||
|| (vin != null && !vin.isBlank())
|
|| (vin != null && !vin.isBlank())
|
||||||
|
|| (sourceRegistrationEntityId != null && !sourceRegistrationEntityId.isBlank())
|
||||||
|| (vehicleRegistration != null && vehicleRegistration.hasValue());
|
|| (vehicleRegistration != null && vehicleRegistration.hasValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
public String stableKey() {
|
public String stableKey() {
|
||||||
String registrationKey = vehicleRegistration == null ? "" : vehicleRegistration.stableKey();
|
String registrationKey = vehicleRegistration == null ? "" : vehicleRegistration.stableKey();
|
||||||
return (sourceEntityId == null ? "" : sourceEntityId) + "|"
|
return (sourceVehicleEntityId == null ? "" : sourceVehicleEntityId) + "|"
|
||||||
+ (vin == null ? "" : vin) + "|"
|
+ (vin == null ? "" : vin) + "|"
|
||||||
|
+ (sourceRegistrationEntityId == null ? "" : sourceRegistrationEntityId) + "|"
|
||||||
+ registrationKey;
|
+ registrationKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backward-compatible accessor for older code paths that treated vehicle and
|
||||||
|
* registration source IDs as one field.
|
||||||
|
*/
|
||||||
|
public String sourceEntityId() {
|
||||||
|
return sourceVehicleEntityId != null ? sourceVehicleEntityId : sourceRegistrationEntityId;
|
||||||
|
}
|
||||||
|
|
||||||
private static String normalizeNullable(String value) {
|
private static String normalizeNullable(String value) {
|
||||||
return value == null || value.isBlank() ? null : value.trim();
|
return value == null || value.isBlank() ? null : value.trim();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -109,13 +109,17 @@ public abstract class AbstractJdbcExtractionBatchExecutor<R extends ImportRunReq
|
||||||
|
|
||||||
protected Map<String, Object> parameters(R request, ImportScopeDto scope, ImportCursorStateDto cursor) {
|
protected Map<String, Object> parameters(R request, ImportScopeDto scope, ImportCursorStateDto cursor) {
|
||||||
Map<String, Object> params = new HashMap<>();
|
Map<String, Object> params = new HashMap<>();
|
||||||
|
String organisationId = scope == null || scope.rootSourceOrganisation() == null
|
||||||
|
? null
|
||||||
|
: scope.rootSourceOrganisation().sourceEntityId();
|
||||||
params.put("tenantKey", request.tenantKey());
|
params.put("tenantKey", request.tenantKey());
|
||||||
params.put("occurredFrom", scope == null ? null : scope.occurredFrom());
|
params.put("occurredFrom", scope == null ? null : scope.occurredFrom());
|
||||||
params.put("occurredTo", scope == null ? null : scope.occurredTo());
|
params.put("occurredTo", scope == null ? null : scope.occurredTo());
|
||||||
params.put("rootOrganisationId", scope == null || scope.rootSourceOrganisation() == null ? null : scope.rootSourceOrganisation().sourceEntityId());
|
params.put("organisationId", organisationId);
|
||||||
params.put("includeChildren", scope != null && scope.includeChildren());
|
params.put("includeChildren", scope != null && scope.includeChildren());
|
||||||
params.put("lastSourcePackageImportedAt", cursor == null ? null : cursor.lastSourcePackageImportedAt());
|
params.put("lastSourcePackageImportedAt", cursor == null ? null : cursor.lastSourcePackageImportedAt());
|
||||||
params.put("lastSourcePackageId", cursor == null ? null : cursor.lastSourcePackageId());
|
params.put("lastSourcePackageId", cursor == null ? null : cursor.lastSourcePackageId());
|
||||||
|
params.put("lastSourcePackageIdNumeric", parseLong(cursor == null ? null : cursor.lastSourcePackageId()));
|
||||||
params.put("lastSourceRowUpdatedAt", cursor == null ? null : cursor.lastSourceRowUpdatedAt());
|
params.put("lastSourceRowUpdatedAt", cursor == null ? null : cursor.lastSourceRowUpdatedAt());
|
||||||
params.put("lastOccurredTo", cursor == null ? null : cursor.lastOccurredTo());
|
params.put("lastOccurredTo", cursor == null ? null : cursor.lastOccurredTo());
|
||||||
return params;
|
return params;
|
||||||
|
|
@ -200,4 +204,15 @@ public abstract class AbstractJdbcExtractionBatchExecutor<R extends ImportRunReq
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Long parseLong(String value) {
|
||||||
|
if (value == null || value.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return Long.parseLong(value.trim());
|
||||||
|
} catch (NumberFormatException ignored) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,12 @@
|
||||||
package at.procon.eventhub.importing.extraction;
|
package at.procon.eventhub.importing.extraction;
|
||||||
|
|
||||||
import at.procon.eventhub.importing.ImportRunRequest;
|
import at.procon.eventhub.importing.ImportRunRequest;
|
||||||
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
@ -21,6 +23,16 @@ public class ExtractionDefinitionRegistry<R extends ImportRunRequest> {
|
||||||
return Optional.ofNullable(definitionsByCode.get(normalize(code)));
|
return Optional.ofNullable(definitionsByCode.get(normalize(code)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<String> supportedCodes() {
|
||||||
|
return definitionsByCode.keySet();
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<ExtractionDefinition<R>> definitions() {
|
||||||
|
return definitionsByCode.values().stream()
|
||||||
|
.sorted(Comparator.comparing(ExtractionDefinition::code))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
private String normalize(String value) {
|
private String normalize(String value) {
|
||||||
return value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
|
return value == null ? "" : value.trim().toUpperCase(Locale.ROOT);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,12 +7,21 @@ import at.procon.eventhub.dto.ImportScopeDto;
|
||||||
import at.procon.eventhub.dto.SourceGroupRefDto;
|
import at.procon.eventhub.dto.SourceGroupRefDto;
|
||||||
import at.procon.eventhub.dto.SourcePackageRefDto;
|
import at.procon.eventhub.dto.SourcePackageRefDto;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.lang.reflect.Array;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.temporal.TemporalAccessor;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
import org.springframework.transaction.annotation.Propagation;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public class DataPackageRepository {
|
public class DataPackageRepository {
|
||||||
|
|
@ -202,6 +211,7 @@ public class DataPackageRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(propagation = Propagation.REQUIRES_NEW)
|
||||||
public void markFailed(UUID packageId, String errorMessage) {
|
public void markFailed(UUID packageId, String errorMessage) {
|
||||||
jdbcTemplate.update(
|
jdbcTemplate.update(
|
||||||
"""
|
"""
|
||||||
|
|
@ -217,9 +227,68 @@ public class DataPackageRepository {
|
||||||
|
|
||||||
private String toJson(Map<String, Object> value) {
|
private String toJson(Map<String, Object> value) {
|
||||||
try {
|
try {
|
||||||
return objectMapper.writeValueAsString(value == null ? Map.of() : value);
|
return objectMapper.writeValueAsString(normalizeMetadataMap(value));
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
throw new IllegalArgumentException("Cannot serialize package metadata", e);
|
throw new IllegalArgumentException("Cannot serialize package metadata", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> normalizeMetadataMap(Map<String, Object> value) {
|
||||||
|
if (value == null || value.isEmpty()) {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
Map<String, Object> normalized = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<String, Object> entry : value.entrySet()) {
|
||||||
|
normalized.put(entry.getKey(), normalizeMetadataValue(entry.getValue()));
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object normalizeMetadataValue(Object value) {
|
||||||
|
if (value == null
|
||||||
|
|| value instanceof String
|
||||||
|
|| value instanceof Number
|
||||||
|
|| value instanceof Boolean
|
||||||
|
|| value instanceof JsonNode) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value instanceof CharSequence charSequence) {
|
||||||
|
return charSequence.toString();
|
||||||
|
}
|
||||||
|
if (value instanceof Enum<?> enumValue) {
|
||||||
|
return enumValue.name();
|
||||||
|
}
|
||||||
|
if (value instanceof UUID uuid) {
|
||||||
|
return uuid.toString();
|
||||||
|
}
|
||||||
|
if (value instanceof TemporalAccessor temporalValue) {
|
||||||
|
return temporalValue.toString();
|
||||||
|
}
|
||||||
|
if (value instanceof Date dateValue) {
|
||||||
|
return dateValue.toInstant().toString();
|
||||||
|
}
|
||||||
|
if (value instanceof Map<?, ?> mapValue) {
|
||||||
|
Map<String, Object> nested = new LinkedHashMap<>();
|
||||||
|
for (Map.Entry<?, ?> entry : mapValue.entrySet()) {
|
||||||
|
nested.put(String.valueOf(entry.getKey()), normalizeMetadataValue(entry.getValue()));
|
||||||
|
}
|
||||||
|
return nested;
|
||||||
|
}
|
||||||
|
if (value instanceof Iterable<?> iterable) {
|
||||||
|
List<Object> nested = new ArrayList<>();
|
||||||
|
for (Object item : iterable) {
|
||||||
|
nested.add(normalizeMetadataValue(item));
|
||||||
|
}
|
||||||
|
return nested;
|
||||||
|
}
|
||||||
|
if (value.getClass().isArray()) {
|
||||||
|
int length = Array.getLength(value);
|
||||||
|
List<Object> nested = new ArrayList<>(length);
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
nested.add(normalizeMetadataValue(Array.get(value, i)));
|
||||||
|
}
|
||||||
|
return nested;
|
||||||
|
}
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,7 @@ import at.procon.eventhub.dto.DriverCardRefDto;
|
||||||
import at.procon.eventhub.dto.DriverRefDto;
|
import at.procon.eventhub.dto.DriverRefDto;
|
||||||
import at.procon.eventhub.dto.EventHubEventDto;
|
import at.procon.eventhub.dto.EventHubEventDto;
|
||||||
import at.procon.eventhub.dto.SourcePackageRefDto;
|
import at.procon.eventhub.dto.SourcePackageRefDto;
|
||||||
import at.procon.eventhub.dto.VehicleRefDto;
|
import at.procon.eventhub.persistence.VehicleIdentityRepository.ResolvedVehicleReference;
|
||||||
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
|
|
||||||
import at.procon.eventhub.service.EventAcquisitionRecordKeyService;
|
import at.procon.eventhub.service.EventAcquisitionRecordKeyService;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
import com.fasterxml.jackson.databind.JsonNode;
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
|
|
@ -26,17 +25,20 @@ public class EventRepository {
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final EventAcquisitionRecordKeyService recordKeyService;
|
private final EventAcquisitionRecordKeyService recordKeyService;
|
||||||
private final SourceMasterDataRepository sourceMasterDataRepository;
|
private final SourceMasterDataRepository sourceMasterDataRepository;
|
||||||
|
private final VehicleIdentityRepository vehicleIdentityRepository;
|
||||||
|
|
||||||
public EventRepository(
|
public EventRepository(
|
||||||
JdbcTemplate jdbcTemplate,
|
JdbcTemplate jdbcTemplate,
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
EventAcquisitionRecordKeyService recordKeyService,
|
EventAcquisitionRecordKeyService recordKeyService,
|
||||||
SourceMasterDataRepository sourceMasterDataRepository
|
SourceMasterDataRepository sourceMasterDataRepository,
|
||||||
|
VehicleIdentityRepository vehicleIdentityRepository
|
||||||
) {
|
) {
|
||||||
this.jdbcTemplate = jdbcTemplate;
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.recordKeyService = recordKeyService;
|
this.recordKeyService = recordKeyService;
|
||||||
this.sourceMasterDataRepository = sourceMasterDataRepository;
|
this.sourceMasterDataRepository = sourceMasterDataRepository;
|
||||||
|
this.vehicleIdentityRepository = vehicleIdentityRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -68,6 +70,8 @@ public class EventRepository {
|
||||||
UUID requestedEventId = event.eventId() == null ? UUID.randomUUID() : event.eventId();
|
UUID requestedEventId = event.eventId() == null ? UUID.randomUUID() : event.eventId();
|
||||||
OffsetDateTime receivedHubAt = event.receivedHubAt() == null ? OffsetDateTime.now() : event.receivedHubAt();
|
OffsetDateTime receivedHubAt = event.receivedHubAt() == null ? OffsetDateTime.now() : event.receivedHubAt();
|
||||||
String sourceRecordKeyHash = recordKeyService.buildSourceRecordKeyHash(event, eventSourceId);
|
String sourceRecordKeyHash = recordKeyService.buildSourceRecordKeyHash(event, eventSourceId);
|
||||||
|
var longitude = event.position() == null ? null : event.position().longitude();
|
||||||
|
var latitude = event.position() == null ? null : event.position().latitude();
|
||||||
|
|
||||||
return jdbcTemplate.query(
|
return jdbcTemplate.query(
|
||||||
"""
|
"""
|
||||||
|
|
@ -75,7 +79,7 @@ public class EventRepository {
|
||||||
insert into eventhub.event(
|
insert into eventhub.event(
|
||||||
id, event_source_id, data_package_id,
|
id, event_source_id, data_package_id,
|
||||||
external_source_event_id,
|
external_source_event_id,
|
||||||
driver_entity_id, vehicle_entity_id, source_package_entity_id,
|
driver_entity_id, vehicle_id, vehicle_registration_id, source_package_entity_id,
|
||||||
occurred_at, received_partner_at, received_hub_at,
|
occurred_at, received_partner_at, received_hub_at,
|
||||||
event_domain, event_type, lifecycle,
|
event_domain, event_type, lifecycle,
|
||||||
odometer_m, position,
|
odometer_m, position,
|
||||||
|
|
@ -84,12 +88,12 @@ public class EventRepository {
|
||||||
) values (
|
) values (
|
||||||
?, ?, ?,
|
?, ?, ?,
|
||||||
?,
|
?,
|
||||||
?, ?, ?,
|
?, ?, ?, ?,
|
||||||
?, ?, ?,
|
?, ?, ?,
|
||||||
?, ?, ?,
|
?, ?, ?,
|
||||||
?, case
|
?, case
|
||||||
when ? is null or ? is null then null
|
when ?::double precision is null or ?::double precision is null then null
|
||||||
else ST_SetSRID(ST_MakePoint(?, ?), 4326)::geography
|
else ST_SetSRID(ST_MakePoint(?::double precision, ?::double precision), 4326)::geography
|
||||||
end,
|
end,
|
||||||
?::jsonb, ?,
|
?::jsonb, ?,
|
||||||
?, ?
|
?, ?
|
||||||
|
|
@ -120,7 +124,8 @@ public class EventRepository {
|
||||||
packageId,
|
packageId,
|
||||||
event.externalSourceEventId(),
|
event.externalSourceEventId(),
|
||||||
refs.driverEntityId(),
|
refs.driverEntityId(),
|
||||||
refs.vehicleEntityId(),
|
refs.vehicleId(),
|
||||||
|
refs.vehicleRegistrationId(),
|
||||||
refs.sourcePackageEntityId(),
|
refs.sourcePackageEntityId(),
|
||||||
event.occurredAt(),
|
event.occurredAt(),
|
||||||
event.receivedPartnerAt(),
|
event.receivedPartnerAt(),
|
||||||
|
|
@ -129,10 +134,10 @@ public class EventRepository {
|
||||||
event.eventType().name(),
|
event.eventType().name(),
|
||||||
event.lifecycle().name(),
|
event.lifecycle().name(),
|
||||||
event.odometerM(),
|
event.odometerM(),
|
||||||
event.position() == null ? null : event.position().longitude(),
|
longitude,
|
||||||
event.position() == null ? null : event.position().latitude(),
|
latitude,
|
||||||
event.position() == null ? null : event.position().longitude(),
|
longitude,
|
||||||
event.position() == null ? null : event.position().latitude(),
|
latitude,
|
||||||
toJson(event.payload()),
|
toJson(event.payload()),
|
||||||
event.manualEntry(),
|
event.manualEntry(),
|
||||||
sourceRecordKeyHash,
|
sourceRecordKeyHash,
|
||||||
|
|
@ -168,9 +173,9 @@ public class EventRepository {
|
||||||
Map<String, UUID> entityIdCache
|
Map<String, UUID> entityIdCache
|
||||||
) {
|
) {
|
||||||
UUID driverEntityId = resolveDriverEntityId(tenantKey, eventSourceId, event, entityIdCache);
|
UUID driverEntityId = resolveDriverEntityId(tenantKey, eventSourceId, event, entityIdCache);
|
||||||
UUID vehicleEntityId = resolveVehicleEntityId(tenantKey, eventSourceId, event, entityIdCache);
|
ResolvedVehicleReference vehicleRef = resolveVehicleReference(tenantKey, eventSourceId, event);
|
||||||
UUID sourcePackageEntityId = resolveSourcePackageEntityId(tenantKey, eventSourceId, event, entityIdCache);
|
UUID sourcePackageEntityId = resolveSourcePackageEntityId(tenantKey, eventSourceId, event, entityIdCache);
|
||||||
return new ResolvedEntityRefs(driverEntityId, vehicleEntityId, sourcePackageEntityId);
|
return new ResolvedEntityRefs(driverEntityId, vehicleRef.vehicleId(), vehicleRef.vehicleRegistrationId(), sourcePackageEntityId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private UUID resolveDriverEntityId(
|
private UUID resolveDriverEntityId(
|
||||||
|
|
@ -211,45 +216,16 @@ public class EventRepository {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private UUID resolveVehicleEntityId(
|
private ResolvedVehicleReference resolveVehicleReference(
|
||||||
String tenantKey,
|
String tenantKey,
|
||||||
int eventSourceId,
|
int eventSourceId,
|
||||||
EventHubEventDto event,
|
EventHubEventDto event
|
||||||
Map<String, UUID> entityIdCache
|
|
||||||
) {
|
) {
|
||||||
VehicleRefDto vehicleRef = event.vehicleRef();
|
return vehicleIdentityRepository.resolveOrCreateVehicleReference(
|
||||||
if (vehicleRef == null || !vehicleRef.hasAnyReference()) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
VehicleRegistrationRefDto registration = vehicleRef.vehicleRegistration();
|
|
||||||
String registrationKey = registration == null || !registration.hasValue() ? null : registration.stableKey();
|
|
||||||
String sourceEntityId = normalizeNullable(vehicleRef.sourceEntityId());
|
|
||||||
if (sourceEntityId == null && normalizeNullable(vehicleRef.vin()) != null) {
|
|
||||||
sourceEntityId = "VIN:" + vehicleRef.vin();
|
|
||||||
}
|
|
||||||
if (sourceEntityId == null && registrationKey != null) {
|
|
||||||
sourceEntityId = "VRN:" + registrationKey;
|
|
||||||
}
|
|
||||||
if (sourceEntityId == null) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<String, Object> payload = new LinkedHashMap<>();
|
|
||||||
put(payload, "source_entity_id", vehicleRef.sourceEntityId());
|
|
||||||
put(payload, "vin", vehicleRef.vin());
|
|
||||||
put(payload, "vehicle_registration_nation", registration == null ? null : registration.nation());
|
|
||||||
put(payload, "vehicle_registration_number", registration == null ? null : registration.number());
|
|
||||||
return resolveEntityId(
|
|
||||||
tenantKey,
|
tenantKey,
|
||||||
eventSourceId,
|
eventSourceId,
|
||||||
"VEHICLE",
|
event.vehicleRef(),
|
||||||
sourceEntityId,
|
event.occurredAt()
|
||||||
normalizeNullable(vehicleRef.vin()) == null ? registrationKey : vehicleRef.vin(),
|
|
||||||
sourceEntityId,
|
|
||||||
null,
|
|
||||||
payload,
|
|
||||||
entityIdCache
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -357,7 +333,8 @@ public class EventRepository {
|
||||||
|
|
||||||
private record ResolvedEntityRefs(
|
private record ResolvedEntityRefs(
|
||||||
UUID driverEntityId,
|
UUID driverEntityId,
|
||||||
UUID vehicleEntityId,
|
UUID vehicleId,
|
||||||
|
UUID vehicleRegistrationId,
|
||||||
UUID sourcePackageEntityId
|
UUID sourcePackageEntityId
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,21 @@ package at.procon.eventhub.persistence;
|
||||||
import at.procon.eventhub.importing.masterdata.SourceMasterEntityUpsert;
|
import at.procon.eventhub.importing.masterdata.SourceMasterEntityUpsert;
|
||||||
import at.procon.eventhub.importing.masterdata.SourceMasterRelationUpsert;
|
import at.procon.eventhub.importing.masterdata.SourceMasterRelationUpsert;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.JsonNode;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.fasterxml.jackson.databind.node.POJONode;
|
||||||
|
import java.lang.reflect.Array;
|
||||||
import java.sql.PreparedStatement;
|
import java.sql.PreparedStatement;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.temporal.TemporalAccessor;
|
||||||
|
import java.time.temporal.TemporalAmount;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Base64;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
|
@ -180,13 +190,125 @@ public class SourceMasterDataRepository {
|
||||||
}
|
}
|
||||||
|
|
||||||
private String toJson(Map<String, Object> value) {
|
private String toJson(Map<String, Object> value) {
|
||||||
|
Map<String, Object> source = value == null ? Map.of() : value;
|
||||||
try {
|
try {
|
||||||
return objectMapper.writeValueAsString(value == null ? Map.of() : value);
|
return objectMapper.writeValueAsString(normalizeJsonMap(source));
|
||||||
} catch (JsonProcessingException e) {
|
} catch (JsonProcessingException e) {
|
||||||
throw new IllegalArgumentException("Cannot serialize source master data payload", e);
|
throw new IllegalArgumentException(
|
||||||
|
"Cannot serialize source master data payload. payloadTypes=" + describePayloadTypes(source),
|
||||||
|
e
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> normalizeJsonMap(Map<?, ?> source) {
|
||||||
|
Map<String, Object> normalized = new LinkedHashMap<>(source.size());
|
||||||
|
for (Map.Entry<?, ?> entry : source.entrySet()) {
|
||||||
|
if (entry.getKey() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
normalized.put(String.valueOf(entry.getKey()), normalizeJsonValue(entry.getValue()));
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object normalizeJsonValue(Object value) {
|
||||||
|
if (value == null
|
||||||
|
|| value instanceof String
|
||||||
|
|| value instanceof Number
|
||||||
|
|| value instanceof Boolean) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value instanceof JsonNode node) {
|
||||||
|
return normalizeJsonNode(node);
|
||||||
|
}
|
||||||
|
if (value instanceof Enum<?> enumValue) {
|
||||||
|
return enumValue.name();
|
||||||
|
}
|
||||||
|
if (value instanceof UUID uuid) {
|
||||||
|
return uuid.toString();
|
||||||
|
}
|
||||||
|
if (value instanceof byte[] bytes) {
|
||||||
|
return Base64.getEncoder().encodeToString(bytes);
|
||||||
|
}
|
||||||
|
if (value instanceof Timestamp timestamp) {
|
||||||
|
return timestamp.toInstant().toString();
|
||||||
|
}
|
||||||
|
if (value instanceof java.util.Date date) {
|
||||||
|
return date.toInstant().toString();
|
||||||
|
}
|
||||||
|
if (value instanceof TemporalAccessor || value instanceof TemporalAmount) {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
if (value instanceof Map<?, ?> map) {
|
||||||
|
return normalizeJsonMap(map);
|
||||||
|
}
|
||||||
|
if (value instanceof Iterable<?> iterable) {
|
||||||
|
List<Object> values = new ArrayList<>();
|
||||||
|
for (Object entry : iterable) {
|
||||||
|
values.add(normalizeJsonValue(entry));
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
if (value.getClass().isArray()) {
|
||||||
|
int length = Array.getLength(value);
|
||||||
|
List<Object> values = new ArrayList<>(length);
|
||||||
|
for (int i = 0; i < length; i++) {
|
||||||
|
values.add(normalizeJsonValue(Array.get(value, i)));
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
return String.valueOf(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object normalizeJsonNode(JsonNode node) {
|
||||||
|
if (node == null || node.isNull() || node.isMissingNode()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (node instanceof POJONode pojoNode) {
|
||||||
|
return normalizeJsonValue(pojoNode.getPojo());
|
||||||
|
}
|
||||||
|
if (node.isObject()) {
|
||||||
|
return normalizeJsonMap(objectNodeToMap(node));
|
||||||
|
}
|
||||||
|
if (node.isArray()) {
|
||||||
|
List<Object> values = new ArrayList<>();
|
||||||
|
for (JsonNode child : node) {
|
||||||
|
values.add(normalizeJsonNode(child));
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
if (node.isTextual()) {
|
||||||
|
return node.textValue();
|
||||||
|
}
|
||||||
|
if (node.isNumber()) {
|
||||||
|
return node.numberValue();
|
||||||
|
}
|
||||||
|
if (node.isBoolean()) {
|
||||||
|
return node.booleanValue();
|
||||||
|
}
|
||||||
|
if (node.isBinary()) {
|
||||||
|
return node.asText();
|
||||||
|
}
|
||||||
|
return node.asText();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Map<String, Object> objectNodeToMap(JsonNode node) {
|
||||||
|
Map<String, Object> values = new LinkedHashMap<>();
|
||||||
|
node.fields().forEachRemaining(entry -> values.put(entry.getKey(), entry.getValue()));
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String describePayloadTypes(Map<String, Object> payload) {
|
||||||
|
if (payload.isEmpty()) {
|
||||||
|
return "{}";
|
||||||
|
}
|
||||||
|
return payload.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(entry -> entry.getKey() + "=" + (entry.getValue() == null ? "null" : entry.getValue().getClass().getName()))
|
||||||
|
.collect(Collectors.joining(", ", "{", "}"));
|
||||||
|
}
|
||||||
|
|
||||||
private String normalizeRequired(String value, String fieldName) {
|
private String normalizeRequired(String value, String fieldName) {
|
||||||
String normalized = normalizeNullable(value);
|
String normalized = normalizeNullable(value);
|
||||||
if (normalized == null) {
|
if (normalized == null) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,596 @@
|
||||||
|
package at.procon.eventhub.persistence;
|
||||||
|
|
||||||
|
import at.procon.eventhub.dto.VehicleRefDto;
|
||||||
|
import at.procon.eventhub.dto.VehicleRegistrationRefDto;
|
||||||
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.sql.Timestamp;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
public class VehicleIdentityRepository {
|
||||||
|
|
||||||
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
public VehicleIdentityRepository(JdbcTemplate jdbcTemplate, ObjectMapper objectMapper) {
|
||||||
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
|
this.objectMapper = objectMapper;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ResolvedVehicleReference resolveOrCreateVehicleReference(
|
||||||
|
String tenantKey,
|
||||||
|
int eventSourceId,
|
||||||
|
VehicleRefDto vehicleRef,
|
||||||
|
OffsetDateTime occurredAt
|
||||||
|
) {
|
||||||
|
if (vehicleRef == null || !vehicleRef.hasAnyReference()) {
|
||||||
|
return ResolvedVehicleReference.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
String normalizedTenantKey = normalizeRequired(tenantKey, "tenantKey");
|
||||||
|
String sourceVehicleEntityId = normalizeNullable(vehicleRef.sourceVehicleEntityId());
|
||||||
|
String vin = normalizeNullable(vehicleRef.vin());
|
||||||
|
String sourceRegistrationEntityId = normalizeNullable(vehicleRef.sourceRegistrationEntityId());
|
||||||
|
VehicleRegistrationRefDto registration = vehicleRef.vehicleRegistration();
|
||||||
|
String registrationNation = registration == null ? null : normalizeNullable(registration.nation());
|
||||||
|
String registrationNumber = registration == null ? null : normalizeNullable(registration.number());
|
||||||
|
|
||||||
|
UUID registrationId = resolveRegistrationId(
|
||||||
|
normalizedTenantKey,
|
||||||
|
eventSourceId,
|
||||||
|
sourceRegistrationEntityId,
|
||||||
|
registrationNation,
|
||||||
|
registrationNumber,
|
||||||
|
occurredAt
|
||||||
|
);
|
||||||
|
if (registrationId == null && (sourceRegistrationEntityId != null || registrationNumber != null)) {
|
||||||
|
registrationId = createRegistration(
|
||||||
|
normalizedTenantKey,
|
||||||
|
eventSourceId,
|
||||||
|
sourceRegistrationEntityId,
|
||||||
|
registrationNation,
|
||||||
|
registrationNumber,
|
||||||
|
null,
|
||||||
|
Map.of("source", "event")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
UUID vehicleId = resolveVehicleId(
|
||||||
|
normalizedTenantKey,
|
||||||
|
eventSourceId,
|
||||||
|
sourceVehicleEntityId,
|
||||||
|
vin
|
||||||
|
);
|
||||||
|
if (vehicleId == null && registrationId != null) {
|
||||||
|
vehicleId = resolveAssignedVehicleId(registrationId, occurredAt);
|
||||||
|
}
|
||||||
|
if (vehicleId == null && (sourceVehicleEntityId != null || vin != null)) {
|
||||||
|
vehicleId = createVehicle(
|
||||||
|
normalizedTenantKey,
|
||||||
|
eventSourceId,
|
||||||
|
sourceVehicleEntityId,
|
||||||
|
vin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vehicleId != null) {
|
||||||
|
touchVehicle(vehicleId, sourceVehicleEntityId, vin);
|
||||||
|
}
|
||||||
|
if (registrationId != null) {
|
||||||
|
touchRegistration(registrationId, sourceRegistrationEntityId, registrationNation, registrationNumber);
|
||||||
|
}
|
||||||
|
return new ResolvedVehicleReference(vehicleId, registrationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int reconcileFromMasterData(String tenantKey, int eventSourceId) {
|
||||||
|
String normalizedTenantKey = normalizeRequired(tenantKey, "tenantKey");
|
||||||
|
int updates = 0;
|
||||||
|
|
||||||
|
List<VehicleMasterRow> vehicles = jdbcTemplate.query(
|
||||||
|
compatibleSourcesCte() + """
|
||||||
|
select event_source_id, source_entity_id, source_external_key, source_updated_at
|
||||||
|
from eventhub.source_master_entity
|
||||||
|
where tenant_key = ?
|
||||||
|
and event_source_id in (select id from compatible_sources)
|
||||||
|
and entity_type = 'VEHICLE'
|
||||||
|
""",
|
||||||
|
(rs, rowNum) -> new VehicleMasterRow(
|
||||||
|
rs.getInt("event_source_id"),
|
||||||
|
normalizeNullable(rs.getString("source_entity_id")),
|
||||||
|
normalizeNullable(rs.getString("source_external_key")),
|
||||||
|
offsetDateTime(rs.getObject("source_updated_at"))
|
||||||
|
),
|
||||||
|
eventSourceId,
|
||||||
|
normalizedTenantKey
|
||||||
|
);
|
||||||
|
for (VehicleMasterRow vehicle : vehicles) {
|
||||||
|
if (vehicle.sourceVehicleEntityId() == null && vehicle.vin() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
UUID vehicleId = resolveVehicleId(normalizedTenantKey, vehicle.eventSourceId(), vehicle.sourceVehicleEntityId(), vehicle.vin());
|
||||||
|
if (vehicleId == null) {
|
||||||
|
createVehicle(normalizedTenantKey, vehicle.eventSourceId(), vehicle.sourceVehicleEntityId(), vehicle.vin());
|
||||||
|
} else {
|
||||||
|
touchVehicle(vehicleId, vehicle.sourceVehicleEntityId(), vehicle.vin());
|
||||||
|
}
|
||||||
|
updates++;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<RegistrationMasterRow> registrations = jdbcTemplate.query(
|
||||||
|
compatibleSourcesCte() + """
|
||||||
|
select event_source_id,
|
||||||
|
source_entity_id,
|
||||||
|
source_external_key,
|
||||||
|
source_updated_at,
|
||||||
|
payload ->> 'registration_nation' as registration_nation,
|
||||||
|
payload ->> 'registration_number' as registration_number
|
||||||
|
from eventhub.source_master_entity
|
||||||
|
where tenant_key = ?
|
||||||
|
and event_source_id in (select id from compatible_sources)
|
||||||
|
and entity_type = 'VEHICLE_REGISTRATION'
|
||||||
|
""",
|
||||||
|
(rs, rowNum) -> registrationMasterRow(
|
||||||
|
rs.getInt("event_source_id"),
|
||||||
|
normalizeNullable(rs.getString("source_entity_id")),
|
||||||
|
normalizeNullable(rs.getString("source_external_key")),
|
||||||
|
normalizeNullable(rs.getString("registration_nation")),
|
||||||
|
normalizeNullable(rs.getString("registration_number")),
|
||||||
|
offsetDateTime(rs.getObject("source_updated_at"))
|
||||||
|
),
|
||||||
|
eventSourceId,
|
||||||
|
normalizedTenantKey
|
||||||
|
);
|
||||||
|
for (RegistrationMasterRow registration : registrations) {
|
||||||
|
if (registration.nation() == null || registration.registrationNumber() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
UUID registrationId = resolveRegistrationId(
|
||||||
|
normalizedTenantKey,
|
||||||
|
registration.eventSourceId(),
|
||||||
|
registration.sourceRegistrationEntityId(),
|
||||||
|
registration.nation(),
|
||||||
|
registration.registrationNumber(),
|
||||||
|
null
|
||||||
|
);
|
||||||
|
if (registrationId == null) {
|
||||||
|
createRegistration(
|
||||||
|
normalizedTenantKey,
|
||||||
|
registration.eventSourceId(),
|
||||||
|
registration.sourceRegistrationEntityId(),
|
||||||
|
registration.nation(),
|
||||||
|
registration.registrationNumber(),
|
||||||
|
registration.sourceUpdatedAt(),
|
||||||
|
Map.of("source", "master-data")
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
updateRegistrationFromMasterData(
|
||||||
|
registrationId,
|
||||||
|
registration.sourceRegistrationEntityId(),
|
||||||
|
registration.nation(),
|
||||||
|
registration.registrationNumber(),
|
||||||
|
registration.sourceUpdatedAt()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
updates++;
|
||||||
|
}
|
||||||
|
|
||||||
|
updates += projectVehicleRegistrationAssignments(normalizedTenantKey, eventSourceId);
|
||||||
|
|
||||||
|
return updates;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID resolveVehicleId(
|
||||||
|
String tenantKey,
|
||||||
|
int eventSourceId,
|
||||||
|
String sourceVehicleEntityId,
|
||||||
|
String vin
|
||||||
|
) {
|
||||||
|
UUID vehicleId = findVehicleBySourceVehicleEntityId(tenantKey, eventSourceId, sourceVehicleEntityId);
|
||||||
|
if (vehicleId == null) {
|
||||||
|
vehicleId = findVehicleByVin(tenantKey, eventSourceId, vin);
|
||||||
|
}
|
||||||
|
return vehicleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID resolveRegistrationId(
|
||||||
|
String tenantKey,
|
||||||
|
int eventSourceId,
|
||||||
|
String sourceRegistrationEntityId,
|
||||||
|
String nation,
|
||||||
|
String registrationNumber,
|
||||||
|
OffsetDateTime occurredAt
|
||||||
|
) {
|
||||||
|
UUID registrationId = findRegistrationBySourceRegistrationEntityId(tenantKey, eventSourceId, sourceRegistrationEntityId, occurredAt);
|
||||||
|
if (registrationId == null) {
|
||||||
|
registrationId = findRegistrationByPlate(tenantKey, eventSourceId, nation, registrationNumber, occurredAt);
|
||||||
|
}
|
||||||
|
return registrationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID findVehicleBySourceVehicleEntityId(String tenantKey, int eventSourceId, String sourceVehicleEntityId) {
|
||||||
|
if (sourceVehicleEntityId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return jdbcTemplate.query(
|
||||||
|
compatibleSourcesCte() + """
|
||||||
|
select v.id
|
||||||
|
from eventhub.vehicle v
|
||||||
|
where v.tenant_key = ?
|
||||||
|
and v.event_source_id in (select id from compatible_sources)
|
||||||
|
and v.source_vehicle_entity_id = ?
|
||||||
|
order by v.updated_at desc
|
||||||
|
limit 1
|
||||||
|
""",
|
||||||
|
rs -> rs.next() ? (UUID) rs.getObject("id") : null,
|
||||||
|
eventSourceId,
|
||||||
|
tenantKey,
|
||||||
|
sourceVehicleEntityId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID findVehicleByVin(String tenantKey, int eventSourceId, String vin) {
|
||||||
|
if (vin == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return jdbcTemplate.query(
|
||||||
|
compatibleSourcesCte() + """
|
||||||
|
select v.id
|
||||||
|
from eventhub.vehicle v
|
||||||
|
where v.tenant_key = ?
|
||||||
|
and v.event_source_id in (select id from compatible_sources)
|
||||||
|
and v.vin = ?
|
||||||
|
order by v.updated_at desc
|
||||||
|
limit 1
|
||||||
|
""",
|
||||||
|
rs -> rs.next() ? (UUID) rs.getObject("id") : null,
|
||||||
|
eventSourceId,
|
||||||
|
tenantKey,
|
||||||
|
vin
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID findRegistrationBySourceRegistrationEntityId(
|
||||||
|
String tenantKey,
|
||||||
|
int eventSourceId,
|
||||||
|
String sourceRegistrationEntityId,
|
||||||
|
OffsetDateTime occurredAt
|
||||||
|
) {
|
||||||
|
if (sourceRegistrationEntityId == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return jdbcTemplate.query(
|
||||||
|
compatibleSourcesCte() + """
|
||||||
|
select r.id
|
||||||
|
from eventhub.vehicle_registration r
|
||||||
|
where r.tenant_key = ?
|
||||||
|
and r.event_source_id in (select id from compatible_sources)
|
||||||
|
and r.source_registration_entity_id = ?
|
||||||
|
order by r.updated_at desc
|
||||||
|
limit 1
|
||||||
|
""",
|
||||||
|
rs -> rs.next() ? (UUID) rs.getObject("id") : null,
|
||||||
|
eventSourceId,
|
||||||
|
tenantKey,
|
||||||
|
sourceRegistrationEntityId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID findRegistrationByPlate(
|
||||||
|
String tenantKey,
|
||||||
|
int eventSourceId,
|
||||||
|
String nation,
|
||||||
|
String registrationNumber,
|
||||||
|
OffsetDateTime occurredAt
|
||||||
|
) {
|
||||||
|
if (nation == null || registrationNumber == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return jdbcTemplate.query(
|
||||||
|
compatibleSourcesCte() + """
|
||||||
|
select r.id
|
||||||
|
from eventhub.vehicle_registration r
|
||||||
|
where r.tenant_key = ?
|
||||||
|
and r.event_source_id in (select id from compatible_sources)
|
||||||
|
and r.nation = ?
|
||||||
|
and r.registration_number = ?
|
||||||
|
order by r.updated_at desc
|
||||||
|
limit 1
|
||||||
|
""",
|
||||||
|
rs -> rs.next() ? (UUID) rs.getObject("id") : null,
|
||||||
|
eventSourceId,
|
||||||
|
tenantKey,
|
||||||
|
nation,
|
||||||
|
registrationNumber
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID resolveAssignedVehicleId(UUID registrationId, OffsetDateTime occurredAt) {
|
||||||
|
return jdbcTemplate.query(
|
||||||
|
"""
|
||||||
|
select vehicle_id
|
||||||
|
from eventhub.vehicle_registration_assignment
|
||||||
|
where vehicle_registration_id = ?
|
||||||
|
and (? is null or valid_from is null or valid_from <= ?)
|
||||||
|
and (? is null or valid_to is null or ? < valid_to)
|
||||||
|
order by valid_from desc nulls last, updated_at desc
|
||||||
|
limit 1
|
||||||
|
""",
|
||||||
|
rs -> rs.next() ? (UUID) rs.getObject("vehicle_id") : null,
|
||||||
|
registrationId,
|
||||||
|
occurredAt,
|
||||||
|
occurredAt,
|
||||||
|
occurredAt,
|
||||||
|
occurredAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID createVehicle(
|
||||||
|
String tenantKey,
|
||||||
|
int eventSourceId,
|
||||||
|
String sourceVehicleEntityId,
|
||||||
|
String vin
|
||||||
|
) {
|
||||||
|
UUID vehicleId = UUID.randomUUID();
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"""
|
||||||
|
insert into eventhub.vehicle(id, tenant_key, event_source_id, source_vehicle_entity_id, vin, updated_at)
|
||||||
|
values (?, ?, ?, ?, ?, now())
|
||||||
|
""",
|
||||||
|
vehicleId,
|
||||||
|
tenantKey,
|
||||||
|
eventSourceId,
|
||||||
|
sourceVehicleEntityId,
|
||||||
|
vin
|
||||||
|
);
|
||||||
|
return vehicleId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UUID createRegistration(
|
||||||
|
String tenantKey,
|
||||||
|
int eventSourceId,
|
||||||
|
String sourceRegistrationEntityId,
|
||||||
|
String nation,
|
||||||
|
String registrationNumber,
|
||||||
|
OffsetDateTime sourceUpdatedAt,
|
||||||
|
Map<String, Object> payload
|
||||||
|
) {
|
||||||
|
if (nation == null || registrationNumber == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
UUID registrationId = UUID.randomUUID();
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"""
|
||||||
|
insert into eventhub.vehicle_registration(
|
||||||
|
id, tenant_key, event_source_id, source_registration_entity_id, nation, registration_number,
|
||||||
|
source_updated_at, payload, updated_at
|
||||||
|
) values (?, ?, ?, ?, ?, ?, ?, ?::jsonb, now())
|
||||||
|
""",
|
||||||
|
registrationId,
|
||||||
|
tenantKey,
|
||||||
|
eventSourceId,
|
||||||
|
sourceRegistrationEntityId,
|
||||||
|
nation,
|
||||||
|
registrationNumber,
|
||||||
|
sourceUpdatedAt,
|
||||||
|
toJson(payload)
|
||||||
|
);
|
||||||
|
return registrationId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void touchVehicle(UUID vehicleId, String sourceVehicleEntityId, String vin) {
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"""
|
||||||
|
update eventhub.vehicle
|
||||||
|
set source_vehicle_entity_id = coalesce(source_vehicle_entity_id, ?),
|
||||||
|
vin = coalesce(vin, ?),
|
||||||
|
updated_at = now()
|
||||||
|
where id = ?
|
||||||
|
""",
|
||||||
|
sourceVehicleEntityId,
|
||||||
|
vin,
|
||||||
|
vehicleId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private int projectVehicleRegistrationAssignments(String tenantKey, int eventSourceId) {
|
||||||
|
return jdbcTemplate.update(
|
||||||
|
compatibleSourcesCte() + """
|
||||||
|
insert into eventhub.vehicle_registration_assignment(
|
||||||
|
id, tenant_key, event_source_id, vehicle_registration_id, vehicle_id,
|
||||||
|
valid_from, valid_to, source_updated_at, payload, updated_at
|
||||||
|
)
|
||||||
|
select gen_random_uuid(),
|
||||||
|
relation.tenant_key,
|
||||||
|
relation.event_source_id,
|
||||||
|
registration.id,
|
||||||
|
vehicle.id,
|
||||||
|
relation.valid_from,
|
||||||
|
relation.valid_to,
|
||||||
|
relation.source_updated_at,
|
||||||
|
jsonb_build_object(
|
||||||
|
'source', 'master-data',
|
||||||
|
'sourceRelationId', relation.id,
|
||||||
|
'relationKey', relation.relation_key
|
||||||
|
),
|
||||||
|
now()
|
||||||
|
from eventhub.source_master_relation relation
|
||||||
|
join eventhub.vehicle_registration registration on registration.tenant_key = relation.tenant_key
|
||||||
|
and registration.event_source_id = relation.event_source_id
|
||||||
|
and registration.source_registration_entity_id = relation.from_source_entity_id
|
||||||
|
join eventhub.vehicle vehicle on vehicle.tenant_key = relation.tenant_key
|
||||||
|
and vehicle.event_source_id = relation.event_source_id
|
||||||
|
and vehicle.source_vehicle_entity_id = relation.to_source_entity_id
|
||||||
|
where relation.tenant_key = ?
|
||||||
|
and relation.event_source_id in (select id from compatible_sources)
|
||||||
|
and relation.relation_type = 'VEHICLE_REGISTRATION_VEHICLE'
|
||||||
|
and relation.from_entity_type = 'VEHICLE_REGISTRATION'
|
||||||
|
and relation.to_entity_type = 'VEHICLE'
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from eventhub.vehicle_registration_assignment existing
|
||||||
|
where existing.vehicle_registration_id = registration.id
|
||||||
|
and existing.vehicle_id = vehicle.id
|
||||||
|
and existing.valid_from is not distinct from relation.valid_from
|
||||||
|
and existing.valid_to is not distinct from relation.valid_to
|
||||||
|
)
|
||||||
|
""",
|
||||||
|
eventSourceId,
|
||||||
|
tenantKey
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void touchRegistration(UUID registrationId, String sourceRegistrationEntityId, String nation, String registrationNumber) {
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"""
|
||||||
|
update eventhub.vehicle_registration
|
||||||
|
set source_registration_entity_id = coalesce(source_registration_entity_id, ?),
|
||||||
|
nation = coalesce(?, nation),
|
||||||
|
registration_number = coalesce(?, registration_number),
|
||||||
|
updated_at = now()
|
||||||
|
where id = ?
|
||||||
|
""",
|
||||||
|
sourceRegistrationEntityId,
|
||||||
|
nation,
|
||||||
|
registrationNumber,
|
||||||
|
registrationId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateRegistrationFromMasterData(
|
||||||
|
UUID registrationId,
|
||||||
|
String sourceRegistrationEntityId,
|
||||||
|
String nation,
|
||||||
|
String registrationNumber,
|
||||||
|
OffsetDateTime sourceUpdatedAt
|
||||||
|
) {
|
||||||
|
jdbcTemplate.update(
|
||||||
|
"""
|
||||||
|
update eventhub.vehicle_registration
|
||||||
|
set source_registration_entity_id = coalesce(source_registration_entity_id, ?),
|
||||||
|
nation = coalesce(?, nation),
|
||||||
|
registration_number = coalesce(?, registration_number),
|
||||||
|
source_updated_at = ?,
|
||||||
|
updated_at = now()
|
||||||
|
where id = ?
|
||||||
|
""",
|
||||||
|
sourceRegistrationEntityId,
|
||||||
|
nation,
|
||||||
|
registrationNumber,
|
||||||
|
sourceUpdatedAt,
|
||||||
|
registrationId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String compatibleSourcesCte() {
|
||||||
|
return """
|
||||||
|
with source_context as (
|
||||||
|
select tenant_key, provider_key, source_instance_key, coalesce(tenant_provider_setting_key, '') as tenant_provider_setting_key
|
||||||
|
from eventhub.event_source
|
||||||
|
where id = ?
|
||||||
|
),
|
||||||
|
compatible_sources as (
|
||||||
|
select es.id
|
||||||
|
from eventhub.event_source es
|
||||||
|
join source_context ctx on ctx.tenant_key = es.tenant_key
|
||||||
|
and ctx.provider_key = es.provider_key
|
||||||
|
and ctx.source_instance_key = es.source_instance_key
|
||||||
|
and ctx.tenant_provider_setting_key = coalesce(es.tenant_provider_setting_key, '')
|
||||||
|
)
|
||||||
|
""";
|
||||||
|
}
|
||||||
|
|
||||||
|
private RegistrationMasterRow registrationMasterRow(
|
||||||
|
int eventSourceId,
|
||||||
|
String sourceRegistrationEntityId,
|
||||||
|
String sourceExternalKey,
|
||||||
|
String nation,
|
||||||
|
String registrationNumber,
|
||||||
|
OffsetDateTime sourceUpdatedAt
|
||||||
|
) {
|
||||||
|
String resolvedNation = nation;
|
||||||
|
String resolvedRegistrationNumber = registrationNumber;
|
||||||
|
if ((resolvedNation == null || resolvedRegistrationNumber == null) && sourceExternalKey != null) {
|
||||||
|
int separator = sourceExternalKey.indexOf(':');
|
||||||
|
if (separator > -1) {
|
||||||
|
resolvedNation = resolvedNation == null ? normalizeNullable(sourceExternalKey.substring(0, separator)) : resolvedNation;
|
||||||
|
resolvedRegistrationNumber = resolvedRegistrationNumber == null
|
||||||
|
? normalizeNullable(sourceExternalKey.substring(separator + 1))
|
||||||
|
: resolvedRegistrationNumber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new RegistrationMasterRow(
|
||||||
|
eventSourceId,
|
||||||
|
sourceRegistrationEntityId,
|
||||||
|
resolvedNation,
|
||||||
|
resolvedRegistrationNumber,
|
||||||
|
sourceUpdatedAt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private OffsetDateTime offsetDateTime(Object value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (value instanceof OffsetDateTime offsetDateTime) {
|
||||||
|
return offsetDateTime;
|
||||||
|
}
|
||||||
|
if (value instanceof Timestamp timestamp) {
|
||||||
|
return timestamp.toInstant().atOffset(ZoneOffset.UTC);
|
||||||
|
}
|
||||||
|
if (value instanceof java.time.LocalDateTime localDateTime) {
|
||||||
|
return localDateTime.atOffset(ZoneOffset.UTC);
|
||||||
|
}
|
||||||
|
return OffsetDateTime.parse(value.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String toJson(Map<String, Object> value) {
|
||||||
|
try {
|
||||||
|
return objectMapper.writeValueAsString(value == null ? Map.of() : value);
|
||||||
|
} catch (JsonProcessingException e) {
|
||||||
|
throw new IllegalArgumentException("Cannot serialize vehicle identity payload", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeRequired(String value, String fieldName) {
|
||||||
|
String normalized = normalizeNullable(value);
|
||||||
|
if (normalized == null) {
|
||||||
|
throw new IllegalArgumentException(fieldName + " must not be blank");
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeNullable(String value) {
|
||||||
|
if (value == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
String trimmed = value.trim();
|
||||||
|
return trimmed.isEmpty() ? null : trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public record ResolvedVehicleReference(UUID vehicleId, UUID vehicleRegistrationId) {
|
||||||
|
public static ResolvedVehicleReference empty() {
|
||||||
|
return new ResolvedVehicleReference(null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private record VehicleMasterRow(int eventSourceId, String sourceVehicleEntityId, String vin, OffsetDateTime sourceUpdatedAt) {
|
||||||
|
}
|
||||||
|
|
||||||
|
private record RegistrationMasterRow(
|
||||||
|
int eventSourceId,
|
||||||
|
String sourceRegistrationEntityId,
|
||||||
|
String nation,
|
||||||
|
String registrationNumber,
|
||||||
|
OffsetDateTime sourceUpdatedAt
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -68,13 +68,20 @@ public class EventAcquisitionRecordKeyService {
|
||||||
if (event.vehicleRef() == null) {
|
if (event.vehicleRef() == null) {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
if (event.vehicleRef().vehicleRegistration() != null && event.vehicleRef().vehicleRegistration().hasValue()) {
|
boolean hasVin = event.vehicleRef().vin() != null && !event.vehicleRef().vin().isBlank();
|
||||||
return "VRN:" + event.vehicleRef().vehicleRegistration().stableKey();
|
boolean hasRegistration = event.vehicleRef().vehicleRegistration() != null
|
||||||
}
|
&& event.vehicleRef().vehicleRegistration().hasValue();
|
||||||
if (event.vehicleRef().vin() != null && !event.vehicleRef().vin().isBlank()) {
|
|
||||||
|
if (hasVin) {
|
||||||
return "VIN:" + event.vehicleRef().vin();
|
return "VIN:" + event.vehicleRef().vin();
|
||||||
}
|
}
|
||||||
return "SOURCE_VEHICLE:" + nullToEmpty(event.vehicleRef().sourceEntityId());
|
if (hasRegistration) {
|
||||||
|
return "VRN:" + event.vehicleRef().vehicleRegistration().stableKey();
|
||||||
|
}
|
||||||
|
if (event.vehicleRef().sourceVehicleEntityId() != null) {
|
||||||
|
return "SOURCE_VEHICLE:" + event.vehicleRef().sourceVehicleEntityId();
|
||||||
|
}
|
||||||
|
return "SOURCE_REGISTRATION:" + nullToEmpty(event.vehicleRef().sourceRegistrationEntityId());
|
||||||
}
|
}
|
||||||
|
|
||||||
private String normalizeTime(OffsetDateTime value) {
|
private String normalizeTime(OffsetDateTime value) {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package at.procon.eventhub.tachograph.api;
|
||||||
|
|
||||||
|
import at.procon.eventhub.tachograph.service.UnsupportedTachographExtractionException;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.Map;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
@RestControllerAdvice(basePackageClasses = TachographIngestionController.class)
|
||||||
|
public class TachographApiExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(UnsupportedTachographExtractionException.class)
|
||||||
|
public ResponseEntity<Map<String, Object>> handleUnsupportedExtraction(UnsupportedTachographExtractionException ex) {
|
||||||
|
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Map.of(
|
||||||
|
"error", "UNSUPPORTED_TACHOGRAPH_EXTRACTION",
|
||||||
|
"message", ex.getMessage(),
|
||||||
|
"timestamp", OffsetDateTime.now().toString()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -19,6 +19,7 @@ import at.procon.eventhub.tachograph.dto.TachographImportRequest;
|
||||||
import java.sql.ResultSet;
|
import java.sql.ResultSet;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
|
@ -40,6 +41,9 @@ abstract class AbstractTachographActivityRowMapper implements ExtractionRowMappe
|
||||||
SourcePackageRefDto sourcePackageRef = sourcePackageRef(rs);
|
SourcePackageRefDto sourcePackageRef = sourcePackageRef(rs);
|
||||||
DriverRefDto driverRef = driverRef(rs);
|
DriverRefDto driverRef = driverRef(rs);
|
||||||
VehicleRefDto vehicleRef = vehicleRef(rs);
|
VehicleRefDto vehicleRef = vehicleRef(rs);
|
||||||
|
CardSlot cardSlot = cardSlot(rs);
|
||||||
|
CardStatus cardStatus = cardStatus(rs);
|
||||||
|
DrivingStatus drivingStatus = drivingStatus(rs);
|
||||||
|
|
||||||
String externalSourceEventId = string(rs, "external_source_event_id");
|
String externalSourceEventId = string(rs, "external_source_event_id");
|
||||||
if (externalSourceEventId == null) {
|
if (externalSourceEventId == null) {
|
||||||
|
|
@ -59,15 +63,21 @@ abstract class AbstractTachographActivityRowMapper implements ExtractionRowMappe
|
||||||
lifecycle(rs),
|
lifecycle(rs),
|
||||||
longValue(rs, "odometer_m"),
|
longValue(rs, "odometer_m"),
|
||||||
null,
|
null,
|
||||||
detailsFactory.driverActivity(cardSlot(rs), cardStatus(rs), drivingStatus(rs)),
|
detailsFactory.driverActivity(cardSlot, cardStatus, drivingStatus),
|
||||||
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
|
sourcePackageRef != null && sourcePackageRef.hasAnyReference() ? sourcePackageRef : null,
|
||||||
detailsFactory.payloadFromMap(payload(rs, context)),
|
detailsFactory.payloadFromMap(payload(rs, context)),
|
||||||
false,
|
isManualEntry(cardStatus, drivingStatus),
|
||||||
context.packageInfo()
|
context.packageInfo()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract Map<String, Object> sourceSpecificPayload(ResultSet rs) throws SQLException;
|
protected Map<String, Object> sourceSpecificPayload(ResultSet rs) throws SQLException {
|
||||||
|
return Map.of();
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isManualEntry(CardStatus cardStatus, DrivingStatus drivingStatus) {
|
||||||
|
return cardStatus == CardStatus.NOT_INSERTED && drivingStatus == DrivingStatus.KNOWN;
|
||||||
|
}
|
||||||
|
|
||||||
private DriverRefDto driverRef(ResultSet rs) throws SQLException {
|
private DriverRefDto driverRef(ResultSet rs) throws SQLException {
|
||||||
DriverCardRefDto driverCard = null;
|
DriverCardRefDto driverCard = null;
|
||||||
|
|
@ -85,7 +95,12 @@ abstract class AbstractTachographActivityRowMapper implements ExtractionRowMappe
|
||||||
if (registrationNumber != null) {
|
if (registrationNumber != null) {
|
||||||
registration = new VehicleRegistrationRefDto(string(rs, "vehicle_registration_nation"), registrationNumber);
|
registration = new VehicleRegistrationRefDto(string(rs, "vehicle_registration_nation"), registrationNumber);
|
||||||
}
|
}
|
||||||
VehicleRefDto vehicleRef = new VehicleRefDto(string(rs, "vehicle_source_entity_id"), string(rs, "vehicle_vin"), registration);
|
VehicleRefDto vehicleRef = new VehicleRefDto(
|
||||||
|
string(rs, "vehicle_source_entity_id"),
|
||||||
|
string(rs, "vehicle_vin"),
|
||||||
|
string(rs, "vehicle_registration_source_entity_id"),
|
||||||
|
registration
|
||||||
|
);
|
||||||
return vehicleRef.hasAnyReference() ? vehicleRef : null;
|
return vehicleRef.hasAnyReference() ? vehicleRef : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,24 +117,7 @@ abstract class AbstractTachographActivityRowMapper implements ExtractionRowMappe
|
||||||
|
|
||||||
private Map<String, Object> payload(ResultSet rs, ExtractionContext<TachographImportRequest> context) throws SQLException {
|
private Map<String, Object> payload(ResultSet rs, ExtractionContext<TachographImportRequest> context) throws SQLException {
|
||||||
Map<String, Object> raw = new LinkedHashMap<>();
|
Map<String, Object> raw = new LinkedHashMap<>();
|
||||||
raw.put("extractionCode", context.planItem().extractionCode());
|
|
||||||
raw.put("sourceKind", context.planItem().sourceKind());
|
|
||||||
raw.put("sourceTables", context.planItem().sourceTables());
|
|
||||||
put(raw, "sourceRowId", string(rs, "source_row_id"));
|
put(raw, "sourceRowId", string(rs, "source_row_id"));
|
||||||
put(raw, "activityCode", object(rs, "activity_code"));
|
|
||||||
put(raw, "activityText", string(rs, "activity_text"));
|
|
||||||
put(raw, "eventType", string(rs, "event_type"));
|
|
||||||
put(raw, "lifecycle", string(rs, "lifecycle"));
|
|
||||||
put(raw, "cardSlot", object(rs, "card_slot"));
|
|
||||||
put(raw, "cardStatus", object(rs, "card_status"));
|
|
||||||
put(raw, "drivingStatus", object(rs, "driving_status"));
|
|
||||||
put(raw, "driverSourceEntityId", string(rs, "driver_source_entity_id"));
|
|
||||||
put(raw, "driverCardNation", string(rs, "driver_card_nation"));
|
|
||||||
put(raw, "driverCardNumber", string(rs, "driver_card_number"));
|
|
||||||
put(raw, "vehicleSourceEntityId", string(rs, "vehicle_source_entity_id"));
|
|
||||||
put(raw, "vehicleVin", string(rs, "vehicle_vin"));
|
|
||||||
put(raw, "vehicleRegistrationNation", string(rs, "vehicle_registration_nation"));
|
|
||||||
put(raw, "vehicleRegistrationNumber", string(rs, "vehicle_registration_number"));
|
|
||||||
raw.putAll(sourceSpecificPayload(rs));
|
raw.putAll(sourceSpecificPayload(rs));
|
||||||
return Map.of("raw", raw);
|
return Map.of("raw", raw);
|
||||||
}
|
}
|
||||||
|
|
@ -160,15 +158,20 @@ abstract class AbstractTachographActivityRowMapper implements ExtractionRowMappe
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (value instanceof OffsetDateTime offsetDateTime) {
|
if (value instanceof OffsetDateTime offsetDateTime) {
|
||||||
return offsetDateTime;
|
return offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC);
|
||||||
}
|
}
|
||||||
if (value instanceof Timestamp timestamp) {
|
if (value instanceof Timestamp timestamp) {
|
||||||
return timestamp.toInstant().atOffset(ZoneOffset.UTC);
|
return timestamp.toLocalDateTime().atOffset(ZoneOffset.UTC);
|
||||||
}
|
}
|
||||||
if (value instanceof java.time.LocalDateTime localDateTime) {
|
if (value instanceof java.time.LocalDateTime localDateTime) {
|
||||||
return localDateTime.atOffset(ZoneOffset.UTC);
|
return localDateTime.atOffset(ZoneOffset.UTC);
|
||||||
}
|
}
|
||||||
return OffsetDateTime.parse(value.toString());
|
String text = value.toString();
|
||||||
|
try {
|
||||||
|
return OffsetDateTime.parse(text).withOffsetSameInstant(ZoneOffset.UTC);
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
return LocalDateTime.parse(text).atOffset(ZoneOffset.UTC);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Long longValue(ResultSet rs, String column) throws SQLException {
|
private Long longValue(ResultSet rs, String column) throws SQLException {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
package at.procon.eventhub.tachograph.service;
|
package at.procon.eventhub.tachograph.service;
|
||||||
|
|
||||||
import at.procon.eventhub.service.EventDetailsFactory;
|
import at.procon.eventhub.service.EventDetailsFactory;
|
||||||
import java.sql.ResultSet;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
|
@ -13,11 +9,4 @@ public class CardActivityRowMapper extends AbstractTachographActivityRowMapper {
|
||||||
public CardActivityRowMapper(EventDetailsFactory detailsFactory) {
|
public CardActivityRowMapper(EventDetailsFactory detailsFactory) {
|
||||||
super(detailsFactory);
|
super(detailsFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Map<String, Object> sourceSpecificPayload(ResultSet rs) throws SQLException {
|
|
||||||
Map<String, Object> raw = new LinkedHashMap<>();
|
|
||||||
put(raw, "cardActivityId", string(rs, "card_activity_id"));
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ package at.procon.eventhub.tachograph.service;
|
||||||
import at.procon.eventhub.dto.EventHubEventDto;
|
import at.procon.eventhub.dto.EventHubEventDto;
|
||||||
import at.procon.eventhub.dto.EventSourceDto;
|
import at.procon.eventhub.dto.EventSourceDto;
|
||||||
import at.procon.eventhub.dto.ImportCursorStateDto;
|
import at.procon.eventhub.dto.ImportCursorStateDto;
|
||||||
|
import at.procon.eventhub.dto.ImportScopeDto;
|
||||||
import at.procon.eventhub.importing.ImportPlanItemDto;
|
import at.procon.eventhub.importing.ImportPlanItemDto;
|
||||||
import at.procon.eventhub.importing.ImportTimeChunkDto;
|
import at.procon.eventhub.importing.ImportTimeChunkDto;
|
||||||
import at.procon.eventhub.importing.extraction.AbstractJdbcExtractionBatchExecutor;
|
import at.procon.eventhub.importing.extraction.AbstractJdbcExtractionBatchExecutor;
|
||||||
|
|
@ -10,7 +11,11 @@ import at.procon.eventhub.importing.extraction.ExtractionDefinition;
|
||||||
import at.procon.eventhub.importing.persistence.ImportCursorRepository;
|
import at.procon.eventhub.importing.persistence.ImportCursorRepository;
|
||||||
import at.procon.eventhub.tachograph.dto.TachographExtractionBatchResultDto;
|
import at.procon.eventhub.tachograph.dto.TachographExtractionBatchResultDto;
|
||||||
import at.procon.eventhub.tachograph.dto.TachographImportRequest;
|
import at.procon.eventhub.tachograph.dto.TachographImportRequest;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.time.ZoneOffset;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import org.apache.camel.ProducerTemplate;
|
import org.apache.camel.ProducerTemplate;
|
||||||
|
|
@ -46,6 +51,21 @@ public class JdbcTachographExtractionBatchExecutor
|
||||||
return definitionRegistry.findByCode(code);
|
return definitionRegistry.findByCode(code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Map<String, Object> parameters(
|
||||||
|
TachographImportRequest request,
|
||||||
|
ImportScopeDto scope,
|
||||||
|
ImportCursorStateDto cursor
|
||||||
|
) {
|
||||||
|
Map<String, Object> params = super.parameters(request, scope, cursor);
|
||||||
|
params.put("occurredFrom", utcLocalDateTime(scope == null ? null : scope.occurredFrom()));
|
||||||
|
params.put("occurredTo", utcLocalDateTime(scope == null ? null : scope.occurredTo()));
|
||||||
|
params.put("lastSourcePackageImportedAt", utcLocalDateTime(cursor == null ? null : cursor.lastSourcePackageImportedAt()));
|
||||||
|
params.put("lastSourceRowUpdatedAt", utcLocalDateTime(cursor == null ? null : cursor.lastSourceRowUpdatedAt()));
|
||||||
|
params.put("lastOccurredTo", utcLocalDateTime(cursor == null ? null : cursor.lastOccurredTo()));
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected TachographExtractionBatchResultDto resultFor(
|
protected TachographExtractionBatchResultDto resultFor(
|
||||||
UUID packageId,
|
UUID packageId,
|
||||||
|
|
@ -91,4 +111,8 @@ public class JdbcTachographExtractionBatchExecutor
|
||||||
protected String providerPackagePrefix() {
|
protected String providerPackagePrefix() {
|
||||||
return "TACHOGRAPH";
|
return "TACHOGRAPH";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private LocalDateTime utcLocalDateTime(OffsetDateTime value) {
|
||||||
|
return value == null ? null : value.withOffsetSameInstant(ZoneOffset.UTC).toLocalDateTime();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import at.procon.eventhub.importing.ImportPlanItemDto;
|
||||||
import at.procon.eventhub.tachograph.dto.TachographImportRequest;
|
import at.procon.eventhub.tachograph.dto.TachographImportRequest;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
|
|
@ -16,10 +18,16 @@ public class TachographImportPlanService {
|
||||||
|
|
||||||
private final EventHubProperties properties;
|
private final EventHubProperties properties;
|
||||||
private final ImportChunkPlanner chunkPlanner;
|
private final ImportChunkPlanner chunkPlanner;
|
||||||
|
private final TachographExtractionDefinitionRegistry definitionRegistry;
|
||||||
|
|
||||||
public TachographImportPlanService(EventHubProperties properties, ImportChunkPlanner chunkPlanner) {
|
public TachographImportPlanService(
|
||||||
|
EventHubProperties properties,
|
||||||
|
ImportChunkPlanner chunkPlanner,
|
||||||
|
TachographExtractionDefinitionRegistry definitionRegistry
|
||||||
|
) {
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
this.chunkPlanner = chunkPlanner;
|
this.chunkPlanner = chunkPlanner;
|
||||||
|
this.definitionRegistry = definitionRegistry;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ImportPlanDto createPlan(TachographImportRequest request) {
|
public ImportPlanDto createPlan(TachographImportRequest request) {
|
||||||
|
|
@ -27,6 +35,7 @@ public class TachographImportPlanService {
|
||||||
for (EventFamily family : request.eventFamilies()) {
|
for (EventFamily family : request.eventFamilies()) {
|
||||||
items.addAll(itemsFor(family, request.acquisitionStrategy()));
|
items.addAll(itemsFor(family, request.acquisitionStrategy()));
|
||||||
}
|
}
|
||||||
|
validateJdbcExtractions(items, request.eventFamilies());
|
||||||
return new ImportPlanDto(
|
return new ImportPlanDto(
|
||||||
request.tenantKey(),
|
request.tenantKey(),
|
||||||
request.mode(),
|
request.mode(),
|
||||||
|
|
@ -87,4 +96,47 @@ public class TachographImportPlanService {
|
||||||
) {
|
) {
|
||||||
return new ImportPlanItemDto(family, sourceKind, extractionCode, sourceTables, entityAxis, description, strategy);
|
return new ImportPlanItemDto(family, sourceKind, extractionCode, sourceTables, entityAxis, description, strategy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void validateJdbcExtractions(List<ImportPlanItemDto> items, Set<EventFamily> requestedFamilies) {
|
||||||
|
String jdbcUrl = properties.getTachograph().getDatasource().getJdbcUrl();
|
||||||
|
if (jdbcUrl == null || jdbcUrl.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> unsupportedCodes = items.stream()
|
||||||
|
.map(ImportPlanItemDto::extractionCode)
|
||||||
|
.filter(code -> definitionRegistry.findByCode(code).isEmpty())
|
||||||
|
.distinct()
|
||||||
|
.sorted()
|
||||||
|
.toList();
|
||||||
|
if (unsupportedCodes.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> supportedCodes = definitionRegistry.supportedCodes().stream()
|
||||||
|
.sorted()
|
||||||
|
.toList();
|
||||||
|
List<String> supportedFamilies = definitionRegistry.definitions().stream()
|
||||||
|
.map(definition -> definition.eventFamily().name())
|
||||||
|
.distinct()
|
||||||
|
.sorted()
|
||||||
|
.toList();
|
||||||
|
String requestedFamilyNames = requestedFamilies.stream()
|
||||||
|
.map(EventFamily::name)
|
||||||
|
.sorted()
|
||||||
|
.collect(Collectors.joining(", "));
|
||||||
|
|
||||||
|
throw new UnsupportedTachographExtractionException(
|
||||||
|
"Tachograph JDBC extraction is enabled, but the plan contains extraction codes without JDBC definitions: "
|
||||||
|
+ unsupportedCodes
|
||||||
|
+ ". Supported JDBC extraction codes are: "
|
||||||
|
+ supportedCodes
|
||||||
|
+ ". Requested event families: ["
|
||||||
|
+ requestedFamilyNames
|
||||||
|
+ "]. "
|
||||||
|
+ "Use only supported event families "
|
||||||
|
+ supportedFamilies
|
||||||
|
+ " for JDBC execution, or add the missing codes to TachographExtractionDefinitionRegistry."
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import at.procon.eventhub.importing.masterdata.SourceMasterEntityUpsert;
|
||||||
import at.procon.eventhub.importing.masterdata.SourceMasterRelationUpsert;
|
import at.procon.eventhub.importing.masterdata.SourceMasterRelationUpsert;
|
||||||
import at.procon.eventhub.persistence.EventSourceRepository;
|
import at.procon.eventhub.persistence.EventSourceRepository;
|
||||||
import at.procon.eventhub.persistence.SourceMasterDataRepository;
|
import at.procon.eventhub.persistence.SourceMasterDataRepository;
|
||||||
|
import at.procon.eventhub.persistence.VehicleIdentityRepository;
|
||||||
import at.procon.eventhub.tachograph.dto.TachographImportRequest;
|
import at.procon.eventhub.tachograph.dto.TachographImportRequest;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
@ -12,6 +13,7 @@ import java.sql.ResultSet;
|
||||||
import java.sql.ResultSetMetaData;
|
import java.sql.ResultSetMetaData;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
import java.sql.Timestamp;
|
import java.sql.Timestamp;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.time.OffsetDateTime;
|
import java.time.OffsetDateTime;
|
||||||
import java.time.ZoneOffset;
|
import java.time.ZoneOffset;
|
||||||
import java.util.LinkedHashMap;
|
import java.util.LinkedHashMap;
|
||||||
|
|
@ -45,17 +47,20 @@ public class TachographMasterDataRefreshService {
|
||||||
private final ObjectProvider<NamedParameterJdbcTemplate> tachographJdbcTemplateProvider;
|
private final ObjectProvider<NamedParameterJdbcTemplate> tachographJdbcTemplateProvider;
|
||||||
private final SourceMasterDataRepository sourceMasterDataRepository;
|
private final SourceMasterDataRepository sourceMasterDataRepository;
|
||||||
private final EventSourceRepository eventSourceRepository;
|
private final EventSourceRepository eventSourceRepository;
|
||||||
|
private final VehicleIdentityRepository vehicleIdentityRepository;
|
||||||
private final ResourceLoader resourceLoader;
|
private final ResourceLoader resourceLoader;
|
||||||
|
|
||||||
public TachographMasterDataRefreshService(
|
public TachographMasterDataRefreshService(
|
||||||
@Qualifier("tachographNamedParameterJdbcTemplate") ObjectProvider<NamedParameterJdbcTemplate> tachographJdbcTemplateProvider,
|
@Qualifier("tachographNamedParameterJdbcTemplate") ObjectProvider<NamedParameterJdbcTemplate> tachographJdbcTemplateProvider,
|
||||||
SourceMasterDataRepository sourceMasterDataRepository,
|
SourceMasterDataRepository sourceMasterDataRepository,
|
||||||
EventSourceRepository eventSourceRepository,
|
EventSourceRepository eventSourceRepository,
|
||||||
|
VehicleIdentityRepository vehicleIdentityRepository,
|
||||||
ResourceLoader resourceLoader
|
ResourceLoader resourceLoader
|
||||||
) {
|
) {
|
||||||
this.tachographJdbcTemplateProvider = tachographJdbcTemplateProvider;
|
this.tachographJdbcTemplateProvider = tachographJdbcTemplateProvider;
|
||||||
this.sourceMasterDataRepository = sourceMasterDataRepository;
|
this.sourceMasterDataRepository = sourceMasterDataRepository;
|
||||||
this.eventSourceRepository = eventSourceRepository;
|
this.eventSourceRepository = eventSourceRepository;
|
||||||
|
this.vehicleIdentityRepository = vehicleIdentityRepository;
|
||||||
this.resourceLoader = resourceLoader;
|
this.resourceLoader = resourceLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -93,10 +98,11 @@ public class TachographMasterDataRefreshService {
|
||||||
(rs, rowNum) -> relation(rs)
|
(rs, rowNum) -> relation(rs)
|
||||||
);
|
);
|
||||||
int relationCount = sourceMasterDataRepository.upsertRelations(tenantKey, eventSourceId, relations);
|
int relationCount = sourceMasterDataRepository.upsertRelations(tenantKey, eventSourceId, relations);
|
||||||
|
int reconciledVehicles = vehicleIdentityRepository.reconcileFromMasterData(tenantKey, eventSourceId);
|
||||||
|
|
||||||
MasterDataRefreshResult result = new MasterDataRefreshResult(entities, relationCount);
|
MasterDataRefreshResult result = new MasterDataRefreshResult(entities, relationCount);
|
||||||
log.info("Refreshed tachograph source master data tenant={} source={} entities={} relations={}",
|
log.info("Refreshed tachograph source master data tenant={} source={} entities={} relations={} reconciledVehicles={}",
|
||||||
tenantKey, request.eventSource().stableKey(), result.entitiesUpserted(), result.relationsUpserted());
|
tenantKey, request.eventSource().stableKey(), result.entitiesUpserted(), result.relationsUpserted(), reconciledVehicles);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -198,15 +204,20 @@ public class TachographMasterDataRefreshService {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (value instanceof OffsetDateTime offsetDateTime) {
|
if (value instanceof OffsetDateTime offsetDateTime) {
|
||||||
return offsetDateTime;
|
return offsetDateTime.withOffsetSameInstant(ZoneOffset.UTC);
|
||||||
}
|
}
|
||||||
if (value instanceof Timestamp timestamp) {
|
if (value instanceof Timestamp timestamp) {
|
||||||
return timestamp.toInstant().atOffset(ZoneOffset.UTC);
|
return timestamp.toLocalDateTime().atOffset(ZoneOffset.UTC);
|
||||||
}
|
}
|
||||||
if (value instanceof java.time.LocalDateTime localDateTime) {
|
if (value instanceof java.time.LocalDateTime localDateTime) {
|
||||||
return localDateTime.atOffset(ZoneOffset.UTC);
|
return localDateTime.atOffset(ZoneOffset.UTC);
|
||||||
}
|
}
|
||||||
return OffsetDateTime.parse(value.toString());
|
String text = value.toString();
|
||||||
|
try {
|
||||||
|
return OffsetDateTime.parse(text).withOffsetSameInstant(ZoneOffset.UTC);
|
||||||
|
} catch (RuntimeException ignored) {
|
||||||
|
return LocalDateTime.parse(text).atOffset(ZoneOffset.UTC);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private ValidityRange normalizeValidityRange(
|
private ValidityRange normalizeValidityRange(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package at.procon.eventhub.tachograph.service;
|
||||||
|
|
||||||
|
public class UnsupportedTachographExtractionException extends IllegalArgumentException {
|
||||||
|
|
||||||
|
public UnsupportedTachographExtractionException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,10 +1,6 @@
|
||||||
package at.procon.eventhub.tachograph.service;
|
package at.procon.eventhub.tachograph.service;
|
||||||
|
|
||||||
import at.procon.eventhub.service.EventDetailsFactory;
|
import at.procon.eventhub.service.EventDetailsFactory;
|
||||||
import java.sql.ResultSet;
|
|
||||||
import java.sql.SQLException;
|
|
||||||
import java.util.LinkedHashMap;
|
|
||||||
import java.util.Map;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
|
|
@ -13,11 +9,4 @@ public class VuActivityRowMapper extends AbstractTachographActivityRowMapper {
|
||||||
public VuActivityRowMapper(EventDetailsFactory detailsFactory) {
|
public VuActivityRowMapper(EventDetailsFactory detailsFactory) {
|
||||||
super(detailsFactory);
|
super(detailsFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Map<String, Object> sourceSpecificPayload(ResultSet rs) throws SQLException {
|
|
||||||
Map<String, Object> raw = new LinkedHashMap<>();
|
|
||||||
put(raw, "vuActivityId", string(rs, "vu_activity_id"));
|
|
||||||
return raw;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ spring:
|
||||||
create-schemas: true
|
create-schemas: true
|
||||||
|
|
||||||
server:
|
server:
|
||||||
port: 8080
|
port: 8085
|
||||||
|
|
||||||
camel:
|
camel:
|
||||||
springboot:
|
springboot:
|
||||||
|
|
@ -29,6 +29,9 @@ eventhub:
|
||||||
batch:
|
batch:
|
||||||
completion-size: 1000
|
completion-size: 1000
|
||||||
completion-timeout: 5s
|
completion-timeout: 5s
|
||||||
|
queue-size: 10000
|
||||||
|
block-when-full: true
|
||||||
|
queue-offer-timeout: 5m
|
||||||
tachograph:
|
tachograph:
|
||||||
default-chunk-days: 1
|
default-chunk-days: 1
|
||||||
occurred-at-overlap: 7d
|
occurred-at-overlap: 7d
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
create table eventhub.vehicle (
|
||||||
|
id uuid primary key,
|
||||||
|
tenant_key text not null,
|
||||||
|
event_source_id integer not null references eventhub.event_source(id),
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now()
|
||||||
|
);
|
||||||
|
|
||||||
|
create table eventhub.vehicle_identifier (
|
||||||
|
id uuid primary key,
|
||||||
|
vehicle_id uuid not null references eventhub.vehicle(id) on delete cascade,
|
||||||
|
tenant_key text not null,
|
||||||
|
event_source_id integer not null references eventhub.event_source(id),
|
||||||
|
identifier_type text not null,
|
||||||
|
nation text,
|
||||||
|
identifier_value text not null,
|
||||||
|
valid_from timestamptz,
|
||||||
|
valid_to timestamptz,
|
||||||
|
source_updated_at timestamptz,
|
||||||
|
payload jsonb not null default '{}'::jsonb,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint chk_vehicle_identifier_valid_time_order
|
||||||
|
check (valid_from is null or valid_to is null or valid_from <= valid_to),
|
||||||
|
constraint chk_vehicle_identifier_nation_for_vrn
|
||||||
|
check (
|
||||||
|
identifier_type <> 'VRN'
|
||||||
|
or nation is not null
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
create index idx_vehicle_lookup_ctx
|
||||||
|
on eventhub.vehicle(tenant_key, event_source_id, updated_at desc);
|
||||||
|
|
||||||
|
create index idx_vehicle_identifier_lookup
|
||||||
|
on eventhub.vehicle_identifier(
|
||||||
|
tenant_key, event_source_id, identifier_type, nation, identifier_value, valid_from desc
|
||||||
|
);
|
||||||
|
|
||||||
|
create unique index ux_vehicle_identifier_exact
|
||||||
|
on eventhub.vehicle_identifier(
|
||||||
|
vehicle_id,
|
||||||
|
identifier_type,
|
||||||
|
coalesce(nation, ''),
|
||||||
|
identifier_value,
|
||||||
|
coalesce(valid_from, '-infinity'::timestamptz),
|
||||||
|
coalesce(valid_to, 'infinity'::timestamptz)
|
||||||
|
);
|
||||||
|
|
||||||
|
alter table eventhub.event
|
||||||
|
rename column vehicle_entity_id to vehicle_id;
|
||||||
|
|
||||||
|
alter table eventhub.event
|
||||||
|
drop constraint if exists event_vehicle_entity_id_fkey;
|
||||||
|
|
||||||
|
alter table eventhub.event
|
||||||
|
add constraint fk_event_vehicle
|
||||||
|
foreign key (vehicle_id)
|
||||||
|
references eventhub.vehicle(id);
|
||||||
|
|
||||||
|
drop index if exists eventhub.idx_event_vehicle_time;
|
||||||
|
|
||||||
|
create index idx_event_vehicle_time
|
||||||
|
on eventhub.event(vehicle_id, occurred_at desc)
|
||||||
|
where vehicle_id is not null;
|
||||||
|
|
||||||
|
alter table eventhub.event
|
||||||
|
drop constraint if exists chk_event_driver_or_vehicle_ref;
|
||||||
|
|
||||||
|
alter table eventhub.event
|
||||||
|
add constraint chk_event_driver_or_vehicle_ref
|
||||||
|
check (
|
||||||
|
driver_entity_id is not null
|
||||||
|
or vehicle_id is not null
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,241 @@
|
||||||
|
alter table eventhub.vehicle
|
||||||
|
add column if not exists source_vehicle_entity_id text,
|
||||||
|
add column if not exists vin text;
|
||||||
|
|
||||||
|
do $migration$
|
||||||
|
begin
|
||||||
|
if to_regclass('eventhub.vehicle_identifier') is not null
|
||||||
|
and exists (
|
||||||
|
select 1
|
||||||
|
from information_schema.columns
|
||||||
|
where table_schema = 'eventhub'
|
||||||
|
and table_name = 'vehicle_identifier'
|
||||||
|
and column_name = 'vehicle_id'
|
||||||
|
)
|
||||||
|
then
|
||||||
|
execute $sql$
|
||||||
|
update eventhub.vehicle v
|
||||||
|
set source_vehicle_entity_id = src.identifier_value
|
||||||
|
from eventhub.vehicle_identifier src
|
||||||
|
where src.vehicle_id = v.id
|
||||||
|
and src.identifier_type = 'SOURCE_VEHICLE'
|
||||||
|
and v.source_vehicle_entity_id is null
|
||||||
|
$sql$;
|
||||||
|
|
||||||
|
execute $sql$
|
||||||
|
update eventhub.vehicle v
|
||||||
|
set vin = src.identifier_value
|
||||||
|
from eventhub.vehicle_identifier src
|
||||||
|
where src.vehicle_id = v.id
|
||||||
|
and src.identifier_type = 'VIN'
|
||||||
|
and v.vin is null
|
||||||
|
$sql$;
|
||||||
|
end if;
|
||||||
|
end
|
||||||
|
$migration$;
|
||||||
|
|
||||||
|
create table if not exists eventhub.vehicle_registration (
|
||||||
|
id uuid primary key,
|
||||||
|
tenant_key text not null,
|
||||||
|
event_source_id integer not null references eventhub.event_source(id),
|
||||||
|
source_registration_entity_id text,
|
||||||
|
nation text not null,
|
||||||
|
registration_number text not null,
|
||||||
|
valid_from timestamptz,
|
||||||
|
valid_to timestamptz,
|
||||||
|
source_updated_at timestamptz,
|
||||||
|
payload jsonb not null default '{}'::jsonb,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint chk_vehicle_registration_valid_time_order
|
||||||
|
check (valid_from is null or valid_to is null or valid_from <= valid_to)
|
||||||
|
);
|
||||||
|
|
||||||
|
create table if not exists eventhub.vehicle_registration_assignment (
|
||||||
|
id uuid primary key,
|
||||||
|
tenant_key text not null,
|
||||||
|
event_source_id integer not null references eventhub.event_source(id),
|
||||||
|
vehicle_registration_id uuid not null references eventhub.vehicle_registration(id) on delete cascade,
|
||||||
|
vehicle_id uuid not null references eventhub.vehicle(id) on delete cascade,
|
||||||
|
valid_from timestamptz,
|
||||||
|
valid_to timestamptz,
|
||||||
|
source_updated_at timestamptz,
|
||||||
|
payload jsonb not null default '{}'::jsonb,
|
||||||
|
created_at timestamptz not null default now(),
|
||||||
|
updated_at timestamptz not null default now(),
|
||||||
|
constraint chk_vehicle_registration_assignment_valid_time_order
|
||||||
|
check (valid_from is null or valid_to is null or valid_from <= valid_to)
|
||||||
|
);
|
||||||
|
|
||||||
|
do $migration$
|
||||||
|
begin
|
||||||
|
if to_regclass('eventhub.vehicle_identifier') is not null
|
||||||
|
and exists (
|
||||||
|
select 1
|
||||||
|
from information_schema.columns
|
||||||
|
where table_schema = 'eventhub'
|
||||||
|
and table_name = 'vehicle_identifier'
|
||||||
|
and column_name = 'vehicle_id'
|
||||||
|
)
|
||||||
|
then
|
||||||
|
execute $sql$
|
||||||
|
insert into eventhub.vehicle_registration(
|
||||||
|
id, tenant_key, event_source_id, source_registration_entity_id, nation, registration_number,
|
||||||
|
valid_from, valid_to, payload
|
||||||
|
)
|
||||||
|
select gen_random_uuid(),
|
||||||
|
src.tenant_key,
|
||||||
|
src.event_source_id,
|
||||||
|
source_reg.identifier_value,
|
||||||
|
src.nation,
|
||||||
|
src.identifier_value,
|
||||||
|
src.valid_from,
|
||||||
|
src.valid_to,
|
||||||
|
src.payload
|
||||||
|
from eventhub.vehicle_identifier src
|
||||||
|
left join eventhub.vehicle_identifier source_reg on source_reg.vehicle_id = src.vehicle_id
|
||||||
|
and source_reg.identifier_type = 'SOURCE_REGISTRATION'
|
||||||
|
where src.identifier_type = 'VRN'
|
||||||
|
and src.nation is not null
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from eventhub.vehicle_registration existing
|
||||||
|
where existing.tenant_key = src.tenant_key
|
||||||
|
and existing.event_source_id = src.event_source_id
|
||||||
|
and existing.nation = src.nation
|
||||||
|
and existing.registration_number = src.identifier_value
|
||||||
|
and existing.valid_from is not distinct from src.valid_from
|
||||||
|
and existing.valid_to is not distinct from src.valid_to
|
||||||
|
)
|
||||||
|
$sql$;
|
||||||
|
|
||||||
|
execute $sql$
|
||||||
|
insert into eventhub.vehicle_registration_assignment(
|
||||||
|
id, tenant_key, event_source_id, vehicle_registration_id, vehicle_id, valid_from, valid_to, payload
|
||||||
|
)
|
||||||
|
select gen_random_uuid(),
|
||||||
|
reg.tenant_key,
|
||||||
|
reg.event_source_id,
|
||||||
|
reg.id,
|
||||||
|
v.id,
|
||||||
|
reg.valid_from,
|
||||||
|
reg.valid_to,
|
||||||
|
jsonb_build_object('migrated_from', 'vehicle_identifier')
|
||||||
|
from eventhub.vehicle_registration reg
|
||||||
|
join eventhub.vehicle_identifier vrn on vrn.tenant_key = reg.tenant_key
|
||||||
|
and vrn.event_source_id = reg.event_source_id
|
||||||
|
and vrn.identifier_type = 'VRN'
|
||||||
|
and vrn.nation = reg.nation
|
||||||
|
and vrn.identifier_value = reg.registration_number
|
||||||
|
and vrn.valid_from is not distinct from reg.valid_from
|
||||||
|
and vrn.valid_to is not distinct from reg.valid_to
|
||||||
|
join eventhub.vehicle v on v.id = vrn.vehicle_id
|
||||||
|
where not exists (
|
||||||
|
select 1
|
||||||
|
from eventhub.vehicle_registration_assignment existing
|
||||||
|
where existing.vehicle_registration_id = reg.id
|
||||||
|
and existing.vehicle_id = v.id
|
||||||
|
and existing.valid_from is not distinct from reg.valid_from
|
||||||
|
and existing.valid_to is not distinct from reg.valid_to
|
||||||
|
)
|
||||||
|
$sql$;
|
||||||
|
end if;
|
||||||
|
end
|
||||||
|
$migration$;
|
||||||
|
|
||||||
|
do $migration$
|
||||||
|
begin
|
||||||
|
if exists (
|
||||||
|
select 1
|
||||||
|
from information_schema.columns
|
||||||
|
where table_schema = 'eventhub'
|
||||||
|
and table_name = 'event'
|
||||||
|
and column_name = 'vehicle_entity_id'
|
||||||
|
)
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from information_schema.columns
|
||||||
|
where table_schema = 'eventhub'
|
||||||
|
and table_name = 'event'
|
||||||
|
and column_name = 'vehicle_id'
|
||||||
|
)
|
||||||
|
then
|
||||||
|
alter table eventhub.event rename column vehicle_entity_id to vehicle_id;
|
||||||
|
end if;
|
||||||
|
|
||||||
|
if not exists (
|
||||||
|
select 1
|
||||||
|
from information_schema.columns
|
||||||
|
where table_schema = 'eventhub'
|
||||||
|
and table_name = 'event'
|
||||||
|
and column_name = 'vehicle_id'
|
||||||
|
)
|
||||||
|
then
|
||||||
|
alter table eventhub.event add column vehicle_id uuid references eventhub.vehicle(id);
|
||||||
|
end if;
|
||||||
|
end
|
||||||
|
$migration$;
|
||||||
|
|
||||||
|
alter table eventhub.event
|
||||||
|
drop constraint if exists event_vehicle_entity_id_fkey;
|
||||||
|
|
||||||
|
alter table eventhub.event
|
||||||
|
drop constraint if exists fk_event_vehicle;
|
||||||
|
|
||||||
|
do $migration$
|
||||||
|
begin
|
||||||
|
if exists (
|
||||||
|
select 1
|
||||||
|
from information_schema.columns
|
||||||
|
where table_schema = 'eventhub'
|
||||||
|
and table_name = 'event'
|
||||||
|
and column_name = 'vehicle_id'
|
||||||
|
)
|
||||||
|
then
|
||||||
|
alter table eventhub.event
|
||||||
|
add constraint fk_event_vehicle
|
||||||
|
foreign key (vehicle_id)
|
||||||
|
references eventhub.vehicle(id)
|
||||||
|
not valid;
|
||||||
|
end if;
|
||||||
|
end
|
||||||
|
$migration$;
|
||||||
|
|
||||||
|
alter table eventhub.event
|
||||||
|
add column if not exists vehicle_registration_id uuid references eventhub.vehicle_registration(id);
|
||||||
|
|
||||||
|
create index if not exists idx_vehicle_source_entity
|
||||||
|
on eventhub.vehicle(tenant_key, event_source_id, source_vehicle_entity_id)
|
||||||
|
where source_vehicle_entity_id is not null;
|
||||||
|
|
||||||
|
create index if not exists idx_vehicle_vin
|
||||||
|
on eventhub.vehicle(tenant_key, event_source_id, vin)
|
||||||
|
where vin is not null;
|
||||||
|
|
||||||
|
create index if not exists idx_vehicle_registration_source_entity
|
||||||
|
on eventhub.vehicle_registration(tenant_key, event_source_id, source_registration_entity_id)
|
||||||
|
where source_registration_entity_id is not null;
|
||||||
|
|
||||||
|
create index if not exists idx_vehicle_registration_plate
|
||||||
|
on eventhub.vehicle_registration(tenant_key, event_source_id, nation, registration_number, valid_from desc);
|
||||||
|
|
||||||
|
create index if not exists idx_vehicle_registration_assignment_registration_time
|
||||||
|
on eventhub.vehicle_registration_assignment(vehicle_registration_id, valid_from desc, valid_to);
|
||||||
|
|
||||||
|
create index if not exists idx_vehicle_registration_assignment_vehicle_time
|
||||||
|
on eventhub.vehicle_registration_assignment(vehicle_id, valid_from desc, valid_to);
|
||||||
|
|
||||||
|
create index if not exists idx_event_vehicle_registration_time
|
||||||
|
on eventhub.event(vehicle_registration_id, occurred_at desc)
|
||||||
|
where vehicle_registration_id is not null;
|
||||||
|
|
||||||
|
alter table eventhub.event
|
||||||
|
drop constraint if exists chk_event_driver_or_vehicle_ref;
|
||||||
|
|
||||||
|
alter table eventhub.event
|
||||||
|
add constraint chk_event_driver_or_vehicle_ref
|
||||||
|
check (
|
||||||
|
driver_entity_id is not null
|
||||||
|
or vehicle_id is not null
|
||||||
|
or vehicle_registration_id is not null
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
alter table eventhub.event
|
||||||
|
drop constraint if exists event_vehicle_entity_id_fkey;
|
||||||
|
|
||||||
|
alter table eventhub.event
|
||||||
|
drop constraint if exists fk_event_vehicle;
|
||||||
|
|
||||||
|
do $migration$
|
||||||
|
begin
|
||||||
|
if exists (
|
||||||
|
select 1
|
||||||
|
from information_schema.columns
|
||||||
|
where table_schema = 'eventhub'
|
||||||
|
and table_name = 'event'
|
||||||
|
and column_name = 'vehicle_id'
|
||||||
|
)
|
||||||
|
then
|
||||||
|
alter table eventhub.event
|
||||||
|
add constraint fk_event_vehicle
|
||||||
|
foreign key (vehicle_id)
|
||||||
|
references eventhub.vehicle(id)
|
||||||
|
not valid;
|
||||||
|
end if;
|
||||||
|
end
|
||||||
|
$migration$;
|
||||||
|
|
@ -0,0 +1,193 @@
|
||||||
|
drop index if exists eventhub.idx_vehicle_registration_plate;
|
||||||
|
|
||||||
|
alter table eventhub.vehicle_registration
|
||||||
|
drop constraint if exists chk_vehicle_registration_valid_time_order;
|
||||||
|
|
||||||
|
alter table eventhub.vehicle_registration
|
||||||
|
drop column if exists valid_from,
|
||||||
|
drop column if exists valid_to;
|
||||||
|
|
||||||
|
create index if not exists idx_vehicle_registration_plate
|
||||||
|
on eventhub.vehicle_registration(tenant_key, event_source_id, nation, registration_number);
|
||||||
|
|
||||||
|
update eventhub.source_master_entity
|
||||||
|
set valid_from = null,
|
||||||
|
valid_to = null,
|
||||||
|
updated_at = now()
|
||||||
|
where entity_type = 'VEHICLE_REGISTRATION'
|
||||||
|
and (valid_from is not null or valid_to is not null);
|
||||||
|
|
||||||
|
with vehicle_entities as (
|
||||||
|
select tenant_key,
|
||||||
|
event_source_id,
|
||||||
|
nullif(source_entity_id, '') as source_vehicle_entity_id,
|
||||||
|
nullif(source_external_key, '') as vin
|
||||||
|
from eventhub.source_master_entity
|
||||||
|
where entity_type = 'VEHICLE'
|
||||||
|
and (nullif(source_entity_id, '') is not null or nullif(source_external_key, '') is not null)
|
||||||
|
)
|
||||||
|
update eventhub.vehicle vehicle
|
||||||
|
set source_vehicle_entity_id = coalesce(vehicle.source_vehicle_entity_id, vehicle_entities.source_vehicle_entity_id),
|
||||||
|
vin = coalesce(vehicle.vin, vehicle_entities.vin),
|
||||||
|
updated_at = now()
|
||||||
|
from vehicle_entities
|
||||||
|
where vehicle.tenant_key = vehicle_entities.tenant_key
|
||||||
|
and vehicle.event_source_id = vehicle_entities.event_source_id
|
||||||
|
and (
|
||||||
|
vehicle.source_vehicle_entity_id = vehicle_entities.source_vehicle_entity_id
|
||||||
|
or vehicle.vin = vehicle_entities.vin
|
||||||
|
);
|
||||||
|
|
||||||
|
with vehicle_entities as (
|
||||||
|
select tenant_key,
|
||||||
|
event_source_id,
|
||||||
|
nullif(source_entity_id, '') as source_vehicle_entity_id,
|
||||||
|
nullif(source_external_key, '') as vin
|
||||||
|
from eventhub.source_master_entity
|
||||||
|
where entity_type = 'VEHICLE'
|
||||||
|
and (nullif(source_entity_id, '') is not null or nullif(source_external_key, '') is not null)
|
||||||
|
)
|
||||||
|
insert into eventhub.vehicle(id, tenant_key, event_source_id, source_vehicle_entity_id, vin)
|
||||||
|
select gen_random_uuid(),
|
||||||
|
vehicle_entities.tenant_key,
|
||||||
|
vehicle_entities.event_source_id,
|
||||||
|
vehicle_entities.source_vehicle_entity_id,
|
||||||
|
vehicle_entities.vin
|
||||||
|
from vehicle_entities
|
||||||
|
where not exists (
|
||||||
|
select 1
|
||||||
|
from eventhub.vehicle vehicle
|
||||||
|
where vehicle.tenant_key = vehicle_entities.tenant_key
|
||||||
|
and vehicle.event_source_id = vehicle_entities.event_source_id
|
||||||
|
and (
|
||||||
|
vehicle.source_vehicle_entity_id = vehicle_entities.source_vehicle_entity_id
|
||||||
|
or vehicle.vin = vehicle_entities.vin
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
with registration_entities as (
|
||||||
|
select tenant_key,
|
||||||
|
event_source_id,
|
||||||
|
nullif(source_entity_id, '') as source_registration_entity_id,
|
||||||
|
coalesce(
|
||||||
|
nullif(payload ->> 'registration_nation', ''),
|
||||||
|
nullif(split_part(source_external_key, ':', 1), '')
|
||||||
|
) as nation,
|
||||||
|
coalesce(
|
||||||
|
nullif(payload ->> 'registration_number', ''),
|
||||||
|
case
|
||||||
|
when position(':' in source_external_key) > 0
|
||||||
|
then nullif(substring(source_external_key from position(':' in source_external_key) + 1), '')
|
||||||
|
else nullif(source_external_key, '')
|
||||||
|
end
|
||||||
|
) as registration_number,
|
||||||
|
source_updated_at
|
||||||
|
from eventhub.source_master_entity
|
||||||
|
where entity_type = 'VEHICLE_REGISTRATION'
|
||||||
|
)
|
||||||
|
update eventhub.vehicle_registration registration
|
||||||
|
set source_registration_entity_id = coalesce(
|
||||||
|
registration.source_registration_entity_id,
|
||||||
|
registration_entities.source_registration_entity_id
|
||||||
|
),
|
||||||
|
nation = coalesce(registration_entities.nation, registration.nation),
|
||||||
|
registration_number = coalesce(registration_entities.registration_number, registration.registration_number),
|
||||||
|
source_updated_at = registration_entities.source_updated_at,
|
||||||
|
updated_at = now()
|
||||||
|
from registration_entities
|
||||||
|
where registration_entities.nation is not null
|
||||||
|
and registration_entities.registration_number is not null
|
||||||
|
and registration.tenant_key = registration_entities.tenant_key
|
||||||
|
and registration.event_source_id = registration_entities.event_source_id
|
||||||
|
and (
|
||||||
|
registration.source_registration_entity_id = registration_entities.source_registration_entity_id
|
||||||
|
or (
|
||||||
|
registration.nation = registration_entities.nation
|
||||||
|
and registration.registration_number = registration_entities.registration_number
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
with registration_entities as (
|
||||||
|
select tenant_key,
|
||||||
|
event_source_id,
|
||||||
|
nullif(source_entity_id, '') as source_registration_entity_id,
|
||||||
|
coalesce(
|
||||||
|
nullif(payload ->> 'registration_nation', ''),
|
||||||
|
nullif(split_part(source_external_key, ':', 1), '')
|
||||||
|
) as nation,
|
||||||
|
coalesce(
|
||||||
|
nullif(payload ->> 'registration_number', ''),
|
||||||
|
case
|
||||||
|
when position(':' in source_external_key) > 0
|
||||||
|
then nullif(substring(source_external_key from position(':' in source_external_key) + 1), '')
|
||||||
|
else nullif(source_external_key, '')
|
||||||
|
end
|
||||||
|
) as registration_number,
|
||||||
|
source_updated_at
|
||||||
|
from eventhub.source_master_entity
|
||||||
|
where entity_type = 'VEHICLE_REGISTRATION'
|
||||||
|
)
|
||||||
|
insert into eventhub.vehicle_registration(
|
||||||
|
id, tenant_key, event_source_id, source_registration_entity_id,
|
||||||
|
nation, registration_number, source_updated_at, payload
|
||||||
|
)
|
||||||
|
select gen_random_uuid(),
|
||||||
|
registration_entities.tenant_key,
|
||||||
|
registration_entities.event_source_id,
|
||||||
|
registration_entities.source_registration_entity_id,
|
||||||
|
registration_entities.nation,
|
||||||
|
registration_entities.registration_number,
|
||||||
|
registration_entities.source_updated_at,
|
||||||
|
jsonb_build_object('source', 'master-data')
|
||||||
|
from registration_entities
|
||||||
|
where registration_entities.nation is not null
|
||||||
|
and registration_entities.registration_number is not null
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from eventhub.vehicle_registration registration
|
||||||
|
where registration.tenant_key = registration_entities.tenant_key
|
||||||
|
and registration.event_source_id = registration_entities.event_source_id
|
||||||
|
and (
|
||||||
|
registration.source_registration_entity_id = registration_entities.source_registration_entity_id
|
||||||
|
or (
|
||||||
|
registration.nation = registration_entities.nation
|
||||||
|
and registration.registration_number = registration_entities.registration_number
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
insert into eventhub.vehicle_registration_assignment(
|
||||||
|
id, tenant_key, event_source_id, vehicle_registration_id, vehicle_id,
|
||||||
|
valid_from, valid_to, source_updated_at, payload
|
||||||
|
)
|
||||||
|
select gen_random_uuid(),
|
||||||
|
relation.tenant_key,
|
||||||
|
relation.event_source_id,
|
||||||
|
registration.id,
|
||||||
|
vehicle.id,
|
||||||
|
relation.valid_from,
|
||||||
|
relation.valid_to,
|
||||||
|
relation.source_updated_at,
|
||||||
|
jsonb_build_object(
|
||||||
|
'source', 'master-data',
|
||||||
|
'sourceRelationId', relation.id,
|
||||||
|
'relationKey', relation.relation_key
|
||||||
|
)
|
||||||
|
from eventhub.source_master_relation relation
|
||||||
|
join eventhub.vehicle_registration registration on registration.tenant_key = relation.tenant_key
|
||||||
|
and registration.event_source_id = relation.event_source_id
|
||||||
|
and registration.source_registration_entity_id = relation.from_source_entity_id
|
||||||
|
join eventhub.vehicle vehicle on vehicle.tenant_key = relation.tenant_key
|
||||||
|
and vehicle.event_source_id = relation.event_source_id
|
||||||
|
and vehicle.source_vehicle_entity_id = relation.to_source_entity_id
|
||||||
|
where relation.relation_type = 'VEHICLE_REGISTRATION_VEHICLE'
|
||||||
|
and relation.from_entity_type = 'VEHICLE_REGISTRATION'
|
||||||
|
and relation.to_entity_type = 'VEHICLE'
|
||||||
|
and not exists (
|
||||||
|
select 1
|
||||||
|
from eventhub.vehicle_registration_assignment existing
|
||||||
|
where existing.vehicle_registration_id = registration.id
|
||||||
|
and existing.vehicle_id = vehicle.id
|
||||||
|
and existing.valid_from is not distinct from relation.valid_from
|
||||||
|
and existing.valid_to is not distinct from relation.valid_to
|
||||||
|
);
|
||||||
|
|
@ -8,16 +8,104 @@
|
||||||
* the best matching CardVehiclesUsed row for the activity timestamp when one is
|
* the best matching CardVehiclesUsed row for the activity timestamp when one is
|
||||||
* available.
|
* available.
|
||||||
*/
|
*/
|
||||||
select
|
with OrgTree as (
|
||||||
cast(ca.ID as varchar(128)) as source_row_id,
|
select org.I_90021_OID
|
||||||
cast(ca.ID as varchar(128)) as card_activity_id,
|
from dbo.GetOrganisationTree(null, :organisationId, 0, null) org
|
||||||
concat('TACHOGRAPH:CARD_ACTIVITY:', ca.ID) as external_source_event_id,
|
where :organisationId is not null
|
||||||
|
)
|
||||||
|
,
|
||||||
|
CandidateActivity as (
|
||||||
|
select
|
||||||
|
ca.ID,
|
||||||
|
ca.BeginTime,
|
||||||
|
activity_time.EndTime,
|
||||||
|
ca.Activity,
|
||||||
|
ca.Slot,
|
||||||
|
ca.CardStatus,
|
||||||
|
ca.DrivingStatus,
|
||||||
|
ca.ID_FileLog,
|
||||||
|
cda.RecordDate,
|
||||||
|
cda.RecordDateTo,
|
||||||
|
cda.ID_FileLog as cda_filelog_id,
|
||||||
|
c.ID as card_id,
|
||||||
|
c.ID_Driver as driver_id,
|
||||||
|
c.ID_Nation as driver_card_nation_id,
|
||||||
|
c.CardNumber as driver_card_number,
|
||||||
|
c.ID_FileLog as card_filelog_id,
|
||||||
|
coalesce(ca.ID_FileLog, cda.ID_FileLog, c.ID_FileLog) as file_log_id
|
||||||
|
from dbo.CardActivity ca
|
||||||
|
join dbo.CardDailyActivity cda on cda.ID = ca.ID_DailyActivity
|
||||||
|
join dbo.Card c on c.ID = cda.ID_Card
|
||||||
|
cross apply (values (dateadd(minute, coalesce(ca.Duration, 0), ca.BeginTime))) activity_time(EndTime)
|
||||||
|
where (:occurredTo is null or ca.BeginTime < :occurredTo)
|
||||||
|
and (
|
||||||
|
:occurredFrom is null
|
||||||
|
or ca.BeginTime >= :occurredFrom
|
||||||
|
or activity_time.EndTime >= :occurredFrom
|
||||||
|
)
|
||||||
|
and (
|
||||||
|
:organisationId is null
|
||||||
|
or exists (
|
||||||
|
select 1
|
||||||
|
from dbo.Driver_I_90021 rel
|
||||||
|
join OrgTree on OrgTree.I_90021_OID = rel.ID_I_90021
|
||||||
|
where rel.ID_Driver = c.ID_Driver
|
||||||
|
and rel.GILT_BIS is null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
,
|
||||||
|
Base as (
|
||||||
|
select
|
||||||
|
ca.ID,
|
||||||
|
ca.BeginTime,
|
||||||
|
ca.EndTime,
|
||||||
|
ca.Activity,
|
||||||
|
ca.Slot,
|
||||||
|
ca.CardStatus,
|
||||||
|
ca.DrivingStatus,
|
||||||
|
ca.ID_FileLog,
|
||||||
|
ca.cda_filelog_id,
|
||||||
|
ca.card_filelog_id,
|
||||||
|
|
||||||
ca.BeginTime as occurred_at,
|
coalesce(fl.DownloadDate, fl.OriginalDownloadDate, fl.TStamp, fl.CreationDate) as received_partner_at,
|
||||||
coalesce(fl.DownloadDate, fl.OriginalDownloadDate, fl.TStamp, fl.CreationDate) as received_partner_at,
|
coalesce(fl.ID, ca.ID_FileLog, ca.cda_filelog_id, ca.card_filelog_id, ca.ID) as source_package_id_raw,
|
||||||
ca.Activity as activity_code,
|
coalesce(fl.ID_Card, ca.card_id) as source_package_entity_id_raw,
|
||||||
ca.Activity as activity_text,
|
coalesce(fl.DownloadFrom, ca.RecordDate) as source_package_period_from,
|
||||||
case upper(coalesce(ca.Activity, ''))
|
coalesce(fl.DownloadTo, ca.RecordDateTo, dateadd(day, 1, ca.RecordDate)) as source_package_period_to,
|
||||||
|
coalesce(fl.CreationDate, fl.TStamp) as source_package_imported_at,
|
||||||
|
|
||||||
|
ca.driver_id,
|
||||||
|
cn.AlphaCode as driver_card_nation,
|
||||||
|
ca.driver_card_number,
|
||||||
|
coalesce(cvu.ID_Vehicle, v.ID) as vehicle_registration_id,
|
||||||
|
null as vehicle_vin,
|
||||||
|
vn.AlphaCode as vehicle_registration_nation,
|
||||||
|
v.VRN as vehicle_registration_number
|
||||||
|
from CandidateActivity ca
|
||||||
|
left join dbo.FileLog fl on fl.ID = ca.file_log_id
|
||||||
|
left join dbo.Nation cn on cn.ID = ca.driver_card_nation_id
|
||||||
|
outer apply (
|
||||||
|
select top 1 used.ID_Vehicle
|
||||||
|
from dbo.CardVehiclesUsed used
|
||||||
|
where used.ID_Card = ca.card_id
|
||||||
|
and (used.FirstUse is null or used.FirstUse <= ca.BeginTime)
|
||||||
|
and (used.LastUse is null or used.LastUse >= ca.BeginTime)
|
||||||
|
order by
|
||||||
|
used.FirstUse desc,
|
||||||
|
used.ID desc
|
||||||
|
) cvu
|
||||||
|
left join dbo.Vehicle v on v.ID = cvu.ID_Vehicle
|
||||||
|
left join dbo.Nation vn on vn.ID = v.ID_Nation
|
||||||
|
)
|
||||||
|
select
|
||||||
|
cast(base.ID as varchar(128)) as source_row_id,
|
||||||
|
concat('TACHOGRAPH:CARD_ACTIVITY:', base.ID, ':', evt.lifecycle) as external_source_event_id,
|
||||||
|
|
||||||
|
evt.occurred_at as occurred_at,
|
||||||
|
base.received_partner_at,
|
||||||
|
base.Activity as activity_code,
|
||||||
|
case upper(coalesce(base.Activity, ''))
|
||||||
when 'DRIVING' then 'DRIVE'
|
when 'DRIVING' then 'DRIVE'
|
||||||
when 'DRIVE' then 'DRIVE'
|
when 'DRIVE' then 'DRIVE'
|
||||||
when 'WORK' then 'WORK'
|
when 'WORK' then 'WORK'
|
||||||
|
|
@ -28,67 +116,35 @@ select
|
||||||
when 'REST' then 'BREAK_REST'
|
when 'REST' then 'BREAK_REST'
|
||||||
else 'UNKNOWN_ACTIVITY'
|
else 'UNKNOWN_ACTIVITY'
|
||||||
end as event_type,
|
end as event_type,
|
||||||
'SNAPSHOT' as lifecycle,
|
evt.lifecycle as lifecycle,
|
||||||
ca.Slot as card_slot,
|
base.Slot as card_slot,
|
||||||
ca.CardStatus as card_status,
|
base.CardStatus as card_status,
|
||||||
ca.DrivingStatus as driving_status,
|
base.DrivingStatus as driving_status,
|
||||||
cast(null as bigint) as odometer_m,
|
cast(null as bigint) as odometer_m,
|
||||||
|
|
||||||
cast(d.ID as varchar(128)) as driver_source_entity_id,
|
cast(base.driver_id as varchar(128)) as driver_source_entity_id,
|
||||||
cn.AlphaCode as driver_card_nation,
|
base.driver_card_nation,
|
||||||
c.CardNumber as driver_card_number,
|
base.driver_card_number,
|
||||||
|
|
||||||
cast(coalesce(cvu.ID_Vehicle, v.ID) as varchar(128)) as vehicle_source_entity_id,
|
cast(null as varchar(128)) as vehicle_source_entity_id,
|
||||||
coalesce(cvu.VIN, vi.VIN) as vehicle_vin,
|
base.vehicle_vin,
|
||||||
vn.AlphaCode as vehicle_registration_nation,
|
cast(base.vehicle_registration_id as varchar(128)) as vehicle_registration_source_entity_id,
|
||||||
v.VRN as vehicle_registration_number,
|
base.vehicle_registration_nation,
|
||||||
|
base.vehicle_registration_number,
|
||||||
|
|
||||||
'DRIVER_CARD' as source_package_kind,
|
'DRIVER_CARD' as source_package_kind,
|
||||||
cast(coalesce(fl.ID, ca.ID_FileLog, cda.ID_FileLog, c.ID_FileLog, ca.ID) as varchar(128)) as source_package_id,
|
cast(base.source_package_id_raw as varchar(128)) as source_package_id,
|
||||||
cast(coalesce(fl.ID_Card, c.ID) as varchar(128)) as source_package_entity_id,
|
cast(base.source_package_entity_id_raw as varchar(128)) as source_package_entity_id,
|
||||||
coalesce(fl.DownloadFrom, cda.RecordDate) as source_package_period_from,
|
base.source_package_period_from,
|
||||||
coalesce(fl.DownloadTo, cda.RecordDateTo, dateadd(day, 1, cda.RecordDate)) as source_package_period_to,
|
base.source_package_period_to,
|
||||||
coalesce(fl.CreationDate, fl.TStamp) as source_package_imported_at
|
base.source_package_imported_at
|
||||||
from dbo.CardActivity ca
|
from Base base
|
||||||
join dbo.CardDailyActivity cda on cda.ID = ca.ID_DailyActivity
|
cross apply (values
|
||||||
join dbo.Card c on c.ID = cda.ID_Card
|
('START', base.BeginTime),
|
||||||
left join dbo.FileLog fl on fl.ID = coalesce(ca.ID_FileLog, cda.ID_FileLog, c.ID_FileLog)
|
('END', base.EndTime)
|
||||||
left join dbo.Driver d on d.ID = c.ID_Driver
|
) evt(lifecycle, occurred_at)
|
||||||
left join dbo.Nation cn on cn.ID = c.ID_Nation
|
where (:occurredFrom is null or evt.occurred_at >= :occurredFrom)
|
||||||
outer apply (
|
and (:occurredTo is null or evt.occurred_at < :occurredTo)
|
||||||
select top 1 used.ID_Vehicle,
|
|
||||||
used.VIN,
|
|
||||||
used.OdoBegin
|
|
||||||
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 coalesce(fl.CreationDate, fl.TStamp) > :lastSourcePackageImportedAt
|
|
||||||
or (
|
|
||||||
coalesce(fl.CreationDate, fl.TStamp) = :lastSourcePackageImportedAt
|
|
||||||
and coalesce(fl.ID, ca.ID_FileLog, cda.ID_FileLog, c.ID_FileLog, ca.ID) > try_convert(int, :lastSourcePackageId)
|
|
||||||
)
|
|
||||||
or (
|
|
||||||
coalesce(fl.CreationDate, fl.TStamp) is null
|
|
||||||
and (
|
|
||||||
:lastSourcePackageId is null
|
|
||||||
or coalesce(fl.ID, ca.ID_FileLog, cda.ID_FileLog, c.ID_FileLog, ca.ID) > try_convert(int, :lastSourcePackageId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
/*
|
/*
|
||||||
* Organisation filtering can use FileLog.I_90021_ID / FileLog.OrgID or
|
* Organisation filter: driver membership in GetOrganisationTree(null, :organisationId, 0, null).
|
||||||
* Driver_I_90021 / Vehicle_I_90021 once subtree semantics are confirmed.
|
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -4,8 +4,8 @@ select
|
||||||
concat(coalesce(n.AlphaCode, ''), ':', v.VRN) as source_external_key,
|
concat(coalesce(n.AlphaCode, ''), ':', v.VRN) as source_external_key,
|
||||||
concat(coalesce(n.AlphaCode, ''), ':', v.VRN) as display_name,
|
concat(coalesce(n.AlphaCode, ''), ':', v.VRN) as display_name,
|
||||||
cast(case when v.ValidTo is null or v.ValidTo > getutcdate() then 1 else 0 end as bit) as active,
|
cast(case when v.ValidTo is null or v.ValidTo > getutcdate() then 1 else 0 end as bit) as active,
|
||||||
v.ValidFrom as valid_from,
|
cast(null as datetime) as valid_from,
|
||||||
v.ValidTo as valid_to,
|
cast(null as datetime) as valid_to,
|
||||||
cast(null as datetime) as source_updated_at,
|
cast(null as datetime) as source_updated_at,
|
||||||
v.ID as vehicle_registration_id,
|
v.ID as vehicle_registration_id,
|
||||||
cast(v.ID_VehicleIdentification as varchar(128)) as vehicle_identification_id,
|
cast(v.ID_VehicleIdentification as varchar(128)) as vehicle_identification_id,
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,133 @@
|
||||||
* VUActivity -> VUDailyActivity -> VUInstallation -> VehicleIdentification
|
* VUActivity -> VUDailyActivity -> VUInstallation -> VehicleIdentification
|
||||||
* Optional driver/card context comes from VUActivity.ID_IWCycle -> IWCycle -> Card.
|
* Optional driver/card context comes from VUActivity.ID_IWCycle -> IWCycle -> Card.
|
||||||
*/
|
*/
|
||||||
select
|
with OrgTree as (
|
||||||
cast(va.ID as varchar(128)) as source_row_id,
|
select org.I_90021_OID
|
||||||
cast(va.ID as varchar(128)) as vu_activity_id,
|
from dbo.GetOrganisationTree(null, :organisationId, 0, null) org
|
||||||
concat('TACHOGRAPH:VU_ACTIVITY:', va.ID) as external_source_event_id,
|
where :organisationId is not null
|
||||||
|
)
|
||||||
|
,
|
||||||
|
CandidateActivity as (
|
||||||
|
select
|
||||||
|
va.ID,
|
||||||
|
va.BeginTime,
|
||||||
|
activity_time.EndTime,
|
||||||
|
va.Activity,
|
||||||
|
va.Slot,
|
||||||
|
va.CardStatus,
|
||||||
|
va.DrivingStatus,
|
||||||
|
va.ID_FileLog,
|
||||||
|
va.ID_IWCycle,
|
||||||
|
vda.RecordDate,
|
||||||
|
vda.ID_FileLog as vda_filelog_id,
|
||||||
|
vui.ID_VehicleIdentification,
|
||||||
|
vui.ID_FileLog as vui_filelog_id,
|
||||||
|
vi.ID as vehicle_identification_id,
|
||||||
|
vi.VIN as vehicle_vin,
|
||||||
|
coalesce(va.ID_FileLog, vda.ID_FileLog, vui.ID_FileLog) as file_log_id
|
||||||
|
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
|
||||||
|
cross apply (values (dateadd(minute, coalesce(va.Duration, 0), va.BeginTime))) activity_time(EndTime)
|
||||||
|
where (:occurredTo is null or va.BeginTime < :occurredTo)
|
||||||
|
and (
|
||||||
|
:occurredFrom is null
|
||||||
|
or va.BeginTime >= :occurredFrom
|
||||||
|
or activity_time.EndTime >= :occurredFrom
|
||||||
|
)
|
||||||
|
)
|
||||||
|
,
|
||||||
|
CandidateVehicle as (
|
||||||
|
select
|
||||||
|
va.ID,
|
||||||
|
va.BeginTime,
|
||||||
|
va.EndTime,
|
||||||
|
va.Activity,
|
||||||
|
va.Slot,
|
||||||
|
va.CardStatus,
|
||||||
|
va.DrivingStatus,
|
||||||
|
va.ID_FileLog,
|
||||||
|
va.ID_IWCycle,
|
||||||
|
va.RecordDate,
|
||||||
|
va.vda_filelog_id,
|
||||||
|
va.ID_VehicleIdentification,
|
||||||
|
va.vui_filelog_id,
|
||||||
|
va.vehicle_identification_id,
|
||||||
|
va.vehicle_vin,
|
||||||
|
va.file_log_id,
|
||||||
|
v.ID as vehicle_registration_id,
|
||||||
|
v.ID_Nation as vehicle_registration_nation_id,
|
||||||
|
v.VRN as vehicle_registration_number
|
||||||
|
from CandidateActivity va
|
||||||
|
outer apply (
|
||||||
|
select top 1 vehicle.ID,
|
||||||
|
vehicle.VRN,
|
||||||
|
vehicle.ID_Nation
|
||||||
|
from dbo.Vehicle vehicle
|
||||||
|
where vehicle.ID_VehicleIdentification = va.vehicle_identification_id
|
||||||
|
and (vehicle.ValidFrom is null or vehicle.ValidFrom <= va.BeginTime)
|
||||||
|
and (vehicle.ValidTo is null or vehicle.ValidTo > va.BeginTime)
|
||||||
|
order by
|
||||||
|
vehicle.ValidFrom desc,
|
||||||
|
vehicle.ID desc
|
||||||
|
) v
|
||||||
|
where (
|
||||||
|
:organisationId is null
|
||||||
|
or exists (
|
||||||
|
select 1
|
||||||
|
from dbo.Vehicle_I_90021 rel
|
||||||
|
join OrgTree on OrgTree.I_90021_OID = rel.ID_I_90021
|
||||||
|
where rel.ID_Vehicle = v.ID
|
||||||
|
and rel.GILT_BIS is null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
,
|
||||||
|
Base as (
|
||||||
|
select
|
||||||
|
va.ID,
|
||||||
|
va.BeginTime,
|
||||||
|
va.EndTime,
|
||||||
|
va.Activity,
|
||||||
|
va.Slot,
|
||||||
|
va.CardStatus,
|
||||||
|
va.DrivingStatus,
|
||||||
|
va.ID_FileLog,
|
||||||
|
va.vda_filelog_id,
|
||||||
|
va.vui_filelog_id,
|
||||||
|
|
||||||
va.BeginTime as occurred_at,
|
coalesce(fl.DownloadDate, fl.OriginalDownloadDate, fl.TStamp, fl.CreationDate) as received_partner_at,
|
||||||
coalesce(fl.DownloadDate, fl.OriginalDownloadDate, fl.TStamp, fl.CreationDate) as received_partner_at,
|
coalesce(fl.ID, va.ID_FileLog, va.vda_filelog_id, va.vui_filelog_id, va.ID) as source_package_id_raw,
|
||||||
va.Activity as activity_code,
|
coalesce(fl.ID_VehicleIdentification, va.ID_VehicleIdentification) as source_package_entity_id_raw,
|
||||||
va.Activity as activity_text,
|
coalesce(fl.DownloadFrom, va.RecordDate) as source_package_period_from,
|
||||||
case upper(coalesce(va.Activity, ''))
|
coalesce(fl.DownloadTo, dateadd(day, 1, va.RecordDate)) as source_package_period_to,
|
||||||
|
coalesce(fl.CreationDate, fl.TStamp) as source_package_imported_at,
|
||||||
|
|
||||||
|
null as odometer_m,
|
||||||
|
c.ID_Driver as driver_id,
|
||||||
|
cn.AlphaCode as driver_card_nation,
|
||||||
|
c.CardNumber as driver_card_number,
|
||||||
|
va.vehicle_identification_id,
|
||||||
|
va.vehicle_registration_id,
|
||||||
|
va.vehicle_vin,
|
||||||
|
vn.AlphaCode as vehicle_registration_nation,
|
||||||
|
va.vehicle_registration_number
|
||||||
|
from CandidateVehicle va
|
||||||
|
left join dbo.FileLog fl on fl.ID = va.file_log_id
|
||||||
|
left join dbo.Nation vn on vn.ID = va.vehicle_registration_nation_id
|
||||||
|
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.Nation cn on cn.ID = c.ID_Nation
|
||||||
|
)
|
||||||
|
select
|
||||||
|
cast(base.ID as varchar(128)) as source_row_id,
|
||||||
|
concat('TACHOGRAPH:VU_ACTIVITY:', base.ID, ':', evt.lifecycle) as external_source_event_id,
|
||||||
|
|
||||||
|
evt.occurred_at as occurred_at,
|
||||||
|
base.received_partner_at,
|
||||||
|
base.Activity as activity_code,
|
||||||
|
case upper(coalesce(base.Activity, ''))
|
||||||
when 'DRIVING' then 'DRIVE'
|
when 'DRIVING' then 'DRIVE'
|
||||||
when 'DRIVE' then 'DRIVE'
|
when 'DRIVE' then 'DRIVE'
|
||||||
when 'WORK' then 'WORK'
|
when 'WORK' then 'WORK'
|
||||||
|
|
@ -25,68 +142,35 @@ select
|
||||||
when 'REST' then 'BREAK_REST'
|
when 'REST' then 'BREAK_REST'
|
||||||
else 'UNKNOWN_ACTIVITY'
|
else 'UNKNOWN_ACTIVITY'
|
||||||
end as event_type,
|
end as event_type,
|
||||||
'SNAPSHOT' as lifecycle,
|
evt.lifecycle as lifecycle,
|
||||||
va.Slot as card_slot,
|
base.Slot as card_slot,
|
||||||
va.CardStatus as card_status,
|
base.CardStatus as card_status,
|
||||||
va.DrivingStatus as driving_status,
|
base.DrivingStatus as driving_status,
|
||||||
cast(iw.OdoBegin as bigint) as odometer_m,
|
base.odometer_m,
|
||||||
|
|
||||||
cast(d.ID as varchar(128)) as driver_source_entity_id,
|
cast(base.driver_id as varchar(128)) as driver_source_entity_id,
|
||||||
cn.AlphaCode as driver_card_nation,
|
base.driver_card_nation,
|
||||||
c.CardNumber as driver_card_number,
|
base.driver_card_number,
|
||||||
|
|
||||||
cast(v.ID as varchar(128)) as vehicle_source_entity_id,
|
cast(base.vehicle_identification_id as varchar(128)) as vehicle_source_entity_id,
|
||||||
vi.VIN as vehicle_vin,
|
base.vehicle_vin,
|
||||||
vn.AlphaCode as vehicle_registration_nation,
|
cast(base.vehicle_registration_id as varchar(128)) as vehicle_registration_source_entity_id,
|
||||||
v.VRN as vehicle_registration_number,
|
base.vehicle_registration_nation,
|
||||||
|
base.vehicle_registration_number,
|
||||||
|
|
||||||
'VEHICLE_UNIT' as source_package_kind,
|
'VEHICLE_UNIT' as source_package_kind,
|
||||||
cast(coalesce(fl.ID, va.ID_FileLog, vda.ID_FileLog, vui.ID_FileLog, va.ID) as varchar(128)) as source_package_id,
|
cast(base.source_package_id_raw as varchar(128)) as source_package_id,
|
||||||
cast(coalesce(fl.ID_VehicleIdentification, vui.ID_VehicleIdentification) as varchar(128)) as source_package_entity_id,
|
cast(base.source_package_entity_id_raw as varchar(128)) as source_package_entity_id,
|
||||||
coalesce(fl.DownloadFrom, vda.RecordDate) as source_package_period_from,
|
base.source_package_period_from,
|
||||||
coalesce(fl.DownloadTo, dateadd(day, 1, vda.RecordDate)) as source_package_period_to,
|
base.source_package_period_to,
|
||||||
coalesce(fl.CreationDate, fl.TStamp) as source_package_imported_at
|
base.source_package_imported_at
|
||||||
from dbo.VUActivity va
|
from Base base
|
||||||
join dbo.VUDailyActivity vda on vda.ID = va.ID_VUDailyActivity
|
cross apply (values
|
||||||
join dbo.VUInstallation vui on vui.ID = vda.ID_VUInstallation
|
('START', base.BeginTime),
|
||||||
left join dbo.FileLog fl on fl.ID = coalesce(va.ID_FileLog, vda.ID_FileLog, vui.ID_FileLog)
|
('END', base.EndTime)
|
||||||
join dbo.VehicleIdentification vi on vi.ID = vui.ID_VehicleIdentification
|
) evt(lifecycle, occurred_at)
|
||||||
outer apply (
|
where (:occurredFrom is null or evt.occurred_at >= :occurredFrom)
|
||||||
select top 1 vehicle.ID,
|
and (:occurredTo is null or evt.occurred_at < :occurredTo)
|
||||||
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 coalesce(fl.CreationDate, fl.TStamp) > :lastSourcePackageImportedAt
|
|
||||||
or (
|
|
||||||
coalesce(fl.CreationDate, fl.TStamp) = :lastSourcePackageImportedAt
|
|
||||||
and coalesce(fl.ID, va.ID_FileLog, vda.ID_FileLog, vui.ID_FileLog, va.ID) > try_convert(int, :lastSourcePackageId)
|
|
||||||
)
|
|
||||||
or (
|
|
||||||
coalesce(fl.CreationDate, fl.TStamp) is null
|
|
||||||
and (
|
|
||||||
:lastSourcePackageId is null
|
|
||||||
or coalesce(fl.ID, va.ID_FileLog, vda.ID_FileLog, vui.ID_FileLog, va.ID) > try_convert(int, :lastSourcePackageId)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
/*
|
/*
|
||||||
* Organisation filtering can use FileLog.I_90021_ID / FileLog.OrgID or
|
* Organisation filter: vehicle membership in GetOrganisationTree(null, :organisationId, 0, null).
|
||||||
* Vehicle_I_90021 / Driver_I_90021 once subtree semantics are confirmed.
|
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
package at.procon.eventhub.tachograph.service;
|
||||||
|
|
||||||
|
import at.procon.eventhub.config.EventHubProperties;
|
||||||
|
import at.procon.eventhub.dto.EventFamily;
|
||||||
|
import at.procon.eventhub.dto.EventSourceDto;
|
||||||
|
import at.procon.eventhub.dto.ImportScopeDto;
|
||||||
|
import at.procon.eventhub.importing.ImportChunkPlanner;
|
||||||
|
import at.procon.eventhub.service.EventDetailsFactory;
|
||||||
|
import at.procon.eventhub.tachograph.dto.TachographImportRequest;
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.List;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||||
|
|
||||||
|
class TachographImportPlanServiceTest {
|
||||||
|
|
||||||
|
private final EventDetailsFactory detailsFactory = new EventDetailsFactory(new ObjectMapper());
|
||||||
|
private final TachographExtractionDefinitionRegistry definitionRegistry = new TachographExtractionDefinitionRegistry(
|
||||||
|
new CardActivityRowMapper(detailsFactory),
|
||||||
|
new VuActivityRowMapper(detailsFactory)
|
||||||
|
);
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rejectsUnsupportedEventFamiliesWhenJdbcExtractionIsEnabled() {
|
||||||
|
TachographImportPlanService service = serviceWithJdbcExtractor();
|
||||||
|
TachographImportRequest request = requestForFamilies(EventFamily.DRIVER_CARD);
|
||||||
|
|
||||||
|
assertThatThrownBy(() -> service.createPlan(request))
|
||||||
|
.isInstanceOf(UnsupportedTachographExtractionException.class)
|
||||||
|
.hasMessageContaining("IW_CYCLE")
|
||||||
|
.hasMessageContaining("Supported JDBC extraction codes")
|
||||||
|
.hasMessageContaining("DRIVER_ACTIVITY");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void allowsSupportedDriverActivityFamilyWhenJdbcExtractionIsEnabled() {
|
||||||
|
TachographImportPlanService service = serviceWithJdbcExtractor();
|
||||||
|
TachographImportRequest request = requestForFamilies(EventFamily.DRIVER_ACTIVITY);
|
||||||
|
|
||||||
|
var plan = service.createPlan(request);
|
||||||
|
|
||||||
|
assertThat(plan.items())
|
||||||
|
.extracting(item -> item.extractionCode())
|
||||||
|
.containsExactlyInAnyOrder("VU_ACTIVITY", "CARD_ACTIVITY");
|
||||||
|
}
|
||||||
|
|
||||||
|
private TachographImportPlanService serviceWithJdbcExtractor() {
|
||||||
|
EventHubProperties properties = new EventHubProperties();
|
||||||
|
properties.getTachograph().getDatasource().setJdbcUrl("jdbc:sqlserver://tachograph-db");
|
||||||
|
return new TachographImportPlanService(properties, new ImportChunkPlanner(), definitionRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private TachographImportRequest requestForFamilies(EventFamily... families) {
|
||||||
|
EnumSet<EventFamily> selectedFamilies = families.length == 0
|
||||||
|
? EnumSet.noneOf(EventFamily.class)
|
||||||
|
: EnumSet.copyOf(List.of(families));
|
||||||
|
return new TachographImportRequest(
|
||||||
|
"tenant-1",
|
||||||
|
new EventSourceDto("TACHOGRAPH", "MIXED", "TACHOGRAPH_DB", "tachograph-db", null, null),
|
||||||
|
null,
|
||||||
|
ImportScopeDto.tenantAll(null, null),
|
||||||
|
selectedFamilies,
|
||||||
|
null,
|
||||||
|
false,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue