Files
baya-monorepo/server/src/Infrastructure/Baya.Infrastructure.Persistence/Repositories/GeoRepository.cs
T
hamid 82561c4cc6 backend phase 4: geography, addresses & nurse service areas
Adds the province -> city -> district reference hierarchy (geo schema,
seeded with 31 provinces + capital cities + Tehran's 22 districts),
nurse service areas (district_id NULL = whole city, filtered-index-pair
uniqueness -> 409), and encrypted, geocoded customer addresses with a
single-primary invariant. Introduces the IGeocoder seam (mocked) and
409 Conflict on the result envelope. Public cascading lookups are cached
behind a generation-token scheme with invalidate-on-admin-write.

One EF migration (GeographyAddressesServiceAreas, applied). Contract +
swagger snapshot + handoff/report/registry updated. 103 tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-02 16:06:12 +03:30

122 lines
5.5 KiB
C#

using Baya.Application.Contracts.Persistence;
using Baya.Application.Models.Geography;
using Baya.Domain.Entities.Geography;
using Microsoft.EntityFrameworkCore;
namespace Baya.Infrastructure.Persistence.Repositories;
internal sealed class GeoRepository : IGeoRepository
{
private readonly ApplicationDbContext _db;
public GeoRepository(ApplicationDbContext db) => _db = db;
public async Task<IReadOnlyList<ProvinceDto>> ListActiveProvincesAsync(CancellationToken cancellationToken)
=> await _db.Set<Province>()
.AsNoTracking()
.Where(p => p.IsActive)
.OrderBy(p => p.SortOrder)
.Select(p => new ProvinceDto(p.Id, p.NameFa, p.NameEn, p.SortOrder))
.ToListAsync(cancellationToken);
public async Task<IReadOnlyList<CityDto>> ListActiveCitiesAsync(long provinceId, CancellationToken cancellationToken)
=> await _db.Set<City>()
.AsNoTracking()
.Where(c => c.ProvinceId == provinceId && c.IsActive && c.Province.IsActive)
.OrderBy(c => c.SortOrder)
.Select(c => new CityDto(c.Id, c.ProvinceId, c.NameFa, c.NameEn, c.SortOrder))
.ToListAsync(cancellationToken);
public async Task<IReadOnlyList<DistrictDto>> ListActiveDistrictsAsync(long cityId, CancellationToken cancellationToken)
=> await _db.Set<District>()
.AsNoTracking()
.Where(d => d.CityId == cityId && d.IsActive && d.City.IsActive && d.City.Province.IsActive)
.OrderBy(d => d.SortOrder)
.Select(d => new DistrictDto(d.Id, d.CityId, d.NameFa, d.NameEn, d.SortOrder))
.ToListAsync(cancellationToken);
public async Task<IReadOnlyList<ProvinceTreeDto>> GetActiveTreeAsync(CancellationToken cancellationToken)
{
// Three flat ordered reads assembled in memory — avoids a two-level nested collection projection
// (which SQLite can't translate) and keeps the payload cheap for a cached, near-static tree.
var provinces = await _db.Set<Province>()
.AsNoTracking()
.Where(p => p.IsActive)
.OrderBy(p => p.SortOrder)
.Select(p => new { p.Id, p.NameFa, p.NameEn, p.SortOrder })
.ToListAsync(cancellationToken);
var cities = await _db.Set<City>()
.AsNoTracking()
.Where(c => c.IsActive && c.Province.IsActive)
.OrderBy(c => c.SortOrder)
.Select(c => new { c.Id, c.ProvinceId, c.NameFa, c.NameEn, c.SortOrder })
.ToListAsync(cancellationToken);
var districts = await _db.Set<District>()
.AsNoTracking()
.Where(d => d.IsActive && d.City.IsActive && d.City.Province.IsActive)
.OrderBy(d => d.SortOrder)
.Select(d => new DistrictDto(d.Id, d.CityId, d.NameFa, d.NameEn, d.SortOrder))
.ToListAsync(cancellationToken);
var districtsByCity = districts
.GroupBy(d => d.CityId)
.ToDictionary(g => g.Key, g => (IReadOnlyList<DistrictDto>)g.ToList());
var citiesByProvince = cities
.GroupBy(c => c.ProvinceId)
.ToDictionary(
g => g.Key,
g => (IReadOnlyList<CityTreeDto>)g
.Select(c => new CityTreeDto(
c.Id,
c.NameFa,
c.NameEn,
c.SortOrder,
districtsByCity.TryGetValue(c.Id, out var ds) ? ds : []))
.ToList());
return provinces
.Select(p => new ProvinceTreeDto(
p.Id,
p.NameFa,
p.NameEn,
p.SortOrder,
citiesByProvince.TryGetValue(p.Id, out var cs) ? cs : []))
.ToList();
}
public Task<Province> GetProvinceAsync(long id, CancellationToken cancellationToken)
=> _db.Set<Province>().FirstOrDefaultAsync(p => p.Id == id, cancellationToken);
public Task<City> GetCityAsync(long id, CancellationToken cancellationToken)
=> _db.Set<City>().FirstOrDefaultAsync(c => c.Id == id, cancellationToken);
public Task<District> GetDistrictAsync(long id, CancellationToken cancellationToken)
=> _db.Set<District>().FirstOrDefaultAsync(d => d.Id == id, cancellationToken);
public async Task AddProvinceAsync(Province province, CancellationToken cancellationToken)
=> await _db.Set<Province>().AddAsync(province, cancellationToken);
public async Task AddCityAsync(City city, CancellationToken cancellationToken)
=> await _db.Set<City>().AddAsync(city, cancellationToken);
public async Task AddDistrictAsync(District district, CancellationToken cancellationToken)
=> await _db.Set<District>().AddAsync(district, cancellationToken);
public Task<bool> ProvinceExistsAsync(long provinceId, CancellationToken cancellationToken)
=> _db.Set<Province>().AsNoTracking().AnyAsync(p => p.Id == provinceId, cancellationToken);
public Task<bool> IsCityActiveAsync(long cityId, CancellationToken cancellationToken)
=> _db.Set<City>().AsNoTracking()
.AnyAsync(c => c.Id == cityId && c.IsActive && c.Province.IsActive, cancellationToken);
public Task<bool> IsDistrictInActiveCityAsync(long districtId, long cityId, CancellationToken cancellationToken)
=> _db.Set<District>().AsNoTracking()
.AnyAsync(
d => d.Id == districtId && d.CityId == cityId &&
d.IsActive && d.City.IsActive && d.City.Province.IsActive,
cancellationToken);
}