diff --git a/cmd/worker/workers/notice.go b/cmd/worker/workers/notice.go index c8e6c32..dc069a9 100644 --- a/cmd/worker/workers/notice.go +++ b/cmd/worker/workers/notice.go @@ -575,10 +575,16 @@ func (w *NoticeWorker) ToTender(n *notice.Notice) (*tender.Tender, error) { return t, nil } -// resolveExistingTender finds the tender that this notice should update, preferring the procedure-level -// ContractFolderID (so follow-up notices like a Result for an earlier Competition notice merge into the -// same tender), then UBL ProcurementProject/ID (merges per-lot CAN/CN notices), then NoticePublicationID. +// resolveExistingTender finds the tender that this notice should update. TED ContractNoticeID uniquely +// identifies the notice document; try it first so we never open a second tender row for the same notice. +// Then procedure-level ContractFolderID (follow-up notices merge), UBL ProcurementProject/ID (per-lot), +// then NoticePublicationID. func (w *NoticeWorker) resolveExistingTender(ctx context.Context, n *notice.Notice) *tender.Tender { + if strings.TrimSpace(n.ContractNoticeID) != "" { + if t, err := w.TenderRepo.GetByContractNoticeID(ctx, n.ContractNoticeID); err == nil && t != nil { + return t + } + } if strings.TrimSpace(n.ContractFolderID) != "" { if t, err := w.TenderRepo.GetByContractFolderID(ctx, n.ContractFolderID); err == nil && t != nil { return t @@ -998,7 +1004,7 @@ func (w *NoticeWorker) persistTender(ctx context.Context, t *tender.Tender, n *n lastErr = err // Another goroutine inserted the same procedure/publication first: merge into the winner row. - if isDuplicateKeyError(err) && (isDuplicateTenderBusinessKeyError(err) || isDuplicateContractFolderIDKeyError(err) || isDuplicateProcurementProjectIDKeyError(err)) { + if isDuplicateKeyError(err) && (isDuplicateTenderBusinessKeyError(err) || isDuplicateContractNoticeIDKeyError(err) || isDuplicateContractFolderIDKeyError(err) || isDuplicateProcurementProjectIDKeyError(err)) { tMerged, convErr := w.ToTender(n) if convErr != nil { return convErr @@ -1062,6 +1068,13 @@ func isDuplicateTenderBusinessKeyError(err error) bool { return strings.Contains(msg, "notice_publication_id") } +func isDuplicateContractNoticeIDKeyError(err error) bool { + if err == nil { + return false + } + return strings.Contains(err.Error(), "contract_notice_id") +} + func isDuplicateContractFolderIDKeyError(err error) bool { if err == nil { return false diff --git a/internal/tender/repository.go b/internal/tender/repository.go index 41e940e..474f8d9 100644 --- a/internal/tender/repository.go +++ b/internal/tender/repository.go @@ -54,6 +54,9 @@ func NewRepository(mongoManager *orm.ConnectionManager, logger logger.Logger) Te *orm.CreateUniqueIndex("tender_id_idx", bson.D{{Key: "tender_id", Value: 1}}), *orm.NewIndex("project_name_idx", bson.D{{Key: "project_name", Value: 1}}), *orm.NewIndex("contract_notice_id_idx", bson.D{{Key: "contract_notice_id", Value: 1}}), + // One tender row per TED ContractNoticeID when present (parallel ingest / lookup gaps). + *orm.CreateUniqueIndex("contract_notice_id_unique", bson.D{{Key: "contract_notice_id", Value: 1}}). + WithPartialFilterExpression(bson.M{"contract_notice_id": bson.M{"$type": "string", "$ne": ""}}), *orm.NewIndex("contract_folder_id_idx", bson.D{{Key: "contract_folder_id", Value: 1}}), // One tender row per TED publication / procedure when ids are present (prevents parallel worker races). *orm.CreateUniqueIndex("notice_publication_id_unique", bson.D{{Key: "notice_publication_id", Value: 1}}).