📄 BfiScreeningCheckerJob.cs
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Quartz;

[DisallowConcurrentExecution]
internal sealed class BfiScreeningCheckerJob(
    PlaywrightBrowserService browserService,
    ScreeningRepository repository,
    IScheduler scheduler,
    IOptions<MonitorOptions> options,
    ILogger<BfiScreeningCheckerJob> logger
) : IJob
{
    public async Task Execute(IJobExecutionContext context)
    {
        var opts = options.Value;
        var page = await browserService.NewPageAsync();
        try
        {
            await page.GotoAsync(opts.Url);
            // Booking links on BFI What's On pages have "book" in the href.
            // Each href is unique per performance and encodes the date/time/seat map.
            var bookingLinks = await page.QuerySelectorAllAsync("a[href*='book' i], a[href*='seatmap' i]");
            var available = new List<string>();
            foreach (var link in bookingLinks)
            {
                var href = await link.GetAttributeAsync("href");
                if (!string.IsNullOrWhiteSpace(href))
                    available.Add(href);
            }

            logger.LogInformation("Check complete — {Count} booking link(s) found on {Url}", available.Count, opts.Url);

            foreach (var href in available)
            {
                if (await repository.HasBeenNotifiedAsync(href))
                    continue;

                await repository.MarkNotifiedAsync(href);

                var message = $"BFI IMAX tickets available! Book now: {href}";

                foreach (var phone in opts.PhoneNumbers)
                {
                    var dataMap = new JobDataMap { { "phoneNumber", phone }, { "message", message } };

                    var job = JobBuilder
                        .Create<SendSmsJob>()
                        .WithIdentity(Guid.NewGuid().ToString(), "sms")
                        .UsingJobData(dataMap)
                        .Build();

                    var trigger = TriggerBuilder.Create().StartNow().Build();

                    await scheduler.ScheduleJob(job, trigger, context.CancellationToken);
                    logger.LogInformation("Scheduled SMS to {Phone} for screening {Href}", phone, href);
                }
            }
        }
        finally
        {
            await page.CloseAsync();
        }
    }
}