Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 71 additions & 15 deletions app/Actions/Citation/SyncCitations.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,12 +35,9 @@ public function sync(Project $project, array $citations, User $user): array
foreach ($citations as $citationData) {
$this->validateCitationData($citationData);

$doi = $citationData['doi'];

if (! is_null($doi)) {
$citation = $this->findOrCreateCitation($project, $citationData, $doi);
$processedCitations[] = $citation;
}
$citation = $this->findOrCreateCitation($project, $citationData);
$this->rememberCitation($project, $citation);
$processedCitations[] = $citation;
}

DB::transaction(function () use ($project, $processedCitations, $user): void {
Expand All @@ -61,7 +58,7 @@ private function validateCitationData(array $citationData): void
{
Validator::make($citationData, [
'title' => ['required', 'string'],
'doi' => ['required', 'string'],
'doi' => ['nullable', 'string'],
'authors' => ['required', 'string'],
'citation_text' => ['nullable', 'string'],
])->validate();
Expand All @@ -72,11 +69,31 @@ private function validateCitationData(array $citationData): void
*
* @param array<string, mixed> $citationData
*/
private function findOrCreateCitation(Project $project, array $citationData, string $doi): Citation
private function findOrCreateCitation(Project $project, array $citationData): Citation
{
$existingCitation = $project->citations->filter(function ($citation) use ($doi) {
return $doi === $citation->doi;
})->first();
$doi = $this->normalizeDoi($citationData['doi'] ?? null);
$title = $this->normalizeText($citationData['title'] ?? null);
$authors = $this->normalizeText($citationData['authors'] ?? null);

$existingCitation = null;

// 1. Try to match by ID first (explicit reference)
if (! empty($citationData['id'])) {
$existingCitation = $project->citations->firstWhere('id', (int) $citationData['id']);
}

// 2. Try to match by DOI (if not null/empty)
if (! $existingCitation && ! is_null($doi)) {
$existingCitation = $project->citations->firstWhere('doi', $doi);
}

// 3. Try to match by title + authors (content-based matching for missing DOI)
if (! $existingCitation && ! is_null($title) && ! is_null($authors)) {
$existingCitation = $project->citations->first(function ($citation) use ($title, $authors): bool {
return $this->normalizeText($citation->title) === $title
&& $this->normalizeText($citation->authors) === $authors;
});
}

if ($existingCitation) {
$existingCitation->update($this->prepareCitationAttributes($citationData));
Expand All @@ -96,10 +113,49 @@ private function findOrCreateCitation(Project $project, array $citationData, str
private function prepareCitationAttributes(array $citationData): array
{
return [
'doi' => $citationData['doi'] ?? null,
'title' => $citationData['title'] ?? null,
'authors' => $citationData['authors'] ?? null,
'citation_text' => $citationData['citation_text'] ?? null,
'doi' => $this->normalizeDoi($citationData['doi'] ?? null),
'title' => $this->normalizeText($citationData['title'] ?? null),
'authors' => $this->normalizeText($citationData['authors'] ?? null),
'citation_text' => $this->normalizeText($citationData['citation_text'] ?? null),
];
}

private function rememberCitation(Project $project, Citation $citation): void
{
$citations = $project->citations;

if (! $citations->contains('id', $citation->id)) {
$project->setRelation('citations', $citations->push($citation));
}
}

private function normalizeDoi(mixed $doi): ?string
{
if (! is_string($doi)) {
return null;
}

$normalizedDoi = trim($doi);

if ($normalizedDoi === '') {
return null;
}

return $normalizedDoi;
}

private function normalizeText(mixed $value): ?string
{
if (! is_string($value)) {
return null;
}

$normalizedValue = trim($value);

if ($normalizedValue === '') {
return null;
}

return $normalizedValue;
}
}
36 changes: 36 additions & 0 deletions app/Models/Validation.php
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,42 @@ public function process()
array_push($studiesValidation, $studyReport);
}

// Validate citations
$citations = $project->citations;
$citationsValidation = [];
$citationsStatus = true;

foreach ($citations as $citation) {
$citationReport = [
'name' => $citation->title ?? 'Untitled',
'id' => $citation->id,
];

// Check if DOI is present
$hasDoi = is_string($citation->doi) && trim($citation->doi) !== '';

if ($hasDoi) {
$citationReport['doi'] = 'true|required';
} else {
$citationReport['doi'] = 'false|required';
$citationsStatus = false; // Citation validation failed
}

$citationReport['status'] = $hasDoi;
array_push($citationsValidation, $citationReport);
}

// Set overall citations validation status and store detailed report
if ($citationsStatus) {
$report['project']['citations'] = 'true|required';
} else {
$report['project']['citations'] = 'false|required';
$status = false; // Propagate to project status
}

// Store detailed citations validation data separately
$report['project']['citations_detail'] = $citationsValidation;
Comment on lines +270 to +279
Copy link

Copilot AI Mar 25, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block overwrites $report['project']['citations'] that was already set earlier from the schema rules (e.g. array|min:1). That loses the original validation result and can also mark citations as true|required when the project has zero citations (empty loop keeps $citationsStatus true). Consider using a separate key (e.g. citations_doi / citations_doi_detail) or extending the existing project/citation rules without clobbering the earlier report values, and ensure the empty-citations case is handled consistently.

Suggested change
// Set overall citations validation status and store detailed report
if ($citationsStatus) {
$report['project']['citations'] = 'true|required';
} else {
$report['project']['citations'] = 'false|required';
$status = false; // Propagate to project status
}
// Store detailed citations validation data separately
$report['project']['citations_detail'] = $citationsValidation;
// Set overall DOI-specific citations validation status without overwriting base citations validation
if (count($citations) === 0) {
$report['project']['citations_doi'] = 'false|required';
$status = false;
} else {
if ($citationsStatus) {
$report['project']['citations_doi'] = 'true|required';
} else {
$report['project']['citations_doi'] = 'false|required';
$status = false; // Propagate to project status
}
}
// Store detailed DOI-specific citations validation data separately
$report['project']['citations_doi_detail'] = $citationsValidation;

Copilot uses AI. Check for mistakes.

$report['project']['studies'] = $studiesValidation;
$report['project']['status'] = $status;
$project->validation_status = $status;
Expand Down
Loading
Loading