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>
This commit is contained in:
+121
@@ -0,0 +1,121 @@
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user