introduced TIME-domain foundation
This commit is contained in:
parent
439a06d633
commit
8b22e96597
|
|
@ -0,0 +1,53 @@
|
||||||
|
# TIME Phase T1 Foundation
|
||||||
|
|
||||||
|
This phase introduces only the **shared TIME-domain foundation** in the NEW runtime.
|
||||||
|
|
||||||
|
## Included
|
||||||
|
|
||||||
|
- `TIME` schema
|
||||||
|
- common root fact table: `TIME.time_entry`
|
||||||
|
- reverse-link helper table: `TIME.time_entry_source_link`
|
||||||
|
- synchronization audit/state tables:
|
||||||
|
- `TIME.time_sync_run`
|
||||||
|
- `TIME.time_sync_state`
|
||||||
|
- NEW runtime configuration scaffold under `dip.time.*`
|
||||||
|
- new canonical document markers:
|
||||||
|
- `DocumentType.TIME_ENTRY`
|
||||||
|
- `DocumentFamily.TIME`
|
||||||
|
|
||||||
|
## Not included yet
|
||||||
|
|
||||||
|
- Leitstand import
|
||||||
|
- Toggl import
|
||||||
|
- search projection
|
||||||
|
- text representations
|
||||||
|
- embeddings
|
||||||
|
- structured search
|
||||||
|
|
||||||
|
## Design intent
|
||||||
|
|
||||||
|
The TIME domain uses:
|
||||||
|
|
||||||
|
- shared generic canonical documents in `DOC.*`
|
||||||
|
- source-specific imported tables later (`TIME.ls_*`, `TIME.toggl_*`)
|
||||||
|
- one shared canonical root fact per time entry in `TIME.time_entry`
|
||||||
|
|
||||||
|
This allows later source-specific import without forcing all external systems into one prematurely normalized source model.
|
||||||
|
|
||||||
|
## Tables
|
||||||
|
|
||||||
|
### `TIME.time_entry`
|
||||||
|
Shared canonical time-entry fact, linked 1:1 to `DOC.doc_document`.
|
||||||
|
|
||||||
|
### `TIME.time_entry_source_link`
|
||||||
|
Maps a time entry to one or more source-system entity ids such as root entry id, task id, cost-unit id, etc.
|
||||||
|
|
||||||
|
### `TIME.time_sync_run`
|
||||||
|
Stores audit information for one sync/import run.
|
||||||
|
|
||||||
|
### `TIME.time_sync_state`
|
||||||
|
Stores mutable cursor/watermark state per source system and scope.
|
||||||
|
|
||||||
|
## Next phase
|
||||||
|
|
||||||
|
T2 adds Leitstand source import tables and importer logic.
|
||||||
|
|
@ -7,6 +7,7 @@ public final class SchemaNames {
|
||||||
|
|
||||||
public static final String DOC = "DOC";
|
public static final String DOC = "DOC";
|
||||||
public static final String TED = "TED";
|
public static final String TED = "TED";
|
||||||
|
public static final String TIME = "TIME";
|
||||||
|
|
||||||
private SchemaNames() {
|
private SchemaNames() {
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ package at.procon.dip.domain.document;
|
||||||
*/
|
*/
|
||||||
public enum DocumentFamily {
|
public enum DocumentFamily {
|
||||||
PROCUREMENT,
|
PROCUREMENT,
|
||||||
|
TIME,
|
||||||
MAIL,
|
MAIL,
|
||||||
ATTACHMENT,
|
ATTACHMENT,
|
||||||
KNOWLEDGE,
|
KNOWLEDGE,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ package at.procon.dip.domain.document;
|
||||||
public enum DocumentType {
|
public enum DocumentType {
|
||||||
TED_PACKAGE,
|
TED_PACKAGE,
|
||||||
TED_NOTICE,
|
TED_NOTICE,
|
||||||
|
TIME_ENTRY,
|
||||||
EMAIL,
|
EMAIL,
|
||||||
MIME_MESSAGE,
|
MIME_MESSAGE,
|
||||||
PDF,
|
PDF,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,26 @@
|
||||||
|
package at.procon.dip.domain.time.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NEW-runtime configuration scaffold for the TIME domain.
|
||||||
|
* T1 only introduces schema and persistence foundations; source import starts in T2.
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@ConfigurationProperties(prefix = "dip.time")
|
||||||
|
@Data
|
||||||
|
public class TimeDomainProperties {
|
||||||
|
|
||||||
|
private boolean enabled = false;
|
||||||
|
private SourceProperties leitstand = new SourceProperties();
|
||||||
|
private SourceProperties togglTrack = new SourceProperties();
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class SourceProperties {
|
||||||
|
private boolean enabled = false;
|
||||||
|
private String importBatchId;
|
||||||
|
private int reconcileLookbackDays = 7;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,117 @@
|
||||||
|
package at.procon.dip.domain.time.entity;
|
||||||
|
|
||||||
|
import at.procon.dip.architecture.SchemaNames;
|
||||||
|
import at.procon.dip.domain.document.entity.Document;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Index;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.OneToOne;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.PreUpdate;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shared canonical time-entry fact that can be sourced from multiple external systems.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(schema = SchemaNames.TIME, name = "time_entry", indexes = {
|
||||||
|
@Index(name = "idx_time_entry_source_system", columnList = "source_system"),
|
||||||
|
@Index(name = "idx_time_entry_external_id", columnList = "external_id"),
|
||||||
|
@Index(name = "idx_time_entry_start", columnList = "entry_start"),
|
||||||
|
@Index(name = "idx_time_entry_end", columnList = "entry_end"),
|
||||||
|
@Index(name = "idx_time_entry_source_updated", columnList = "source_updated_at")
|
||||||
|
})
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class TimeEntry {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@OneToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "document_id", nullable = false, unique = true)
|
||||||
|
private Document document;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "source_system", nullable = false, length = 32)
|
||||||
|
private TimeSourceSystem sourceSystem;
|
||||||
|
|
||||||
|
@Column(name = "external_id", nullable = false, length = 255)
|
||||||
|
private String externalId;
|
||||||
|
|
||||||
|
@Column(name = "person_external_id", length = 255)
|
||||||
|
private String personExternalId;
|
||||||
|
|
||||||
|
@Column(name = "person_display_name", length = 255)
|
||||||
|
private String personDisplayName;
|
||||||
|
|
||||||
|
@Column(name = "entry_start")
|
||||||
|
private OffsetDateTime entryStart;
|
||||||
|
|
||||||
|
@Column(name = "entry_end")
|
||||||
|
private OffsetDateTime entryEnd;
|
||||||
|
|
||||||
|
@Column(name = "duration_seconds")
|
||||||
|
private Long durationSeconds;
|
||||||
|
|
||||||
|
@Column(name = "description_short", length = 1000)
|
||||||
|
private String descriptionShort;
|
||||||
|
|
||||||
|
@Column(name = "description_long", columnDefinition = "TEXT")
|
||||||
|
private String descriptionLong;
|
||||||
|
|
||||||
|
@Column(name = "billable")
|
||||||
|
private Boolean billable;
|
||||||
|
|
||||||
|
@Column(name = "source_created_at")
|
||||||
|
private OffsetDateTime sourceCreatedAt;
|
||||||
|
|
||||||
|
@Column(name = "source_updated_at")
|
||||||
|
private OffsetDateTime sourceUpdatedAt;
|
||||||
|
|
||||||
|
@Column(name = "source_deleted_at")
|
||||||
|
private OffsetDateTime sourceDeletedAt;
|
||||||
|
|
||||||
|
@Column(name = "raw_status", length = 120)
|
||||||
|
private String rawStatus;
|
||||||
|
|
||||||
|
@Column(name = "search_anchor_label", length = 500)
|
||||||
|
private String searchAnchorLabel;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private OffsetDateTime createdAt = OffsetDateTime.now();
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
private OffsetDateTime updatedAt = OffsetDateTime.now();
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = OffsetDateTime.now();
|
||||||
|
updatedAt = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
updatedAt = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
package at.procon.dip.domain.time.entity;
|
||||||
|
|
||||||
|
import at.procon.dip.architecture.SchemaNames;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Index;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.PreUpdate;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse-link helper that maps a canonical time entry to one or more source-system entity identifiers.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(schema = SchemaNames.TIME, name = "time_entry_source_link", indexes = {
|
||||||
|
@Index(name = "idx_time_entry_link_entry", columnList = "time_entry_id"),
|
||||||
|
@Index(name = "idx_time_entry_link_source_lookup", columnList = "source_system, source_entity_type, source_external_id"),
|
||||||
|
@Index(name = "idx_time_entry_link_role", columnList = "linked_role")
|
||||||
|
})
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class TimeEntrySourceLink {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY, optional = false)
|
||||||
|
@JoinColumn(name = "time_entry_id", nullable = false)
|
||||||
|
private TimeEntry timeEntry;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "source_system", nullable = false, length = 32)
|
||||||
|
private TimeSourceSystem sourceSystem;
|
||||||
|
|
||||||
|
@Column(name = "source_entity_type", nullable = false, length = 120)
|
||||||
|
private String sourceEntityType;
|
||||||
|
|
||||||
|
@Column(name = "source_external_id", nullable = false, length = 255)
|
||||||
|
private String sourceExternalId;
|
||||||
|
|
||||||
|
@Column(name = "linked_role", length = 120)
|
||||||
|
private String linkedRole;
|
||||||
|
|
||||||
|
@Column(name = "parent_source_external_id", length = 255)
|
||||||
|
private String parentSourceExternalId;
|
||||||
|
|
||||||
|
@Column(name = "source_updated_at")
|
||||||
|
private OffsetDateTime sourceUpdatedAt;
|
||||||
|
|
||||||
|
@Column(name = "source_deleted_at")
|
||||||
|
private OffsetDateTime sourceDeletedAt;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private OffsetDateTime createdAt = OffsetDateTime.now();
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
private OffsetDateTime updatedAt = OffsetDateTime.now();
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = OffsetDateTime.now();
|
||||||
|
updatedAt = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
updatedAt = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package at.procon.dip.domain.time.entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* External source systems for imported time-entry data.
|
||||||
|
*/
|
||||||
|
public enum TimeSourceSystem {
|
||||||
|
LEITSTAND,
|
||||||
|
TOGGL_TRACK
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
package at.procon.dip.domain.time.entity;
|
||||||
|
|
||||||
|
import at.procon.dip.architecture.SchemaNames;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Index;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.PreUpdate;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Audit row for one synchronization run of a time-entry source system.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(schema = SchemaNames.TIME, name = "time_sync_run", indexes = {
|
||||||
|
@Index(name = "idx_time_sync_run_source_system", columnList = "source_system"),
|
||||||
|
@Index(name = "idx_time_sync_run_scope_key", columnList = "scope_key"),
|
||||||
|
@Index(name = "idx_time_sync_run_status", columnList = "status"),
|
||||||
|
@Index(name = "idx_time_sync_run_started_at", columnList = "started_at")
|
||||||
|
})
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class TimeSyncRun {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "source_system", nullable = false, length = 32)
|
||||||
|
private TimeSourceSystem sourceSystem;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "run_type", nullable = false, length = 32)
|
||||||
|
private TimeSyncRunType runType;
|
||||||
|
|
||||||
|
@Column(name = "scope_key", nullable = false, length = 255)
|
||||||
|
private String scopeKey;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "status", nullable = false, length = 32)
|
||||||
|
private TimeSyncRunStatus status;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "started_at", nullable = false)
|
||||||
|
private OffsetDateTime startedAt = OffsetDateTime.now();
|
||||||
|
|
||||||
|
@Column(name = "finished_at")
|
||||||
|
private OffsetDateTime finishedAt;
|
||||||
|
|
||||||
|
@Column(name = "watermark_from", length = 255)
|
||||||
|
private String watermarkFrom;
|
||||||
|
|
||||||
|
@Column(name = "watermark_to", length = 255)
|
||||||
|
private String watermarkTo;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "rows_read", nullable = false)
|
||||||
|
private Integer rowsRead = 0;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "rows_created", nullable = false)
|
||||||
|
private Integer rowsCreated = 0;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "rows_updated", nullable = false)
|
||||||
|
private Integer rowsUpdated = 0;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "rows_deactivated", nullable = false)
|
||||||
|
private Integer rowsDeactivated = 0;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "rows_failed", nullable = false)
|
||||||
|
private Integer rowsFailed = 0;
|
||||||
|
|
||||||
|
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||||
|
private String errorMessage;
|
||||||
|
|
||||||
|
@Column(name = "initiated_by", length = 120)
|
||||||
|
private String initiatedBy;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private OffsetDateTime createdAt = OffsetDateTime.now();
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = OffsetDateTime.now();
|
||||||
|
if (startedAt == null) {
|
||||||
|
startedAt = createdAt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
if (finishedAt == null && status != TimeSyncRunStatus.RUNNING) {
|
||||||
|
finishedAt = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,10 @@
|
||||||
|
package at.procon.dip.domain.time.entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lifecycle state of a time-domain synchronization run.
|
||||||
|
*/
|
||||||
|
public enum TimeSyncRunStatus {
|
||||||
|
RUNNING,
|
||||||
|
COMPLETED,
|
||||||
|
FAILED
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package at.procon.dip.domain.time.entity;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of synchronization run for imported time-entry source systems.
|
||||||
|
*/
|
||||||
|
public enum TimeSyncRunType {
|
||||||
|
INITIAL,
|
||||||
|
INCREMENTAL,
|
||||||
|
RECONCILE,
|
||||||
|
MANUAL
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
package at.procon.dip.domain.time.entity;
|
||||||
|
|
||||||
|
import at.procon.dip.architecture.SchemaNames;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.EnumType;
|
||||||
|
import jakarta.persistence.Enumerated;
|
||||||
|
import jakarta.persistence.FetchType;
|
||||||
|
import jakarta.persistence.GeneratedValue;
|
||||||
|
import jakarta.persistence.GenerationType;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.Index;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.PrePersist;
|
||||||
|
import jakarta.persistence.PreUpdate;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
import java.time.OffsetDateTime;
|
||||||
|
import java.util.UUID;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mutable synchronization cursor/state per source system and scope.
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(schema = SchemaNames.TIME, name = "time_sync_state", indexes = {
|
||||||
|
@Index(name = "idx_time_sync_state_source_system", columnList = "source_system"),
|
||||||
|
@Index(name = "idx_time_sync_state_scope_key", columnList = "scope_key")
|
||||||
|
})
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class TimeSyncState {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.UUID)
|
||||||
|
private UUID id;
|
||||||
|
|
||||||
|
@Enumerated(EnumType.STRING)
|
||||||
|
@Column(name = "source_system", nullable = false, length = 32)
|
||||||
|
private TimeSourceSystem sourceSystem;
|
||||||
|
|
||||||
|
@Column(name = "scope_key", nullable = false, length = 255)
|
||||||
|
private String scopeKey;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "enabled", nullable = false)
|
||||||
|
private boolean enabled = true;
|
||||||
|
|
||||||
|
@Column(name = "last_successful_sync_at")
|
||||||
|
private OffsetDateTime lastSuccessfulSyncAt;
|
||||||
|
|
||||||
|
@Column(name = "last_attempted_sync_at")
|
||||||
|
private OffsetDateTime lastAttemptedSyncAt;
|
||||||
|
|
||||||
|
@Column(name = "last_seen_watermark", length = 255)
|
||||||
|
private String lastSeenWatermark;
|
||||||
|
|
||||||
|
@Column(name = "last_seen_external_id", length = 255)
|
||||||
|
private String lastSeenExternalId;
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "last_run_id")
|
||||||
|
private TimeSyncRun lastRun;
|
||||||
|
|
||||||
|
@Column(name = "notes", columnDefinition = "TEXT")
|
||||||
|
private String notes;
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "created_at", nullable = false, updatable = false)
|
||||||
|
private OffsetDateTime createdAt = OffsetDateTime.now();
|
||||||
|
|
||||||
|
@Builder.Default
|
||||||
|
@Column(name = "updated_at", nullable = false)
|
||||||
|
private OffsetDateTime updatedAt = OffsetDateTime.now();
|
||||||
|
|
||||||
|
@PrePersist
|
||||||
|
protected void onCreate() {
|
||||||
|
createdAt = OffsetDateTime.now();
|
||||||
|
updatedAt = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreUpdate
|
||||||
|
protected void onUpdate() {
|
||||||
|
updatedAt = OffsetDateTime.now();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
package at.procon.dip.domain.time.repository;
|
||||||
|
|
||||||
|
import at.procon.dip.domain.time.entity.TimeEntry;
|
||||||
|
import at.procon.dip.domain.time.entity.TimeSourceSystem;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface TimeEntryRepository extends JpaRepository<TimeEntry, UUID> {
|
||||||
|
|
||||||
|
Optional<TimeEntry> findByDocument_Id(UUID documentId);
|
||||||
|
|
||||||
|
Optional<TimeEntry> findBySourceSystemAndExternalId(TimeSourceSystem sourceSystem, String externalId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
package at.procon.dip.domain.time.repository;
|
||||||
|
|
||||||
|
import at.procon.dip.domain.time.entity.TimeEntrySourceLink;
|
||||||
|
import at.procon.dip.domain.time.entity.TimeSourceSystem;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface TimeEntrySourceLinkRepository extends JpaRepository<TimeEntrySourceLink, UUID> {
|
||||||
|
|
||||||
|
List<TimeEntrySourceLink> findByTimeEntry_Id(UUID timeEntryId);
|
||||||
|
|
||||||
|
Optional<TimeEntrySourceLink> findBySourceSystemAndSourceEntityTypeAndSourceExternalId(
|
||||||
|
TimeSourceSystem sourceSystem, String sourceEntityType, String sourceExternalId);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
package at.procon.dip.domain.time.repository;
|
||||||
|
|
||||||
|
import at.procon.dip.domain.time.entity.TimeSourceSystem;
|
||||||
|
import at.procon.dip.domain.time.entity.TimeSyncRun;
|
||||||
|
import at.procon.dip.domain.time.entity.TimeSyncRunStatus;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface TimeSyncRunRepository extends JpaRepository<TimeSyncRun, UUID> {
|
||||||
|
|
||||||
|
List<TimeSyncRun> findTop20BySourceSystemOrderByStartedAtDesc(TimeSourceSystem sourceSystem);
|
||||||
|
|
||||||
|
List<TimeSyncRun> findBySourceSystemAndStatus(TimeSourceSystem sourceSystem, TimeSyncRunStatus status);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package at.procon.dip.domain.time.repository;
|
||||||
|
|
||||||
|
import at.procon.dip.domain.time.entity.TimeSourceSystem;
|
||||||
|
import at.procon.dip.domain.time.entity.TimeSyncState;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
public interface TimeSyncStateRepository extends JpaRepository<TimeSyncState, UUID> {
|
||||||
|
|
||||||
|
Optional<TimeSyncState> findBySourceSystemAndScopeKey(TimeSourceSystem sourceSystem, String scopeKey);
|
||||||
|
}
|
||||||
|
|
@ -175,6 +175,7 @@ public final class DocumentImportSupport {
|
||||||
public static DocumentFamily familyFor(DocumentType documentType) {
|
public static DocumentFamily familyFor(DocumentType documentType) {
|
||||||
return switch (documentType) {
|
return switch (documentType) {
|
||||||
case TED_PACKAGE, TED_NOTICE -> DocumentFamily.PROCUREMENT;
|
case TED_PACKAGE, TED_NOTICE -> DocumentFamily.PROCUREMENT;
|
||||||
|
case TIME_ENTRY -> DocumentFamily.TIME;
|
||||||
case EMAIL, MIME_MESSAGE -> DocumentFamily.MAIL;
|
case EMAIL, MIME_MESSAGE -> DocumentFamily.MAIL;
|
||||||
case PDF, DOCX, HTML, XML_GENERIC, TEXT, MARKDOWN, ZIP_ARCHIVE, GENERIC_BINARY, UNKNOWN ->
|
case PDF, DOCX, HTML, XML_GENERIC, TEXT, MARKDOWN, ZIP_ARCHIVE, GENERIC_BINARY, UNKNOWN ->
|
||||||
DocumentFamily.GENERIC;
|
DocumentFamily.GENERIC;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,165 @@
|
||||||
|
-- TIME Phase T1 foundation for shared time-entry structures in the NEW runtime.
|
||||||
|
-- No source import logic yet; this migration only creates the shared TIME schema and base state tables.
|
||||||
|
|
||||||
|
CREATE SCHEMA IF NOT EXISTS TIME;
|
||||||
|
|
||||||
|
SET search_path TO TIME, DOC, public;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_type t
|
||||||
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||||
|
WHERE t.typname = 'time_source_system' AND n.nspname = 'time'
|
||||||
|
) THEN
|
||||||
|
CREATE TYPE TIME.time_source_system AS ENUM ('LEITSTAND', 'TOGGL_TRACK');
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_type t
|
||||||
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||||
|
WHERE t.typname = 'time_sync_run_status' AND n.nspname = 'time'
|
||||||
|
) THEN
|
||||||
|
CREATE TYPE TIME.time_sync_run_status AS ENUM ('RUNNING', 'COMPLETED', 'FAILED');
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_type t
|
||||||
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||||
|
WHERE t.typname = 'time_sync_run_type' AND n.nspname = 'time'
|
||||||
|
) THEN
|
||||||
|
CREATE TYPE TIME.time_sync_run_type AS ENUM ('INITIAL', 'INCREMENTAL', 'RECONCILE', 'MANUAL');
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_enum e
|
||||||
|
JOIN pg_type t ON t.oid = e.enumtypid
|
||||||
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||||
|
WHERE n.nspname = 'doc' AND t.typname = 'doc_document_type' AND e.enumlabel = 'TIME_ENTRY'
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE DOC.doc_document_type ADD VALUE 'TIME_ENTRY';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (
|
||||||
|
SELECT 1
|
||||||
|
FROM pg_enum e
|
||||||
|
JOIN pg_type t ON t.oid = e.enumtypid
|
||||||
|
JOIN pg_namespace n ON n.oid = t.typnamespace
|
||||||
|
WHERE n.nspname = 'doc' AND t.typname = 'doc_document_family' AND e.enumlabel = 'TIME'
|
||||||
|
) THEN
|
||||||
|
ALTER TYPE DOC.doc_document_family ADD VALUE 'TIME';
|
||||||
|
END IF;
|
||||||
|
END
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS TIME.time_entry (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
document_id UUID NOT NULL UNIQUE REFERENCES DOC.doc_document(id) ON DELETE CASCADE,
|
||||||
|
source_system TIME.time_source_system NOT NULL,
|
||||||
|
external_id VARCHAR(255) NOT NULL,
|
||||||
|
person_external_id VARCHAR(255),
|
||||||
|
person_display_name VARCHAR(255),
|
||||||
|
entry_start TIMESTAMP WITH TIME ZONE,
|
||||||
|
entry_end TIMESTAMP WITH TIME ZONE,
|
||||||
|
duration_seconds BIGINT,
|
||||||
|
description_short VARCHAR(1000),
|
||||||
|
description_long TEXT,
|
||||||
|
billable BOOLEAN,
|
||||||
|
source_created_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
source_updated_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
source_deleted_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
raw_status VARCHAR(120),
|
||||||
|
search_anchor_label VARCHAR(500),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT uq_time_entry_source UNIQUE (source_system, external_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS TIME.time_entry_source_link (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
time_entry_id UUID NOT NULL REFERENCES TIME.time_entry(id) ON DELETE CASCADE,
|
||||||
|
source_system TIME.time_source_system NOT NULL,
|
||||||
|
source_entity_type VARCHAR(120) NOT NULL,
|
||||||
|
source_external_id VARCHAR(255) NOT NULL,
|
||||||
|
linked_role VARCHAR(120),
|
||||||
|
parent_source_external_id VARCHAR(255),
|
||||||
|
source_updated_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
source_deleted_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT uq_time_entry_source_link UNIQUE (time_entry_id, source_system, source_entity_type, source_external_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS TIME.time_sync_run (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
source_system TIME.time_source_system NOT NULL,
|
||||||
|
run_type TIME.time_sync_run_type NOT NULL,
|
||||||
|
scope_key VARCHAR(255) NOT NULL,
|
||||||
|
status TIME.time_sync_run_status NOT NULL,
|
||||||
|
started_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
finished_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
watermark_from VARCHAR(255),
|
||||||
|
watermark_to VARCHAR(255),
|
||||||
|
rows_read INTEGER NOT NULL DEFAULT 0,
|
||||||
|
rows_created INTEGER NOT NULL DEFAULT 0,
|
||||||
|
rows_updated INTEGER NOT NULL DEFAULT 0,
|
||||||
|
rows_deactivated INTEGER NOT NULL DEFAULT 0,
|
||||||
|
rows_failed INTEGER NOT NULL DEFAULT 0,
|
||||||
|
error_message TEXT,
|
||||||
|
initiated_by VARCHAR(120),
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS TIME.time_sync_state (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
source_system TIME.time_source_system NOT NULL,
|
||||||
|
scope_key VARCHAR(255) NOT NULL,
|
||||||
|
enabled BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
last_successful_sync_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
last_attempted_sync_at TIMESTAMP WITH TIME ZONE,
|
||||||
|
last_seen_watermark VARCHAR(255),
|
||||||
|
last_seen_external_id VARCHAR(255),
|
||||||
|
last_run_id UUID REFERENCES TIME.time_sync_run(id) ON DELETE SET NULL,
|
||||||
|
notes TEXT,
|
||||||
|
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT uq_time_sync_state_scope UNIQUE (source_system, scope_key)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time_entry_source_system ON TIME.time_entry(source_system);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time_entry_external_id ON TIME.time_entry(external_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time_entry_start ON TIME.time_entry(entry_start DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time_entry_end ON TIME.time_entry(entry_end DESC);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time_entry_source_updated ON TIME.time_entry(source_updated_at DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time_entry_link_entry ON TIME.time_entry_source_link(time_entry_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time_entry_link_source_lookup ON TIME.time_entry_source_link(source_system, source_entity_type, source_external_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time_entry_link_role ON TIME.time_entry_source_link(linked_role);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time_sync_run_source_system ON TIME.time_sync_run(source_system);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time_sync_run_scope_key ON TIME.time_sync_run(scope_key);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time_sync_run_status ON TIME.time_sync_run(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time_sync_run_started_at ON TIME.time_sync_run(started_at DESC);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time_sync_state_source_system ON TIME.time_sync_state(source_system);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_time_sync_state_scope_key ON TIME.time_sync_state(scope_key);
|
||||||
Loading…
Reference in New Issue