From a335a3b6849e190e30023d8d0e3e488eff2cce29 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Tue, 28 Apr 2026 08:54:30 +0200 Subject: [PATCH] feat(data): add PrimeScheduleRepository Co-Authored-By: Claude Opus 4.7 (1M context) --- .../Repositories/PrimeScheduleRepository.cs | 58 +++++++++++++ .../PrimeScheduleRepositoryTests.cs | 84 +++++++++++++++++++ 2 files changed, 142 insertions(+) create mode 100644 src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs create mode 100644 tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs diff --git a/src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs b/src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs new file mode 100644 index 0000000..2024e37 --- /dev/null +++ b/src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs @@ -0,0 +1,58 @@ +// src/ClaudeDo.Data/Repositories/PrimeScheduleRepository.cs +using ClaudeDo.Data.Models; +using Microsoft.EntityFrameworkCore; + +namespace ClaudeDo.Data.Repositories; + +public sealed class PrimeScheduleRepository +{ + private readonly ClaudeDoDbContext _context; + + public PrimeScheduleRepository(ClaudeDoDbContext context) => _context = context; + + public async Task> ListAsync(CancellationToken ct = default) + { + var rows = await _context.PrimeSchedules.AsNoTracking() + .OrderBy(s => s.StartDate) + .ToListAsync(ct); + return rows.OrderBy(s => s.StartDate).ThenBy(s => s.TimeOfDay).ToList(); + } + + public async Task GetAsync(Guid id, CancellationToken ct = default) => + await _context.PrimeSchedules.AsNoTracking().FirstOrDefaultAsync(s => s.Id == id, ct); + + public async Task UpsertAsync(PrimeScheduleEntity entity, CancellationToken ct = default) + { + var existing = await _context.PrimeSchedules.FirstOrDefaultAsync(s => s.Id == entity.Id, ct); + if (existing is null) + { + _context.PrimeSchedules.Add(entity); + } + else + { + existing.StartDate = entity.StartDate; + existing.EndDate = entity.EndDate; + existing.TimeOfDay = entity.TimeOfDay; + existing.WorkdaysOnly = entity.WorkdaysOnly; + existing.Enabled = entity.Enabled; + existing.PromptOverride = entity.PromptOverride; + } + await _context.SaveChangesAsync(ct); + } + + public async Task DeleteAsync(Guid id, CancellationToken ct = default) + { + var row = await _context.PrimeSchedules.FirstOrDefaultAsync(s => s.Id == id, ct); + if (row is null) return; + _context.PrimeSchedules.Remove(row); + await _context.SaveChangesAsync(ct); + } + + public async Task UpdateLastRunAsync(Guid id, DateTimeOffset when, CancellationToken ct = default) + { + var row = await _context.PrimeSchedules.FirstOrDefaultAsync(s => s.Id == id, ct); + if (row is null) return; + row.LastRunAt = when; + await _context.SaveChangesAsync(ct); + } +} diff --git a/tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs b/tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs new file mode 100644 index 0000000..bcbf33c --- /dev/null +++ b/tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs @@ -0,0 +1,84 @@ +// tests/ClaudeDo.Worker.Tests/Repositories/PrimeScheduleRepositoryTests.cs +using ClaudeDo.Data.Models; +using ClaudeDo.Data.Repositories; +using ClaudeDo.Worker.Tests.Infrastructure; + +namespace ClaudeDo.Worker.Tests.Repositories; + +public class PrimeScheduleRepositoryTests : IDisposable +{ + private readonly DbFixture _db = new(); + public void Dispose() => _db.Dispose(); + + [Fact] + public async Task Upsert_Then_List_RoundTrips() + { + var id = Guid.NewGuid(); + using (var ctx = _db.CreateContext()) + { + await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity + { + Id = id, + StartDate = new DateOnly(2026, 5, 1), + EndDate = new DateOnly(2026, 6, 30), + TimeOfDay = new TimeSpan(7, 0, 0), + WorkdaysOnly = true, + Enabled = true, + CreatedAt = DateTimeOffset.UtcNow, + }); + } + + using var read = _db.CreateContext(); + var rows = await new PrimeScheduleRepository(read).ListAsync(); + Assert.Single(rows); + Assert.Equal(id, rows[0].Id); + Assert.Equal(new TimeSpan(7, 0, 0), rows[0].TimeOfDay); + } + + [Fact] + public async Task UpdateLastRunAt_Persists() + { + var id = Guid.NewGuid(); + var when = new DateTimeOffset(2026, 5, 5, 7, 1, 0, TimeSpan.FromHours(2)); + using (var ctx = _db.CreateContext()) + { + await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity + { + Id = id, + StartDate = new DateOnly(2026, 5, 1), + EndDate = new DateOnly(2026, 5, 31), + TimeOfDay = new TimeSpan(7, 0, 0), + Enabled = true, + CreatedAt = DateTimeOffset.UtcNow, + }); + } + using (var ctx = _db.CreateContext()) + await new PrimeScheduleRepository(ctx).UpdateLastRunAsync(id, when); + + using var read = _db.CreateContext(); + var row = await new PrimeScheduleRepository(read).GetAsync(id); + Assert.NotNull(row); + Assert.Equal(when, row!.LastRunAt); + } + + [Fact] + public async Task Delete_Removes_Row() + { + var id = Guid.NewGuid(); + using (var ctx = _db.CreateContext()) + await new PrimeScheduleRepository(ctx).UpsertAsync(new PrimeScheduleEntity + { + Id = id, + StartDate = new DateOnly(2026, 5, 1), + EndDate = new DateOnly(2026, 5, 1), + TimeOfDay = TimeSpan.Zero, + Enabled = true, + CreatedAt = DateTimeOffset.UtcNow, + }); + using (var ctx = _db.CreateContext()) + await new PrimeScheduleRepository(ctx).DeleteAsync(id); + + using var read = _db.CreateContext(); + Assert.Empty(await new PrimeScheduleRepository(read).ListAsync()); + } +}