From 0334cac3dc0fea92f5ddb395b8b8ee096402daed Mon Sep 17 00:00:00 2001 From: "soroush.asadi" Date: Sun, 21 Jun 2026 20:28:04 +0330 Subject: [PATCH] Make source-link listings reachable + skip uncontactable applicants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit (1) The contact modal only offered a click-through link for Divar sources, so medboom/ iranestekhdam/channel listings with no inline phone looked uncontactable. Offer the source link for ANY source («مشاهده آگهی در منبع»), for talent, shifts, and jobs alike — rescuing the dead applicant cards that actually have a source URL. (2) At publish, skip an applicant («آماده به کار») that has NO contact path at all — no phone, no contact channel, and no source URL. Such a card cannot reach anyone. Existing ones drop out when the talent reprocess button rebuilds the board. Co-Authored-By: Claude Opus 4.8 --- src/JobsMedical.Web/Program.cs | 12 ++++++------ .../Services/Scraping/IngestionService.cs | 7 +++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/JobsMedical.Web/Program.cs b/src/JobsMedical.Web/Program.cs index e7a6895..c67d4ef 100644 --- a/src/JobsMedical.Web/Program.cs +++ b/src/JobsMedical.Web/Program.cs @@ -426,8 +426,8 @@ app.MapGet("/contact", async (string? type, int id, AppDbContext db, InterestSer if (!string.IsNullOrWhiteSpace(s.Facility!.BaleId)) items.Add(Item(ContactType.Bale, s.Facility.BaleId!)); } if (items.Count == 0 && !string.IsNullOrWhiteSpace(s.SourceUrl) - && Uri.TryCreate(s.SourceUrl, UriKind.Absolute, out var ss) && ss.Host.Contains("divar")) - { fallbackUrl = s.SourceUrl; fallbackLabel = "مشاهده شماره در دیوار ↗"; } + && Uri.TryCreate(s.SourceUrl, UriKind.Absolute, out var ss)) + { fallbackUrl = s.SourceUrl; fallbackLabel = ss.Host.Contains("divar") ? "مشاهده شماره در دیوار ↗" : "مشاهده آگهی در منبع ↗"; } await interest.LogAsync(InterestEventType.Apply, id); break; } @@ -444,8 +444,8 @@ app.MapGet("/contact", async (string? type, int id, AppDbContext db, InterestSer if (!string.IsNullOrWhiteSpace(j.Facility!.BaleId)) items.Add(Item(ContactType.Bale, j.Facility.BaleId!)); } if (items.Count == 0 && !string.IsNullOrWhiteSpace(j.SourceUrl) - && Uri.TryCreate(j.SourceUrl, UriKind.Absolute, out var js) && js.Host.Contains("divar")) - { fallbackUrl = j.SourceUrl; fallbackLabel = "مشاهده شماره در دیوار ↗"; } + && Uri.TryCreate(j.SourceUrl, UriKind.Absolute, out var js)) + { fallbackUrl = j.SourceUrl; fallbackLabel = js.Host.Contains("divar") ? "مشاهده شماره در دیوار ↗" : "مشاهده آگهی در منبع ↗"; } await interest.LogJobAsync(InterestEventType.Apply, id); break; } @@ -458,8 +458,8 @@ app.MapGet("/contact", async (string? type, int id, AppDbContext db, InterestSer items.AddRange(t.Contacts.OrderBy(c => c.SortOrder).Select(c => Item(c.Type, c.Value))); if (items.Count == 0 && !string.IsNullOrWhiteSpace(t.Phone)) items.Add(Item(ContactType.Mobile, t.Phone!)); if (items.Count == 0 && !string.IsNullOrWhiteSpace(t.SourceUrl) - && Uri.TryCreate(t.SourceUrl, UriKind.Absolute, out var su) && su.Host.Contains("divar")) - { fallbackUrl = t.SourceUrl; fallbackLabel = "مشاهده شماره در دیوار ↗"; } + && Uri.TryCreate(t.SourceUrl, UriKind.Absolute, out var su)) + { fallbackUrl = t.SourceUrl; fallbackLabel = su.Host.Contains("divar") ? "مشاهده شماره در دیوار ↗" : "مشاهده آگهی در منبع ↗"; } break; } default: return Results.BadRequest(); diff --git a/src/JobsMedical.Web/Services/Scraping/IngestionService.cs b/src/JobsMedical.Web/Services/Scraping/IngestionService.cs index e998e5b..77543a0 100644 --- a/src/JobsMedical.Web/Services/Scraping/IngestionService.cs +++ b/src/JobsMedical.Web/Services/Scraping/IngestionService.cs @@ -677,6 +677,13 @@ public class IngestionService // «آماده به کار» — a worker offering themselves. No facility involved. if (parsed.Kind == ListingKind.Talent || kindStr.Contains("talent") || kindStr.Contains("آماده")) { + // Skip uncontactable applicants: no phone, no other contact channel, AND no source link to + // click through to — such a card is useless to an employer. (A source URL is enough, since + // the detail page now offers a «مشاهده آگهی در منبع» link for any source.) + var reachable = !string.IsNullOrWhiteSpace(d?.Phone) || !string.IsNullOrWhiteSpace(parsed.Phone) + || parsed.Contacts.Count > 0 || !string.IsNullOrWhiteSpace(raw.SourceUrl); + if (!reachable) { raw.Status = RawListingStatus.Discarded; return; } + // ONE person = ONE listing. Do NOT fan out across roles: an applicant has a single // profession, and «پرستار» + «پرستار کودک» from the same ad were producing duplicate // cards. Use the primary (AI) role; any secondary role names become searchable tags.