Refactor phases 3
parent
71fb43a5ea
commit
adc4f2da43
@ -0,0 +1,46 @@
|
||||
# Phase 3 - TED projection model
|
||||
|
||||
## Goal
|
||||
|
||||
Move TED from being the implicit root data model to being a typed projection on top of the generic
|
||||
canonical document model.
|
||||
|
||||
## New persistence model
|
||||
|
||||
### Generic root
|
||||
- `DOC.doc_document`
|
||||
- `DOC.doc_content`
|
||||
- `DOC.doc_text_representation`
|
||||
- `DOC.doc_embedding`
|
||||
|
||||
### TED-specific projection
|
||||
- `TED.ted_notice_projection`
|
||||
- `TED.ted_notice_lot`
|
||||
- `TED.ted_notice_organization`
|
||||
|
||||
## Relationship model
|
||||
|
||||
- one generic `DOC.doc_document`
|
||||
- zero or one `TED.ted_notice_projection`
|
||||
- zero to many `TED.ted_notice_lot`
|
||||
- zero to many `TED.ted_notice_organization`
|
||||
|
||||
The projection also keeps an optional back-reference to the legacy `TED.procurement_document` row to
|
||||
support incremental migration and validation.
|
||||
|
||||
## Runtime behavior
|
||||
|
||||
When a new TED XML document is imported:
|
||||
1. it is parsed into the existing legacy `ProcurementDocument`
|
||||
2. the generic DOC root is ensured/refreshed
|
||||
3. the primary text representation is ensured
|
||||
4. if the generic vectorization pipeline is enabled, a pending embedding is ensured
|
||||
5. the TED structured projection tables are refreshed from the parsed legacy document
|
||||
|
||||
## Why this phase matters
|
||||
|
||||
This is the first phase where TED is explicitly modeled as a document type projection instead of the
|
||||
platform's canonical root entity. That makes the next steps possible:
|
||||
- generic semantic search across multiple document types
|
||||
- future non-TED projections
|
||||
- migration of TED structured search to the new projection tables
|
||||
@ -0,0 +1,30 @@
|
||||
# Phase 3 - TED as a structured projection on the generic document core
|
||||
|
||||
Phase 3 makes TED a proper type-specific projection layered on top of the generic `DOC.doc_document`
|
||||
root introduced in Phase 1 and the generic vectorization model introduced in Phase 2.
|
||||
|
||||
## What is implemented
|
||||
- `TED.ted_notice_projection`
|
||||
- `TED.ted_notice_lot`
|
||||
- `TED.ted_notice_organization`
|
||||
- `TedNoticeProjectionService`
|
||||
- optional startup backfill of missing TED projections
|
||||
- processing flow updated so freshly imported TED notices dual-write to:
|
||||
- `DOC` generic document/content/representation model
|
||||
- `TED` structured projection tables
|
||||
|
||||
## Core intent
|
||||
TED is no longer the root model of the platform. Instead:
|
||||
- `DOC.doc_document` is the canonical document root
|
||||
- `TED.ted_notice_projection` holds TED-specific structured metadata
|
||||
- `TED.ted_notice_lot` and `TED.ted_notice_organization` hold normalized child structures
|
||||
|
||||
## Compatibility
|
||||
This phase is additive:
|
||||
- legacy `TED.procurement_document` remains in place
|
||||
- existing search and API behavior continue to work
|
||||
- new imports are now representable in both the legacy and new projection model
|
||||
|
||||
## Important limitation
|
||||
Structured search endpoints still read from the legacy TED model. Moving TED structured reads to the
|
||||
new projection tables is the next migration step.
|
||||
@ -0,0 +1,95 @@
|
||||
package at.procon.dip.domain.ted.entity;
|
||||
|
||||
import at.procon.dip.architecture.SchemaNames;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
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.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
@Entity
|
||||
@Table(schema = SchemaNames.TED, name = "ted_notice_lot", indexes = {
|
||||
@Index(name = "idx_ted_notice_lot_projection", columnList = "notice_projection_id")
|
||||
}, uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uq_ted_notice_lot_projection_lot", columnNames = {"notice_projection_id", "lot_id"})
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class TedNoticeLot {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "notice_projection_id", nullable = false)
|
||||
private TedNoticeProjection noticeProjection;
|
||||
|
||||
@Column(name = "lot_id", nullable = false, length = 50)
|
||||
private String lotId;
|
||||
|
||||
@Column(name = "internal_id", columnDefinition = "TEXT")
|
||||
private String internalId;
|
||||
|
||||
@Column(name = "title", columnDefinition = "TEXT")
|
||||
private String title;
|
||||
|
||||
@Column(name = "description", columnDefinition = "TEXT")
|
||||
private String description;
|
||||
|
||||
@Column(name = "cpv_codes", columnDefinition = "VARCHAR(100)[]")
|
||||
@JdbcTypeCode(SqlTypes.ARRAY)
|
||||
private String[] cpvCodes;
|
||||
|
||||
@Column(name = "nuts_codes", columnDefinition = "VARCHAR(20)[]")
|
||||
@JdbcTypeCode(SqlTypes.ARRAY)
|
||||
private String[] nutsCodes;
|
||||
|
||||
@Column(name = "estimated_value", precision = 20, scale = 2)
|
||||
private BigDecimal estimatedValue;
|
||||
|
||||
@Column(name = "estimated_value_currency", length = 3)
|
||||
private String estimatedValueCurrency;
|
||||
|
||||
@Column(name = "duration_value")
|
||||
private Double durationValue;
|
||||
|
||||
@Column(name = "duration_unit", length = 20)
|
||||
private String durationUnit;
|
||||
|
||||
@Column(name = "submission_deadline")
|
||||
private OffsetDateTime submissionDeadline;
|
||||
|
||||
@Column(name = "eu_funded")
|
||||
@Builder.Default
|
||||
private Boolean euFunded = false;
|
||||
|
||||
@Builder.Default
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private OffsetDateTime createdAt = OffsetDateTime.now();
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = OffsetDateTime.now();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
package at.procon.dip.domain.ted.entity;
|
||||
|
||||
import at.procon.dip.architecture.SchemaNames;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
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.Table;
|
||||
import jakarta.persistence.UniqueConstraint;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@Entity
|
||||
@Table(schema = SchemaNames.TED, name = "ted_notice_organization", indexes = {
|
||||
@Index(name = "idx_ted_notice_org_projection", columnList = "notice_projection_id"),
|
||||
@Index(name = "idx_ted_notice_org_country", columnList = "country_code")
|
||||
}, uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uq_ted_notice_org_projection_ref", columnNames = {"notice_projection_id", "org_reference"})
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class TedNoticeOrganization {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "notice_projection_id", nullable = false)
|
||||
private TedNoticeProjection noticeProjection;
|
||||
|
||||
@Column(name = "org_reference", length = 50)
|
||||
private String orgReference;
|
||||
|
||||
@Column(name = "role", length = 50)
|
||||
private String role;
|
||||
|
||||
@Column(name = "name", columnDefinition = "TEXT")
|
||||
private String name;
|
||||
|
||||
@Column(name = "company_id", length = 1000)
|
||||
private String companyId;
|
||||
|
||||
@Column(name = "country_code", length = 10)
|
||||
private String countryCode;
|
||||
|
||||
@Column(name = "city", length = 255)
|
||||
private String city;
|
||||
|
||||
@Column(name = "postal_code", length = 255)
|
||||
private String postalCode;
|
||||
|
||||
@Column(name = "street_name", columnDefinition = "TEXT")
|
||||
private String streetName;
|
||||
|
||||
@Column(name = "nuts_code", length = 10)
|
||||
private String nutsCode;
|
||||
|
||||
@Column(name = "website_uri", columnDefinition = "TEXT")
|
||||
private String websiteUri;
|
||||
|
||||
@Column(name = "email", length = 255)
|
||||
private String email;
|
||||
|
||||
@Column(name = "phone", length = 50)
|
||||
private String phone;
|
||||
|
||||
@Builder.Default
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private OffsetDateTime createdAt = OffsetDateTime.now();
|
||||
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = OffsetDateTime.now();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,194 @@
|
||||
package at.procon.dip.domain.ted.entity;
|
||||
|
||||
import at.procon.dip.architecture.SchemaNames;
|
||||
import at.procon.dip.domain.document.entity.Document;
|
||||
import at.procon.ted.model.entity.ContractNature;
|
||||
import at.procon.ted.model.entity.NoticeType;
|
||||
import at.procon.ted.model.entity.ProcedureType;
|
||||
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.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.OffsetDateTime;
|
||||
import java.util.UUID;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
/**
|
||||
* Phase 3 TED-specific projection that sits on top of the generic DOC document root.
|
||||
*/
|
||||
@Entity
|
||||
@Table(schema = SchemaNames.TED, name = "ted_notice_projection", indexes = {
|
||||
@Index(name = "idx_ted_proj_document", columnList = "document_id"),
|
||||
@Index(name = "idx_ted_proj_legacy_doc", columnList = "legacy_procurement_document_id"),
|
||||
@Index(name = "idx_ted_proj_publication_id", columnList = "publication_id"),
|
||||
@Index(name = "idx_ted_proj_notice_type", columnList = "notice_type"),
|
||||
@Index(name = "idx_ted_proj_buyer_country", columnList = "buyer_country_code"),
|
||||
@Index(name = "idx_ted_proj_publication_date", columnList = "publication_date")
|
||||
})
|
||||
@Getter
|
||||
@Setter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class TedNoticeProjection {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.UUID)
|
||||
private UUID id;
|
||||
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "document_id", nullable = false, unique = true)
|
||||
private Document document;
|
||||
|
||||
@Column(name = "legacy_procurement_document_id", unique = true)
|
||||
private UUID legacyProcurementDocumentId;
|
||||
|
||||
@Column(name = "notice_id", length = 100)
|
||||
private String noticeId;
|
||||
|
||||
@Column(name = "publication_id", length = 50)
|
||||
private String publicationId;
|
||||
|
||||
@Column(name = "notice_url", length = 255)
|
||||
private String noticeUrl;
|
||||
|
||||
@Column(name = "ojs_id", length = 20)
|
||||
private String ojsId;
|
||||
|
||||
@Column(name = "contract_folder_id", length = 100)
|
||||
private String contractFolderId;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "notice_type", nullable = false, length = 50)
|
||||
@Builder.Default
|
||||
private NoticeType noticeType = NoticeType.OTHER;
|
||||
|
||||
@Column(name = "notice_subtype_code", length = 10)
|
||||
private String noticeSubtypeCode;
|
||||
|
||||
@Column(name = "sdk_version", length = 20)
|
||||
private String sdkVersion;
|
||||
|
||||
@Column(name = "ubl_version", length = 10)
|
||||
private String ublVersion;
|
||||
|
||||
@Column(name = "language_code", length = 10)
|
||||
private String languageCode;
|
||||
|
||||
@Column(name = "issue_datetime")
|
||||
private OffsetDateTime issueDateTime;
|
||||
|
||||
@Column(name = "publication_date")
|
||||
private LocalDate publicationDate;
|
||||
|
||||
@Column(name = "submission_deadline")
|
||||
private OffsetDateTime submissionDeadline;
|
||||
|
||||
@Column(name = "buyer_name", columnDefinition = "TEXT")
|
||||
private String buyerName;
|
||||
|
||||
@Column(name = "buyer_country_code", length = 10)
|
||||
private String buyerCountryCode;
|
||||
|
||||
@Column(name = "buyer_city", length = 255)
|
||||
private String buyerCity;
|
||||
|
||||
@Column(name = "buyer_postal_code", length = 100)
|
||||
private String buyerPostalCode;
|
||||
|
||||
@Column(name = "buyer_nuts_code", length = 10)
|
||||
private String buyerNutsCode;
|
||||
|
||||
@Column(name = "buyer_activity_type", length = 50)
|
||||
private String buyerActivityType;
|
||||
|
||||
@Column(name = "buyer_legal_type", length = 50)
|
||||
private String buyerLegalType;
|
||||
|
||||
@Column(name = "project_title", columnDefinition = "TEXT")
|
||||
private String projectTitle;
|
||||
|
||||
@Column(name = "project_description", columnDefinition = "TEXT")
|
||||
private String projectDescription;
|
||||
|
||||
@Column(name = "internal_reference", length = 500)
|
||||
private String internalReference;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "contract_nature", nullable = false, length = 50)
|
||||
@Builder.Default
|
||||
private ContractNature contractNature = ContractNature.UNKNOWN;
|
||||
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "procedure_type", length = 50)
|
||||
@Builder.Default
|
||||
private ProcedureType procedureType = ProcedureType.OTHER;
|
||||
|
||||
@Column(name = "cpv_codes", columnDefinition = "VARCHAR(100)[]")
|
||||
@JdbcTypeCode(SqlTypes.ARRAY)
|
||||
private String[] cpvCodes;
|
||||
|
||||
@Column(name = "nuts_codes", columnDefinition = "VARCHAR(20)[]")
|
||||
@JdbcTypeCode(SqlTypes.ARRAY)
|
||||
private String[] nutsCodes;
|
||||
|
||||
@Column(name = "estimated_value", precision = 20, scale = 2)
|
||||
private BigDecimal estimatedValue;
|
||||
|
||||
@Column(name = "estimated_value_currency", length = 3)
|
||||
private String estimatedValueCurrency;
|
||||
|
||||
@Column(name = "total_lots")
|
||||
@Builder.Default
|
||||
private Integer totalLots = 0;
|
||||
|
||||
@Column(name = "max_lots_awarded")
|
||||
private Integer maxLotsAwarded;
|
||||
|
||||
@Column(name = "max_lots_submitted")
|
||||
private Integer maxLotsSubmitted;
|
||||
|
||||
@Column(name = "regulatory_domain", length = 50)
|
||||
private String regulatoryDomain;
|
||||
|
||||
@Column(name = "eu_funded")
|
||||
@Builder.Default
|
||||
private Boolean euFunded = false;
|
||||
|
||||
@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,13 @@
|
||||
package at.procon.dip.domain.ted.repository;
|
||||
|
||||
import at.procon.dip.domain.ted.entity.TedNoticeLot;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface TedNoticeLotRepository extends JpaRepository<TedNoticeLot, UUID> {
|
||||
|
||||
List<TedNoticeLot> findByNoticeProjection_Id(UUID noticeProjectionId);
|
||||
|
||||
void deleteByNoticeProjection_Id(UUID noticeProjectionId);
|
||||
}
|
||||
@ -0,0 +1,13 @@
|
||||
package at.procon.dip.domain.ted.repository;
|
||||
|
||||
import at.procon.dip.domain.ted.entity.TedNoticeOrganization;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface TedNoticeOrganizationRepository extends JpaRepository<TedNoticeOrganization, UUID> {
|
||||
|
||||
List<TedNoticeOrganization> findByNoticeProjection_Id(UUID noticeProjectionId);
|
||||
|
||||
void deleteByNoticeProjection_Id(UUID noticeProjectionId);
|
||||
}
|
||||
@ -0,0 +1,15 @@
|
||||
package at.procon.dip.domain.ted.repository;
|
||||
|
||||
import at.procon.dip.domain.ted.entity.TedNoticeProjection;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
public interface TedNoticeProjectionRepository extends JpaRepository<TedNoticeProjection, UUID> {
|
||||
|
||||
Optional<TedNoticeProjection> findByDocument_Id(UUID documentId);
|
||||
|
||||
Optional<TedNoticeProjection> findByLegacyProcurementDocumentId(UUID legacyProcurementDocumentId);
|
||||
|
||||
boolean existsByLegacyProcurementDocumentId(UUID legacyProcurementDocumentId);
|
||||
}
|
||||
@ -0,0 +1,181 @@
|
||||
package at.procon.dip.domain.ted.service;
|
||||
|
||||
import at.procon.dip.domain.document.entity.Document;
|
||||
import at.procon.dip.domain.document.repository.DocumentRepository;
|
||||
import at.procon.dip.domain.ted.entity.TedNoticeLot;
|
||||
import at.procon.dip.domain.ted.entity.TedNoticeOrganization;
|
||||
import at.procon.dip.domain.ted.entity.TedNoticeProjection;
|
||||
import at.procon.dip.domain.ted.repository.TedNoticeLotRepository;
|
||||
import at.procon.dip.domain.ted.repository.TedNoticeOrganizationRepository;
|
||||
import at.procon.dip.domain.ted.repository.TedNoticeProjectionRepository;
|
||||
import at.procon.ted.config.TedProcessorProperties;
|
||||
import at.procon.ted.model.entity.Organization;
|
||||
import at.procon.ted.model.entity.ProcurementDocument;
|
||||
import at.procon.ted.model.entity.ProcurementLot;
|
||||
import at.procon.ted.service.TedPhase2GenericDocumentService;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* Phase 3 service that materializes TED-specific structured projections on top of the generic DOC document root.
|
||||
*/
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class TedNoticeProjectionService {
|
||||
|
||||
private final TedProcessorProperties properties;
|
||||
private final TedPhase2GenericDocumentService tedPhase2GenericDocumentService;
|
||||
private final DocumentRepository documentRepository;
|
||||
private final TedNoticeProjectionRepository projectionRepository;
|
||||
private final TedNoticeLotRepository lotRepository;
|
||||
private final TedNoticeOrganizationRepository organizationRepository;
|
||||
|
||||
@Transactional
|
||||
public UUID registerOrRefreshProjection(ProcurementDocument legacyDocument) {
|
||||
if (!properties.getProjection().isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
TedPhase2GenericDocumentService.TedGenericDocumentSyncResult syncResult =
|
||||
tedPhase2GenericDocumentService.syncTedDocument(legacyDocument);
|
||||
return registerOrRefreshProjection(legacyDocument, syncResult.documentId());
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public UUID registerOrRefreshProjection(ProcurementDocument legacyDocument, UUID genericDocumentId) {
|
||||
if (!properties.getProjection().isEnabled()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
UUID resolvedDocumentId = genericDocumentId;
|
||||
if (resolvedDocumentId == null) {
|
||||
resolvedDocumentId = tedPhase2GenericDocumentService.ensureGenericTedDocument(legacyDocument);
|
||||
}
|
||||
|
||||
UUID finalResolvedDocumentId = resolvedDocumentId;
|
||||
Document genericDocument = documentRepository.findById(resolvedDocumentId)
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown DOC document id: " + finalResolvedDocumentId));
|
||||
|
||||
TedNoticeProjection projection = projectionRepository.findByLegacyProcurementDocumentId(legacyDocument.getId())
|
||||
.or(() -> projectionRepository.findByDocument_Id(genericDocument.getId()))
|
||||
.orElseGet(TedNoticeProjection::new);
|
||||
|
||||
mapProjection(projection, genericDocument, legacyDocument);
|
||||
projection = projectionRepository.save(projection);
|
||||
replaceLots(projection, legacyDocument.getLots());
|
||||
replaceOrganizations(projection, legacyDocument.getOrganizations());
|
||||
|
||||
log.debug("Phase 3 TED projection ensured for legacy {} -> projection {} / doc {}",
|
||||
legacyDocument.getId(), projection.getId(), genericDocument.getId());
|
||||
return projection.getId();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<TedNoticeProjection> findByLegacyProcurementDocumentId(UUID legacyDocumentId) {
|
||||
return projectionRepository.findByLegacyProcurementDocumentId(legacyDocumentId);
|
||||
}
|
||||
|
||||
private void mapProjection(TedNoticeProjection projection, Document genericDocument, ProcurementDocument legacyDocument) {
|
||||
projection.setDocument(genericDocument);
|
||||
projection.setLegacyProcurementDocumentId(legacyDocument.getId());
|
||||
projection.setNoticeId(legacyDocument.getNoticeId());
|
||||
projection.setPublicationId(legacyDocument.getPublicationId());
|
||||
projection.setNoticeUrl(legacyDocument.getNoticeUrl());
|
||||
projection.setOjsId(legacyDocument.getOjsId());
|
||||
projection.setContractFolderId(legacyDocument.getContractFolderId());
|
||||
projection.setNoticeType(legacyDocument.getNoticeType());
|
||||
projection.setNoticeSubtypeCode(legacyDocument.getNoticeSubtypeCode());
|
||||
projection.setSdkVersion(legacyDocument.getSdkVersion());
|
||||
projection.setUblVersion(legacyDocument.getUblVersion());
|
||||
projection.setLanguageCode(legacyDocument.getLanguageCode());
|
||||
projection.setIssueDateTime(legacyDocument.getIssueDateTime());
|
||||
projection.setPublicationDate(legacyDocument.getPublicationDate());
|
||||
projection.setSubmissionDeadline(legacyDocument.getSubmissionDeadline());
|
||||
projection.setBuyerName(legacyDocument.getBuyerName());
|
||||
projection.setBuyerCountryCode(legacyDocument.getBuyerCountryCode());
|
||||
projection.setBuyerCity(legacyDocument.getBuyerCity());
|
||||
projection.setBuyerPostalCode(legacyDocument.getBuyerPostalCode());
|
||||
projection.setBuyerNutsCode(legacyDocument.getBuyerNutsCode());
|
||||
projection.setBuyerActivityType(legacyDocument.getBuyerActivityType());
|
||||
projection.setBuyerLegalType(legacyDocument.getBuyerLegalType());
|
||||
projection.setProjectTitle(legacyDocument.getProjectTitle());
|
||||
projection.setProjectDescription(legacyDocument.getProjectDescription());
|
||||
projection.setInternalReference(legacyDocument.getInternalReference());
|
||||
projection.setContractNature(legacyDocument.getContractNature());
|
||||
projection.setProcedureType(legacyDocument.getProcedureType());
|
||||
projection.setCpvCodes(copyArray(legacyDocument.getCpvCodes()));
|
||||
projection.setNutsCodes(copyArray(legacyDocument.getNutsCodes()));
|
||||
projection.setEstimatedValue(legacyDocument.getEstimatedValue());
|
||||
projection.setEstimatedValueCurrency(legacyDocument.getEstimatedValueCurrency());
|
||||
projection.setTotalLots(legacyDocument.getTotalLots());
|
||||
projection.setMaxLotsAwarded(legacyDocument.getMaxLotsAwarded());
|
||||
projection.setMaxLotsSubmitted(legacyDocument.getMaxLotsSubmitted());
|
||||
projection.setRegulatoryDomain(legacyDocument.getRegulatoryDomain());
|
||||
projection.setEuFunded(legacyDocument.getEuFunded());
|
||||
}
|
||||
|
||||
private void replaceLots(TedNoticeProjection projection, List<ProcurementLot> legacyLots) {
|
||||
lotRepository.deleteByNoticeProjection_Id(projection.getId());
|
||||
if (legacyLots == null || legacyLots.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<TedNoticeLot> projectedLots = new ArrayList<>();
|
||||
for (ProcurementLot lot : legacyLots) {
|
||||
projectedLots.add(TedNoticeLot.builder()
|
||||
.noticeProjection(projection)
|
||||
.lotId(lot.getLotId())
|
||||
.internalId(lot.getInternalId())
|
||||
.title(lot.getTitle())
|
||||
.description(lot.getDescription())
|
||||
.cpvCodes(copyArray(lot.getCpvCodes()))
|
||||
.nutsCodes(copyArray(lot.getNutsCodes()))
|
||||
.estimatedValue(lot.getEstimatedValue())
|
||||
.estimatedValueCurrency(lot.getEstimatedValueCurrency())
|
||||
.durationValue(lot.getDurationValue())
|
||||
.durationUnit(lot.getDurationUnit())
|
||||
.submissionDeadline(lot.getSubmissionDeadline())
|
||||
.euFunded(lot.getEuFunded())
|
||||
.build());
|
||||
}
|
||||
lotRepository.saveAll(projectedLots);
|
||||
}
|
||||
|
||||
private void replaceOrganizations(TedNoticeProjection projection, List<Organization> legacyOrganizations) {
|
||||
organizationRepository.deleteByNoticeProjection_Id(projection.getId());
|
||||
if (legacyOrganizations == null || legacyOrganizations.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<TedNoticeOrganization> projectedOrganizations = new ArrayList<>();
|
||||
for (Organization organization : legacyOrganizations) {
|
||||
projectedOrganizations.add(TedNoticeOrganization.builder()
|
||||
.noticeProjection(projection)
|
||||
.orgReference(organization.getOrgReference())
|
||||
.role(organization.getRole())
|
||||
.name(organization.getName())
|
||||
.companyId(organization.getCompanyId())
|
||||
.countryCode(organization.getCountryCode())
|
||||
.city(organization.getCity())
|
||||
.postalCode(organization.getPostalCode())
|
||||
.streetName(organization.getStreetName())
|
||||
.nutsCode(organization.getNutsCode())
|
||||
.websiteUri(organization.getWebsiteUri())
|
||||
.email(organization.getEmail())
|
||||
.phone(organization.getPhone())
|
||||
.build());
|
||||
}
|
||||
organizationRepository.saveAll(projectedOrganizations);
|
||||
}
|
||||
|
||||
private String[] copyArray(String[] source) {
|
||||
return source == null ? null : source.clone();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
package at.procon.dip.domain.ted.startup;
|
||||
|
||||
import at.procon.dip.domain.ted.repository.TedNoticeProjectionRepository;
|
||||
import at.procon.dip.domain.ted.service.TedNoticeProjectionService;
|
||||
import at.procon.ted.config.TedProcessorProperties;
|
||||
import at.procon.ted.repository.ProcurementDocumentRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.boot.ApplicationArguments;
|
||||
import org.springframework.boot.ApplicationRunner;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Optional startup backfill for Phase 3 TED projections.
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class TedProjectionStartupRunner implements ApplicationRunner {
|
||||
|
||||
private final TedProcessorProperties properties;
|
||||
private final ProcurementDocumentRepository procurementDocumentRepository;
|
||||
private final TedNoticeProjectionRepository projectionRepository;
|
||||
private final TedNoticeProjectionService projectionService;
|
||||
|
||||
@Override
|
||||
public void run(ApplicationArguments args) {
|
||||
if (!properties.getProjection().isEnabled() || !properties.getProjection().isStartupBackfillEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
int limit = properties.getProjection().getStartupBackfillLimit();
|
||||
log.info("Phase 3 startup backfill enabled - ensuring TED projections for up to {} documents", limit);
|
||||
|
||||
var page = procurementDocumentRepository.findAll(
|
||||
PageRequest.of(0, limit, Sort.by(Sort.Direction.ASC, "createdAt")));
|
||||
|
||||
int synced = 0;
|
||||
for (var legacyDocument : page.getContent()) {
|
||||
if (projectionRepository.existsByLegacyProcurementDocumentId(legacyDocument.getId())) {
|
||||
continue;
|
||||
}
|
||||
projectionService.registerOrRefreshProjection(legacyDocument);
|
||||
synced++;
|
||||
}
|
||||
|
||||
log.info("Phase 3 startup backfill completed - synced {} TED projections", synced);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,105 @@
|
||||
-- Phase 3: TED becomes a structured projection on top of the generic DOC document root.
|
||||
-- Additive migration; legacy TED tables remain in place for compatibility.
|
||||
|
||||
SET search_path TO TED, DOC, public;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS TED.ted_notice_projection (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
document_id UUID NOT NULL UNIQUE REFERENCES DOC.doc_document(id) ON DELETE CASCADE,
|
||||
legacy_procurement_document_id UUID UNIQUE REFERENCES TED.procurement_document(id) ON DELETE SET NULL,
|
||||
notice_id VARCHAR(100),
|
||||
publication_id VARCHAR(50),
|
||||
notice_url VARCHAR(255),
|
||||
ojs_id VARCHAR(20),
|
||||
contract_folder_id VARCHAR(100),
|
||||
notice_type VARCHAR(50) NOT NULL DEFAULT 'OTHER',
|
||||
notice_subtype_code VARCHAR(10),
|
||||
sdk_version VARCHAR(20),
|
||||
ubl_version VARCHAR(10),
|
||||
language_code VARCHAR(10),
|
||||
issue_datetime TIMESTAMP WITH TIME ZONE,
|
||||
publication_date DATE,
|
||||
submission_deadline TIMESTAMP WITH TIME ZONE,
|
||||
buyer_name TEXT,
|
||||
buyer_country_code VARCHAR(10),
|
||||
buyer_city VARCHAR(255),
|
||||
buyer_postal_code VARCHAR(100),
|
||||
buyer_nuts_code VARCHAR(10),
|
||||
buyer_activity_type VARCHAR(50),
|
||||
buyer_legal_type VARCHAR(50),
|
||||
project_title TEXT,
|
||||
project_description TEXT,
|
||||
internal_reference VARCHAR(500),
|
||||
contract_nature VARCHAR(50) NOT NULL DEFAULT 'UNKNOWN',
|
||||
procedure_type VARCHAR(50) DEFAULT 'OTHER',
|
||||
cpv_codes VARCHAR(100)[],
|
||||
nuts_codes VARCHAR(20)[],
|
||||
estimated_value NUMERIC(20,2),
|
||||
estimated_value_currency VARCHAR(3),
|
||||
total_lots INTEGER NOT NULL DEFAULT 0,
|
||||
max_lots_awarded INTEGER,
|
||||
max_lots_submitted INTEGER,
|
||||
regulatory_domain VARCHAR(50),
|
||||
eu_funded BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ted_notice_projection_publication_id
|
||||
ON TED.ted_notice_projection(publication_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ted_notice_projection_notice_type
|
||||
ON TED.ted_notice_projection(notice_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_ted_notice_projection_buyer_country
|
||||
ON TED.ted_notice_projection(buyer_country_code);
|
||||
CREATE INDEX IF NOT EXISTS idx_ted_notice_projection_publication_date
|
||||
ON TED.ted_notice_projection(publication_date DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_ted_notice_projection_document
|
||||
ON TED.ted_notice_projection(document_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ted_notice_projection_legacy_doc
|
||||
ON TED.ted_notice_projection(legacy_procurement_document_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS TED.ted_notice_lot (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
notice_projection_id UUID NOT NULL REFERENCES TED.ted_notice_projection(id) ON DELETE CASCADE,
|
||||
lot_id VARCHAR(50) NOT NULL,
|
||||
internal_id TEXT,
|
||||
title TEXT,
|
||||
description TEXT,
|
||||
cpv_codes VARCHAR(100)[],
|
||||
nuts_codes VARCHAR(20)[],
|
||||
estimated_value NUMERIC(20,2),
|
||||
estimated_value_currency VARCHAR(3),
|
||||
duration_value DOUBLE PRECISION,
|
||||
duration_unit VARCHAR(20),
|
||||
submission_deadline TIMESTAMP WITH TIME ZONE,
|
||||
eu_funded BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_ted_notice_lot_projection_lot UNIQUE (notice_projection_id, lot_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ted_notice_lot_projection
|
||||
ON TED.ted_notice_lot(notice_projection_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS TED.ted_notice_organization (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
notice_projection_id UUID NOT NULL REFERENCES TED.ted_notice_projection(id) ON DELETE CASCADE,
|
||||
org_reference VARCHAR(50),
|
||||
role VARCHAR(50),
|
||||
name TEXT,
|
||||
company_id VARCHAR(1000),
|
||||
country_code VARCHAR(10),
|
||||
city VARCHAR(255),
|
||||
postal_code VARCHAR(255),
|
||||
street_name TEXT,
|
||||
nuts_code VARCHAR(10),
|
||||
website_uri TEXT,
|
||||
email VARCHAR(255),
|
||||
phone VARCHAR(50),
|
||||
created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
CONSTRAINT uq_ted_notice_org_projection_ref UNIQUE (notice_projection_id, org_reference)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_ted_notice_org_projection
|
||||
ON TED.ted_notice_organization(notice_projection_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_ted_notice_org_country
|
||||
ON TED.ted_notice_organization(country_code);
|
||||
Loading…
Reference in New Issue