from __future__ import annotations from datetime import UTC, datetime import pytest from apps.jobs.models import Employer, JobPosting from apps.jobs.services.employer_resolution import resolve_direct_employer_match from apps.jobs.services.normalization import CanonicalJobDraft from apps.sources.models import Source def _draft_for_test(job: JobPosting, **overrides) -> CanonicalJobDraft: values = { "source_url": "https://jobs.smartrecruiters.com/example/security-engineer", "canonical_url": "https://jobs.smartrecruiters.com/example/security-engineer", "external_id": "sr-security-123", "title": job.original_title, "normalized_title": job.normalized_title, "job_family": job.job_family, "employer_name": job.employer_name, "employer_domain": "jobs.example.org", "location_text": job.raw_location, "region": job.region, "municipality": job.municipality, "postal_code": job.postal_code, "country": job.country, "workplace_type": job.workplace_type, "employment_types": job.employment_types, "language": "nl", "description_html": "
Test
", "description_text": "test beschrijving", "date_posted": datetime(2026, 1, 10, tzinfo=UTC), "valid_through": datetime(2026, 6, 1, tzinfo=UTC), "compensation": {}, "skills_required": [], "skills_preferred": [], "content_hash": job.content_hash, "canonical_key": "x" * 64, "evidence": [], "raw": {}, } values.update(overrides) return CanonicalJobDraft(**values) @pytest.mark.django_db def test_resolver_merges_recruiter_alias_to_direct_source(): employer = Employer.objects.create( name="Example IT", normalized_name="example it", domain="jobs.example.org", ) JobPosting.objects.create( employer=employer, original_title="Security Engineer", normalized_title="security engineer", canonical_url="https://jobs.example.org/vacatures/security-engineer", canonical_key="a" * 64, job_family="security", content_hash="a" * 64, description_text="Security engineer voor beveiliging.", raw_location="Antwerpen", region="Antwerpen", municipality="Antwerpen", workplace_type=JobPosting.Workplace.HYBRID, employment_types=["full_time"], direct_employer=True, ) source = Source.objects.create( name="SmartRecruiters", source_type=Source.Type.ATS, base_url="https://jobs.smartrecruiters.com/example", domain="jobs.smartrecruiters.com", status=Source.Status.ACTIVE, policy=Source.Policy.ALLOW, ) # Use an existing direct employer job as the resolver target. target_job = JobPosting.objects.first() decision = resolve_direct_employer_match( _draft_for_test(target_job), source=source, ) assert decision.job == target_job assert decision.reason == "resolved_direct_match" assert decision.canonical_url == "https://jobs.example.org/vacatures/security-engineer" @pytest.mark.django_db def test_resolver_flags_conflicting_location_for_review(): employer = Employer.objects.create( name="Example IT", normalized_name="example it", domain="jobs.example.org", ) JobPosting.objects.create( employer=employer, original_title="Security Engineer", normalized_title="security engineer", canonical_url="https://jobs.example.org/vacatures/security-engineer", canonical_key="b" * 64, job_family="security", content_hash="b" * 64, description_text="Security engineer voor beveiliging.", raw_location="Antwerpen", region="Antwerpen", municipality="Antwerpen", workplace_type=JobPosting.Workplace.HYBRID, employment_types=["full_time"], direct_employer=True, ) source = Source.objects.create( name="SmartRecruiters", source_type=Source.Type.ATS, base_url="https://jobs.smartrecruiters.com/example", domain="jobs.smartrecruiters.com", status=Source.Status.ACTIVE, policy=Source.Policy.ALLOW, ) decision = resolve_direct_employer_match( _draft_for_test( JobPosting.objects.first(), location_text="Gent", municipality="Gent", region="Oost-Vlaanderen", ), source=source, ) assert decision.job is None assert decision.reason == "review_direct_conflict" @pytest.mark.django_db def test_resolver_keeps_candidate_unmerged_when_similarity_is_conservative(): employer = Employer.objects.create( name="Example IT", normalized_name="example it", domain="jobs.example.org", ) JobPosting.objects.create( employer=employer, original_title="Security Engineer", normalized_title="security engineer", canonical_url="https://jobs.example.org/vacatures/security-engineer", canonical_key="c" * 64, job_family="security", content_hash="c" * 64, description_text="Security engineer voor beveiliging.", raw_location="Antwerpen", region="Antwerpen", municipality="Antwerpen", workplace_type=JobPosting.Workplace.HYBRID, employment_types=["full_time"], direct_employer=True, ) source = Source.objects.create( name="StaffingBoard", source_type=Source.Type.ATS, base_url="https://jobs.staffingboard.com/example", domain="jobs.staffingboard.com", status=Source.Status.ACTIVE, policy=Source.Policy.ALLOW, ) decision = resolve_direct_employer_match( _draft_for_test( JobPosting.objects.first(), title="Data Analyst", normalized_title="data analyst", location_text="Brussel", employer_name="Another Employer", employer_domain="jobs.staffingboard.com", ), source=source, ) assert decision.job is None assert decision.reason == "no_direct_resolution"