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:
@@ -0,0 +1,87 @@
|
||||
#nullable enable
|
||||
using System.Text;
|
||||
using Baya.Application.Contracts.Common;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Baya.Infrastructure.CrossCutting.Seams;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic, network-free <see cref="IGeocoder"/> — the mock seam. It resolves an address to a
|
||||
/// stable point jittered around the known city centroid (an unknown city falls back to Iran's centroid),
|
||||
/// so the same address always yields the same coordinates without any external call. A configured global
|
||||
/// switch or a per-address marker forces the null-coordinate path so the "saved without a map pin" state
|
||||
/// is testable. The real implementation swaps to a Neshan/Google geocoding client behind this contract.
|
||||
/// </summary>
|
||||
public sealed class MockGeocoder(IOptions<SeamOptions> options) : IGeocoder
|
||||
{
|
||||
private readonly GeocodingOptions _options = options.Value.Geocoding;
|
||||
|
||||
// A few real city centroids for plausible pins; everything else falls back to Iran's centroid.
|
||||
private static readonly IReadOnlyDictionary<string, (decimal Lat, decimal Lng)> Centroids =
|
||||
new Dictionary<string, (decimal, decimal)>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["Tehran"] = (35.6892m, 51.3890m),
|
||||
["Karaj"] = (35.8400m, 50.9391m),
|
||||
["Mashhad"] = (36.2605m, 59.6168m),
|
||||
["Isfahan"] = (32.6539m, 51.6660m),
|
||||
["Shiraz"] = (29.5918m, 52.5837m),
|
||||
["Tabriz"] = (38.0800m, 46.2919m),
|
||||
["Ahvaz"] = (31.3183m, 48.6706m),
|
||||
["Qom"] = (34.6416m, 50.8746m),
|
||||
};
|
||||
|
||||
private static readonly (decimal Lat, decimal Lng) IranCentroid = (32.4279m, 53.6880m);
|
||||
|
||||
public ValueTask<GeocodeResult> GeocodeAsync(
|
||||
string addressText,
|
||||
string cityName,
|
||||
string? districtName,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var formatted = FormatAddress(addressText, cityName, districtName);
|
||||
|
||||
var unresolved =
|
||||
_options.ReturnNullCoordinates ||
|
||||
(!string.IsNullOrEmpty(_options.LowConfidenceMarker) &&
|
||||
addressText is not null &&
|
||||
addressText.Contains(_options.LowConfidenceMarker, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (unresolved)
|
||||
return ValueTask.FromResult(new GeocodeResult(null, null, formatted, 0.2));
|
||||
|
||||
var centroid = Centroids.TryGetValue(cityName ?? string.Empty, out var c) ? c : IranCentroid;
|
||||
|
||||
// Deterministic ±~0.045° jitter (~5 km) derived from a stable FNV-1a hash of the full text —
|
||||
// never string.GetHashCode(), which is randomized per process and would break test determinism.
|
||||
var seed = StableHash($"{cityName}|{districtName}|{addressText}");
|
||||
var latOffset = Offset(seed);
|
||||
var lngOffset = Offset(seed >> 16 ^ seed);
|
||||
|
||||
var lat = decimal.Round(centroid.Lat + latOffset, 6);
|
||||
var lng = decimal.Round(centroid.Lng + lngOffset, 6);
|
||||
|
||||
return ValueTask.FromResult(new GeocodeResult(lat, lng, formatted, _options.ResolvedConfidence));
|
||||
}
|
||||
|
||||
private static string FormatAddress(string addressText, string cityName, string? districtName) =>
|
||||
string.Join("، ", new[] { cityName, districtName, addressText }
|
||||
.Where(part => !string.IsNullOrWhiteSpace(part)));
|
||||
|
||||
// Maps the low 16 bits of the seed to a signed offset in [-0.045, +0.045] degrees.
|
||||
private static decimal Offset(uint seed) => ((seed & 0xFFFF) / 65535m - 0.5m) * 0.09m;
|
||||
|
||||
private static uint StableHash(string value)
|
||||
{
|
||||
const uint offsetBasis = 2166136261;
|
||||
const uint prime = 16777619;
|
||||
|
||||
var hash = offsetBasis;
|
||||
foreach (var b in Encoding.UTF8.GetBytes(value))
|
||||
{
|
||||
hash ^= b;
|
||||
hash *= prime;
|
||||
}
|
||||
|
||||
return hash;
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,25 @@ public sealed class SeamOptions
|
||||
public FieldEncryptionOptions FieldEncryption { get; set; } = new();
|
||||
public ObjectStorageOptions ObjectStorage { get; set; } = new();
|
||||
public BankOwnershipOptions BankOwnership { get; set; } = new();
|
||||
public GeocodingOptions Geocoding { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tunes the mock <c>IGeocoder</c>. By default it resolves every address to a deterministic point around
|
||||
/// the city centroid. Set <see cref="ReturnNullCoordinates"/> to force the unresolved path globally, or
|
||||
/// embed <see cref="LowConfidenceMarker"/> in a single address to exercise the "saved without a map pin"
|
||||
/// UI state per-request. The real vendor implementation ignores these.
|
||||
/// </summary>
|
||||
public sealed class GeocodingOptions
|
||||
{
|
||||
/// <summary>When true, every geocode returns null coordinates with low confidence.</summary>
|
||||
public bool ReturnNullCoordinates { get; set; }
|
||||
|
||||
/// <summary>An address whose text contains this marker resolves to null coordinates (testability).</summary>
|
||||
public string LowConfidenceMarker { get; set; } = "NO_GEO";
|
||||
|
||||
/// <summary>Confidence returned for a successfully resolved address.</summary>
|
||||
public double ResolvedConfidence { get; set; } = 0.9;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+4
@@ -32,6 +32,10 @@ public static class ServiceCollectionExtension
|
||||
// fake match; a real Finnotech/banking-bridge client replaces this registration only.
|
||||
services.AddSingleton<IBankAccountOwnershipVerifier, MockBankAccountOwnershipVerifier>();
|
||||
|
||||
// Address geocoding (backend-phase-4). The mock derives deterministic coordinates around the city
|
||||
// centroid with no network call; a real Neshan/Google geocoding client replaces this registration.
|
||||
services.AddSingleton<IGeocoder, MockGeocoder>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,5 +121,15 @@ public class ApplicationDbContext: IdentityDbContext<User, Role, int, UserClaim,
|
||||
builder.Property(a => a.AccountHolderName).HasConversion(encrypted);
|
||||
builder.Property(a => a.Iban).HasConversion(encrypted);
|
||||
});
|
||||
|
||||
// b4 address PII: street line, postal code and recipient contact are encrypted at rest through
|
||||
// the same seam; the title label and coordinates are not PII and stay plaintext.
|
||||
modelBuilder.Entity<CustomerAddress>(builder =>
|
||||
{
|
||||
builder.Property(a => a.AddressLine).HasConversion(encrypted);
|
||||
builder.Property(a => a.PostalCode).HasConversion(encrypted);
|
||||
builder.Property(a => a.RecipientName).HasConversion(encrypted);
|
||||
builder.Property(a => a.RecipientPhone).HasConversion(encrypted);
|
||||
});
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
using Baya.Domain.Entities.Geography;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.GeographyConfig;
|
||||
|
||||
internal sealed class CityConfig : IEntityTypeConfiguration<City>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<City> builder)
|
||||
{
|
||||
builder.ToTable("Cities", "geo");
|
||||
|
||||
builder.Property(c => c.NameFa).HasMaxLength(150).IsRequired();
|
||||
builder.Property(c => c.NameEn).HasMaxLength(150).IsRequired();
|
||||
builder.Property(c => c.SortOrder).HasDefaultValue(0);
|
||||
builder.Property(c => c.IsActive).HasDefaultValue(true);
|
||||
|
||||
// Ordered cascading lookup: cities for a province in sort order.
|
||||
builder.HasIndex(c => new { c.ProvinceId, c.SortOrder });
|
||||
|
||||
builder.HasOne(c => c.Province)
|
||||
.WithMany(p => p.Cities)
|
||||
.HasForeignKey(c => c.ProvinceId)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasQueryFilter(c => c.DeletedAt == null);
|
||||
|
||||
builder.HasData(GeographySeed.Cities());
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
using Baya.Domain.Entities.Geography;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.GeographyConfig;
|
||||
|
||||
internal sealed class DistrictConfig : IEntityTypeConfiguration<District>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<District> builder)
|
||||
{
|
||||
builder.ToTable("Districts", "geo");
|
||||
|
||||
builder.Property(d => d.NameFa).HasMaxLength(150).IsRequired();
|
||||
builder.Property(d => d.NameEn).HasMaxLength(150).IsRequired();
|
||||
builder.Property(d => d.SortOrder).HasDefaultValue(0);
|
||||
builder.Property(d => d.IsActive).HasDefaultValue(true);
|
||||
|
||||
builder.HasIndex(d => new { d.CityId, d.SortOrder });
|
||||
|
||||
builder.HasOne(d => d.City)
|
||||
.WithMany(c => c.Districts)
|
||||
.HasForeignKey(d => d.CityId)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasQueryFilter(d => d.DeletedAt == null);
|
||||
|
||||
builder.HasData(GeographySeed.Districts());
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.GeographyConfig;
|
||||
|
||||
/// <summary>
|
||||
/// The one-time province/city/district seed, loaded via <c>HasData</c> so the rows land with the
|
||||
/// migration on a fresh DB (the b1 seeding path). Ids are fixed and deterministic — city id is
|
||||
/// <c>100 + provinceId</c>, Tehran's districts are <c>1001…1022</c> — so re-running is idempotent and the
|
||||
/// model snapshot stays stable. All 31 provinces get their capital city (which covers the product's
|
||||
/// white-space targets — Tehran, Karaj, Mashhad, Isfahan, Shiraz, Tabriz, Ahvaz, Qom, all provincial
|
||||
/// capitals); only Tehran gets districts at seed time. Adding neighborhoods elsewhere is a later admin
|
||||
/// insert, never a deploy.
|
||||
/// </summary>
|
||||
internal static class GeographySeed
|
||||
{
|
||||
public const long TehranProvinceId = 1;
|
||||
public const long TehranCityId = 101;
|
||||
|
||||
// (id, name_fa, name_en, capital_fa, capital_en). Tehran first, then by convention; sort_order = id.
|
||||
private static readonly (long Id, string NameFa, string NameEn, string CapitalFa, string CapitalEn)[] ProvinceRows =
|
||||
[
|
||||
(1, "تهران", "Tehran", "تهران", "Tehran"),
|
||||
(2, "البرز", "Alborz", "کرج", "Karaj"),
|
||||
(3, "اصفهان", "Isfahan", "اصفهان", "Isfahan"),
|
||||
(4, "فارس", "Fars", "شیراز", "Shiraz"),
|
||||
(5, "خراسان رضوی", "Razavi Khorasan", "مشهد", "Mashhad"),
|
||||
(6, "آذربایجان شرقی", "East Azerbaijan", "تبریز", "Tabriz"),
|
||||
(7, "آذربایجان غربی", "West Azerbaijan", "ارومیه", "Urmia"),
|
||||
(8, "خوزستان", "Khuzestan", "اهواز", "Ahvaz"),
|
||||
(9, "قم", "Qom", "قم", "Qom"),
|
||||
(10, "کرمان", "Kerman", "کرمان", "Kerman"),
|
||||
(11, "گیلان", "Gilan", "رشت", "Rasht"),
|
||||
(12, "مازندران", "Mazandaran", "ساری", "Sari"),
|
||||
(13, "مرکزی", "Markazi", "اراک", "Arak"),
|
||||
(14, "اردبیل", "Ardabil", "اردبیل", "Ardabil"),
|
||||
(15, "قزوین", "Qazvin", "قزوین", "Qazvin"),
|
||||
(16, "کرمانشاه", "Kermanshah", "کرمانشاه", "Kermanshah"),
|
||||
(17, "خراسان شمالی", "North Khorasan", "بجنورد", "Bojnord"),
|
||||
(18, "خراسان جنوبی", "South Khorasan", "بیرجند", "Birjand"),
|
||||
(19, "همدان", "Hamadan", "همدان", "Hamadan"),
|
||||
(20, "کردستان", "Kurdistan", "سنندج", "Sanandaj"),
|
||||
(21, "لرستان", "Lorestan", "خرمآباد", "Khorramabad"),
|
||||
(22, "گلستان", "Golestan", "گرگان", "Gorgan"),
|
||||
(23, "هرمزگان", "Hormozgan", "بندرعباس", "Bandar Abbas"),
|
||||
(24, "بوشهر", "Bushehr", "بوشهر", "Bushehr"),
|
||||
(25, "زنجان", "Zanjan", "زنجان", "Zanjan"),
|
||||
(26, "سمنان", "Semnan", "سمنان", "Semnan"),
|
||||
(27, "یزد", "Yazd", "یزد", "Yazd"),
|
||||
(28, "سیستان و بلوچستان", "Sistan and Baluchestan", "زاهدان", "Zahedan"),
|
||||
(29, "چهارمحال و بختیاری", "Chaharmahal and Bakhtiari", "شهرکرد", "Shahrekord"),
|
||||
(30, "کهگیلویه و بویراحمد", "Kohgiluyeh and Boyer-Ahmad", "یاسوج", "Yasuj"),
|
||||
(31, "ایلام", "Ilam", "ایلام", "Ilam"),
|
||||
];
|
||||
|
||||
// Tehran's 22 municipal مناطق, as Persian ordinals ("منطقه ۱" … "منطقه ۲۲").
|
||||
private static readonly string[] PersianDigits = ["۰", "۱", "۲", "۳", "۴", "۵", "۶", "۷", "۸", "۹"];
|
||||
|
||||
public static object[] Provinces()
|
||||
{
|
||||
var ts = SeedConstants.Timestamp;
|
||||
return ProvinceRows
|
||||
.Select(p => (object)new
|
||||
{
|
||||
p.Id,
|
||||
p.NameFa,
|
||||
p.NameEn,
|
||||
SortOrder = (int)p.Id,
|
||||
IsActive = true,
|
||||
CreatedAt = ts
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static object[] Cities()
|
||||
{
|
||||
var ts = SeedConstants.Timestamp;
|
||||
return ProvinceRows
|
||||
.Select(p => (object)new
|
||||
{
|
||||
Id = 100 + p.Id,
|
||||
ProvinceId = p.Id,
|
||||
NameFa = p.CapitalFa,
|
||||
NameEn = p.CapitalEn,
|
||||
SortOrder = 1,
|
||||
IsActive = true,
|
||||
CreatedAt = ts
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
public static object[] Districts()
|
||||
{
|
||||
var ts = SeedConstants.Timestamp;
|
||||
return Enumerable.Range(1, 22)
|
||||
.Select(n => (object)new
|
||||
{
|
||||
Id = 1000L + n,
|
||||
CityId = TehranCityId,
|
||||
NameFa = $"منطقه {ToPersianNumber(n)}",
|
||||
NameEn = $"District {n}",
|
||||
SortOrder = n,
|
||||
IsActive = true,
|
||||
CreatedAt = ts
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
private static string ToPersianNumber(int value) =>
|
||||
string.Concat(value.ToString(System.Globalization.CultureInfo.InvariantCulture)
|
||||
.Select(c => PersianDigits[c - '0']));
|
||||
}
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
using Baya.Domain.Entities.Geography;
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.GeographyConfig;
|
||||
|
||||
internal sealed class NurseServiceAreaConfig : IEntityTypeConfiguration<NurseServiceArea>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<NurseServiceArea> builder)
|
||||
{
|
||||
builder.ToTable("NurseServiceAreas", "geo");
|
||||
|
||||
builder.Property(a => a.IsActive).HasDefaultValue(true);
|
||||
|
||||
// UNIQUE(nurse_id, city_id, district_id) that correctly rejects a duplicate whole-city row.
|
||||
// SQL Server treats NULLs as distinct, so a single unique index would wrongly allow two
|
||||
// "whole city" rows for the same nurse+city. Split into a filtered pair: one enforces at most one
|
||||
// whole-city row (district_id IS NULL), the other enforces uniqueness of city+district rows. Both
|
||||
// exclude soft-deleted rows so a removed area can be re-declared.
|
||||
builder.HasIndex(a => new { a.NurseId, a.CityId })
|
||||
.IsUnique()
|
||||
.HasFilter("[DistrictId] IS NULL AND [DeletedAt] IS NULL")
|
||||
.HasDatabaseName("UX_NurseServiceAreas_Nurse_City_WholeCity");
|
||||
|
||||
builder.HasIndex(a => new { a.NurseId, a.CityId, a.DistrictId })
|
||||
.IsUnique()
|
||||
.HasFilter("[DistrictId] IS NOT NULL AND [DeletedAt] IS NULL")
|
||||
.HasDatabaseName("UX_NurseServiceAreas_Nurse_City_District");
|
||||
|
||||
builder.HasOne<NurseProfile>()
|
||||
.WithMany()
|
||||
.HasForeignKey(a => a.NurseId)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasOne(a => a.City)
|
||||
.WithMany()
|
||||
.HasForeignKey(a => a.CityId)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasOne(a => a.District)
|
||||
.WithMany()
|
||||
.HasForeignKey(a => a.DistrictId)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasQueryFilter(a => a.DeletedAt == null);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
using Baya.Domain.Entities.Geography;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.GeographyConfig;
|
||||
|
||||
internal sealed class ProvinceConfig : IEntityTypeConfiguration<Province>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Province> builder)
|
||||
{
|
||||
builder.ToTable("Provinces", "geo");
|
||||
|
||||
builder.Property(p => p.NameFa).HasMaxLength(150).IsRequired();
|
||||
builder.Property(p => p.NameEn).HasMaxLength(150).IsRequired();
|
||||
builder.Property(p => p.SortOrder).HasDefaultValue(0);
|
||||
builder.Property(p => p.IsActive).HasDefaultValue(true);
|
||||
|
||||
builder.HasIndex(p => p.SortOrder);
|
||||
|
||||
builder.HasQueryFilter(p => p.DeletedAt == null);
|
||||
|
||||
builder.HasData(GeographySeed.Provinces());
|
||||
}
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
using Baya.Domain.Entities.Geography;
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.IdentityConfig;
|
||||
|
||||
internal sealed class CustomerAddressConfig : IEntityTypeConfiguration<CustomerAddress>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<CustomerAddress> builder)
|
||||
{
|
||||
builder.ToTable("CustomerAddresses", "usr");
|
||||
|
||||
builder.Property(a => a.Title).HasMaxLength(100).IsRequired();
|
||||
|
||||
// address_line, postal_code and recipient contact are encrypted at rest (converters wired in
|
||||
// ApplicationDbContext). The title and coordinates are not PII and stay plaintext.
|
||||
builder.Property(a => a.Latitude).HasPrecision(9, 6);
|
||||
builder.Property(a => a.Longitude).HasPrecision(9, 6);
|
||||
builder.Property(a => a.IsPrimary).HasDefaultValue(false);
|
||||
|
||||
// Exactly one primary address per customer — the authoritative DB backstop the set-primary
|
||||
// transaction must never trip. Excludes soft-deleted rows.
|
||||
builder.HasIndex(a => a.CustomerId)
|
||||
.IsUnique()
|
||||
.HasFilter("[IsPrimary] = 1 AND [DeletedAt] IS NULL")
|
||||
.HasDatabaseName("UX_CustomerAddresses_Customer_Primary");
|
||||
|
||||
builder.HasOne(a => a.Customer)
|
||||
.WithMany()
|
||||
.HasForeignKey(a => a.CustomerId)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasOne(a => a.City)
|
||||
.WithMany()
|
||||
.HasForeignKey(a => a.CityId)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasOne(a => a.District)
|
||||
.WithMany()
|
||||
.HasForeignKey(a => a.DistrictId)
|
||||
.IsRequired(false);
|
||||
|
||||
builder.HasQueryFilter(a => a.DeletedAt == null);
|
||||
}
|
||||
}
|
||||
+2516
File diff suppressed because it is too large
Load Diff
+391
@@ -0,0 +1,391 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
#pragma warning disable CA1814 // Prefer jagged arrays over multidimensional
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class GeographyAddressesServiceAreas : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.EnsureSchema(
|
||||
name: "geo");
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Provinces",
|
||||
schema: "geo",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
NameFa = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: false),
|
||||
NameEn = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: false),
|
||||
SortOrder = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
|
||||
DeletedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
CreatedById = table.Column<int>(type: "int", nullable: true),
|
||||
ModifiedById = table.Column<int>(type: "int", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Provinces", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Cities",
|
||||
schema: "geo",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
ProvinceId = table.Column<long>(type: "bigint", nullable: false),
|
||||
NameFa = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: false),
|
||||
NameEn = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: false),
|
||||
SortOrder = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
|
||||
DeletedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
CreatedById = table.Column<int>(type: "int", nullable: true),
|
||||
ModifiedById = table.Column<int>(type: "int", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Cities", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Cities_Provinces_ProvinceId",
|
||||
column: x => x.ProvinceId,
|
||||
principalSchema: "geo",
|
||||
principalTable: "Provinces",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Districts",
|
||||
schema: "geo",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
CityId = table.Column<long>(type: "bigint", nullable: false),
|
||||
NameFa = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: false),
|
||||
NameEn = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: false),
|
||||
SortOrder = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
|
||||
DeletedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
CreatedById = table.Column<int>(type: "int", nullable: true),
|
||||
ModifiedById = table.Column<int>(type: "int", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_Districts", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Districts_Cities_CityId",
|
||||
column: x => x.CityId,
|
||||
principalSchema: "geo",
|
||||
principalTable: "Cities",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CustomerAddresses",
|
||||
schema: "usr",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
CustomerId = table.Column<long>(type: "bigint", nullable: false),
|
||||
CityId = table.Column<long>(type: "bigint", nullable: false),
|
||||
DistrictId = table.Column<long>(type: "bigint", nullable: true),
|
||||
Title = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
|
||||
AddressLine = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
PostalCode = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Latitude = table.Column<decimal>(type: "decimal(9,6)", precision: 9, scale: 6, nullable: true),
|
||||
Longitude = table.Column<decimal>(type: "decimal(9,6)", precision: 9, scale: 6, nullable: true),
|
||||
IsPrimary = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
RecipientName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
RecipientPhone = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
DeletedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
CreatedById = table.Column<int>(type: "int", nullable: true),
|
||||
ModifiedById = table.Column<int>(type: "int", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_CustomerAddresses", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CustomerAddresses_Cities_CityId",
|
||||
column: x => x.CityId,
|
||||
principalSchema: "geo",
|
||||
principalTable: "Cities",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_CustomerAddresses_CustomerProfiles_CustomerId",
|
||||
column: x => x.CustomerId,
|
||||
principalSchema: "usr",
|
||||
principalTable: "CustomerProfiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_CustomerAddresses_Districts_DistrictId",
|
||||
column: x => x.DistrictId,
|
||||
principalSchema: "geo",
|
||||
principalTable: "Districts",
|
||||
principalColumn: "Id");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NurseServiceAreas",
|
||||
schema: "geo",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
NurseId = table.Column<long>(type: "bigint", nullable: false),
|
||||
CityId = table.Column<long>(type: "bigint", nullable: false),
|
||||
DistrictId = table.Column<long>(type: "bigint", nullable: true),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: true),
|
||||
DeletedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false),
|
||||
ModifiedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
|
||||
CreatedById = table.Column<int>(type: "int", nullable: true),
|
||||
ModifiedById = table.Column<int>(type: "int", nullable: true)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("PK_NurseServiceAreas", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_NurseServiceAreas_Cities_CityId",
|
||||
column: x => x.CityId,
|
||||
principalSchema: "geo",
|
||||
principalTable: "Cities",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_NurseServiceAreas_Districts_DistrictId",
|
||||
column: x => x.DistrictId,
|
||||
principalSchema: "geo",
|
||||
principalTable: "Districts",
|
||||
principalColumn: "Id");
|
||||
table.ForeignKey(
|
||||
name: "FK_NurseServiceAreas_NurseProfiles_NurseId",
|
||||
column: x => x.NurseId,
|
||||
principalSchema: "usr",
|
||||
principalTable: "NurseProfiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
schema: "geo",
|
||||
table: "Provinces",
|
||||
columns: new[] { "Id", "CreatedAt", "CreatedById", "DeletedAt", "IsActive", "ModifiedAt", "ModifiedById", "NameEn", "NameFa", "SortOrder" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Tehran", "تهران", 1 },
|
||||
{ 2L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Alborz", "البرز", 2 },
|
||||
{ 3L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Isfahan", "اصفهان", 3 },
|
||||
{ 4L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Fars", "فارس", 4 },
|
||||
{ 5L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Razavi Khorasan", "خراسان رضوی", 5 },
|
||||
{ 6L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "East Azerbaijan", "آذربایجان شرقی", 6 },
|
||||
{ 7L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "West Azerbaijan", "آذربایجان غربی", 7 },
|
||||
{ 8L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Khuzestan", "خوزستان", 8 },
|
||||
{ 9L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Qom", "قم", 9 },
|
||||
{ 10L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Kerman", "کرمان", 10 },
|
||||
{ 11L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Gilan", "گیلان", 11 },
|
||||
{ 12L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Mazandaran", "مازندران", 12 },
|
||||
{ 13L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Markazi", "مرکزی", 13 },
|
||||
{ 14L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Ardabil", "اردبیل", 14 },
|
||||
{ 15L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Qazvin", "قزوین", 15 },
|
||||
{ 16L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Kermanshah", "کرمانشاه", 16 },
|
||||
{ 17L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "North Khorasan", "خراسان شمالی", 17 },
|
||||
{ 18L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "South Khorasan", "خراسان جنوبی", 18 },
|
||||
{ 19L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Hamadan", "همدان", 19 },
|
||||
{ 20L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Kurdistan", "کردستان", 20 },
|
||||
{ 21L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Lorestan", "لرستان", 21 },
|
||||
{ 22L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Golestan", "گلستان", 22 },
|
||||
{ 23L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Hormozgan", "هرمزگان", 23 },
|
||||
{ 24L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Bushehr", "بوشهر", 24 },
|
||||
{ 25L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Zanjan", "زنجان", 25 },
|
||||
{ 26L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Semnan", "سمنان", 26 },
|
||||
{ 27L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Yazd", "یزد", 27 },
|
||||
{ 28L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Sistan and Baluchestan", "سیستان و بلوچستان", 28 },
|
||||
{ 29L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Chaharmahal and Bakhtiari", "چهارمحال و بختیاری", 29 },
|
||||
{ 30L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Kohgiluyeh and Boyer-Ahmad", "کهگیلویه و بویراحمد", 30 },
|
||||
{ 31L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Ilam", "ایلام", 31 }
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
schema: "geo",
|
||||
table: "Cities",
|
||||
columns: new[] { "Id", "CreatedAt", "CreatedById", "DeletedAt", "IsActive", "ModifiedAt", "ModifiedById", "NameEn", "NameFa", "ProvinceId", "SortOrder" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Tehran", "تهران", 1L, 1 },
|
||||
{ 102L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Karaj", "کرج", 2L, 1 },
|
||||
{ 103L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Isfahan", "اصفهان", 3L, 1 },
|
||||
{ 104L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Shiraz", "شیراز", 4L, 1 },
|
||||
{ 105L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Mashhad", "مشهد", 5L, 1 },
|
||||
{ 106L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Tabriz", "تبریز", 6L, 1 },
|
||||
{ 107L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Urmia", "ارومیه", 7L, 1 },
|
||||
{ 108L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Ahvaz", "اهواز", 8L, 1 },
|
||||
{ 109L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Qom", "قم", 9L, 1 },
|
||||
{ 110L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Kerman", "کرمان", 10L, 1 },
|
||||
{ 111L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Rasht", "رشت", 11L, 1 },
|
||||
{ 112L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Sari", "ساری", 12L, 1 },
|
||||
{ 113L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Arak", "اراک", 13L, 1 },
|
||||
{ 114L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Ardabil", "اردبیل", 14L, 1 },
|
||||
{ 115L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Qazvin", "قزوین", 15L, 1 },
|
||||
{ 116L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Kermanshah", "کرمانشاه", 16L, 1 },
|
||||
{ 117L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Bojnord", "بجنورد", 17L, 1 },
|
||||
{ 118L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Birjand", "بیرجند", 18L, 1 },
|
||||
{ 119L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Hamadan", "همدان", 19L, 1 },
|
||||
{ 120L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Sanandaj", "سنندج", 20L, 1 },
|
||||
{ 121L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Khorramabad", "خرمآباد", 21L, 1 },
|
||||
{ 122L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Gorgan", "گرگان", 22L, 1 },
|
||||
{ 123L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Bandar Abbas", "بندرعباس", 23L, 1 },
|
||||
{ 124L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Bushehr", "بوشهر", 24L, 1 },
|
||||
{ 125L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Zanjan", "زنجان", 25L, 1 },
|
||||
{ 126L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Semnan", "سمنان", 26L, 1 },
|
||||
{ 127L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Yazd", "یزد", 27L, 1 },
|
||||
{ 128L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Zahedan", "زاهدان", 28L, 1 },
|
||||
{ 129L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Shahrekord", "شهرکرد", 29L, 1 },
|
||||
{ 130L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Yasuj", "یاسوج", 30L, 1 },
|
||||
{ 131L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "Ilam", "ایلام", 31L, 1 }
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
schema: "geo",
|
||||
table: "Districts",
|
||||
columns: new[] { "Id", "CityId", "CreatedAt", "CreatedById", "DeletedAt", "IsActive", "ModifiedAt", "ModifiedById", "NameEn", "NameFa", "SortOrder" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1001L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 1", "منطقه ۱", 1 },
|
||||
{ 1002L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 2", "منطقه ۲", 2 },
|
||||
{ 1003L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 3", "منطقه ۳", 3 },
|
||||
{ 1004L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 4", "منطقه ۴", 4 },
|
||||
{ 1005L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 5", "منطقه ۵", 5 },
|
||||
{ 1006L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 6", "منطقه ۶", 6 },
|
||||
{ 1007L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 7", "منطقه ۷", 7 },
|
||||
{ 1008L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 8", "منطقه ۸", 8 },
|
||||
{ 1009L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 9", "منطقه ۹", 9 },
|
||||
{ 1010L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 10", "منطقه ۱۰", 10 },
|
||||
{ 1011L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 11", "منطقه ۱۱", 11 },
|
||||
{ 1012L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 12", "منطقه ۱۲", 12 },
|
||||
{ 1013L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 13", "منطقه ۱۳", 13 },
|
||||
{ 1014L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 14", "منطقه ۱۴", 14 },
|
||||
{ 1015L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 15", "منطقه ۱۵", 15 },
|
||||
{ 1016L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 16", "منطقه ۱۶", 16 },
|
||||
{ 1017L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 17", "منطقه ۱۷", 17 },
|
||||
{ 1018L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 18", "منطقه ۱۸", 18 },
|
||||
{ 1019L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 19", "منطقه ۱۹", 19 },
|
||||
{ 1020L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 20", "منطقه ۲۰", 20 },
|
||||
{ 1021L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 21", "منطقه ۲۱", 21 },
|
||||
{ 1022L, 101L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, true, null, null, "District 22", "منطقه ۲۲", 22 }
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Cities_ProvinceId_SortOrder",
|
||||
schema: "geo",
|
||||
table: "Cities",
|
||||
columns: new[] { "ProvinceId", "SortOrder" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CustomerAddresses_CityId",
|
||||
schema: "usr",
|
||||
table: "CustomerAddresses",
|
||||
column: "CityId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CustomerAddresses_DistrictId",
|
||||
schema: "usr",
|
||||
table: "CustomerAddresses",
|
||||
column: "DistrictId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_CustomerAddresses_Customer_Primary",
|
||||
schema: "usr",
|
||||
table: "CustomerAddresses",
|
||||
column: "CustomerId",
|
||||
unique: true,
|
||||
filter: "[IsPrimary] = 1 AND [DeletedAt] IS NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Districts_CityId_SortOrder",
|
||||
schema: "geo",
|
||||
table: "Districts",
|
||||
columns: new[] { "CityId", "SortOrder" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NurseServiceAreas_CityId",
|
||||
schema: "geo",
|
||||
table: "NurseServiceAreas",
|
||||
column: "CityId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NurseServiceAreas_DistrictId",
|
||||
schema: "geo",
|
||||
table: "NurseServiceAreas",
|
||||
column: "DistrictId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_NurseServiceAreas_Nurse_City_District",
|
||||
schema: "geo",
|
||||
table: "NurseServiceAreas",
|
||||
columns: new[] { "NurseId", "CityId", "DistrictId" },
|
||||
unique: true,
|
||||
filter: "[DistrictId] IS NOT NULL AND [DeletedAt] IS NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_NurseServiceAreas_Nurse_City_WholeCity",
|
||||
schema: "geo",
|
||||
table: "NurseServiceAreas",
|
||||
columns: new[] { "NurseId", "CityId" },
|
||||
unique: true,
|
||||
filter: "[DistrictId] IS NULL AND [DeletedAt] IS NULL");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Provinces_SortOrder",
|
||||
schema: "geo",
|
||||
table: "Provinces",
|
||||
column: "SortOrder");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "CustomerAddresses",
|
||||
schema: "usr");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "NurseServiceAreas",
|
||||
schema: "geo");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Districts",
|
||||
schema: "geo");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Cities",
|
||||
schema: "geo");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Provinces",
|
||||
schema: "geo");
|
||||
}
|
||||
}
|
||||
}
|
||||
+1183
File diff suppressed because it is too large
Load Diff
+6
@@ -13,6 +13,9 @@ public class UnitOfWork : IUnitOfWork
|
||||
public ICustomerProfileRepository CustomerProfileRepository { get; }
|
||||
public IPatientRepository PatientRepository { get; }
|
||||
public INurseBankAccountRepository NurseBankAccountRepository { get; }
|
||||
public IGeoRepository GeoRepository { get; }
|
||||
public INurseServiceAreaRepository NurseServiceAreaRepository { get; }
|
||||
public ICustomerAddressRepository CustomerAddressRepository { get; }
|
||||
|
||||
public UnitOfWork(ApplicationDbContext db)
|
||||
{
|
||||
@@ -24,6 +27,9 @@ public class UnitOfWork : IUnitOfWork
|
||||
CustomerProfileRepository = new CustomerProfileRepository(_db);
|
||||
PatientRepository = new PatientRepository(_db);
|
||||
NurseBankAccountRepository = new NurseBankAccountRepository(_db);
|
||||
GeoRepository = new GeoRepository(_db);
|
||||
NurseServiceAreaRepository = new NurseServiceAreaRepository(_db);
|
||||
CustomerAddressRepository = new CustomerAddressRepository(_db);
|
||||
}
|
||||
|
||||
public Task CommitAsync()
|
||||
|
||||
+78
@@ -0,0 +1,78 @@
|
||||
using Baya.Application.Contracts.Persistence;
|
||||
using Baya.Application.Models.Addresses;
|
||||
using Baya.Application.Models.Common;
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Baya.Infrastructure.Persistence.Repositories.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Repositories;
|
||||
|
||||
internal sealed class CustomerAddressRepository : BaseAsyncRepository<CustomerAddress>, ICustomerAddressRepository
|
||||
{
|
||||
public CustomerAddressRepository(ApplicationDbContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
|
||||
public Task AddAsync(CustomerAddress address, CancellationToken cancellationToken)
|
||||
=> base.AddAsync(address);
|
||||
|
||||
public Task<CustomerAddress> GetOwnedAsync(long id, long customerId, CancellationToken cancellationToken)
|
||||
=> Table.FirstOrDefaultAsync(a => a.Id == id && a.CustomerId == customerId, cancellationToken);
|
||||
|
||||
public Task<bool> HasAnyAsync(long customerId, CancellationToken cancellationToken)
|
||||
=> TableNoTracking.AnyAsync(a => a.CustomerId == customerId, cancellationToken);
|
||||
|
||||
public async Task ClearOtherPrimaryAsync(long customerId, long addressId, CancellationToken cancellationToken)
|
||||
=> await Entities
|
||||
.Where(a => a.CustomerId == customerId && a.IsPrimary && a.Id != addressId)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(a => a.IsPrimary, false), cancellationToken);
|
||||
|
||||
public async Task SetPrimaryAsync(long customerId, long addressId, CancellationToken cancellationToken)
|
||||
{
|
||||
// Clear-then-set inside one transaction so the filtered unique index is never momentarily violated.
|
||||
await using var transaction = await DbContext.Database.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
await Entities
|
||||
.Where(a => a.CustomerId == customerId && a.IsPrimary)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(a => a.IsPrimary, false), cancellationToken);
|
||||
|
||||
await Entities
|
||||
.Where(a => a.Id == addressId && a.CustomerId == customerId)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(a => a.IsPrimary, true), cancellationToken);
|
||||
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public async Task<PagedResult<CustomerAddressDto>> ListAsync(long customerId, int page, int pageSize, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = TableNoTracking.Where(a => a.CustomerId == customerId);
|
||||
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
var items = await query
|
||||
.OrderByDescending(a => a.IsPrimary)
|
||||
.ThenByDescending(a => a.Id)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
// The encrypted columns are decrypted by the value converter on materialization — the owner's
|
||||
// own read is the only path that returns the plaintext address.
|
||||
.Select(a => new CustomerAddressDto(
|
||||
a.Id,
|
||||
a.Title,
|
||||
a.CityId,
|
||||
a.City.NameFa,
|
||||
a.City.NameEn,
|
||||
a.DistrictId,
|
||||
a.DistrictId == null ? null : a.District.NameFa,
|
||||
a.DistrictId == null ? null : a.District.NameEn,
|
||||
a.AddressLine,
|
||||
a.PostalCode,
|
||||
a.Latitude,
|
||||
a.Longitude,
|
||||
a.IsPrimary,
|
||||
a.RecipientName,
|
||||
a.RecipientPhone))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new PagedResult<CustomerAddressDto>(items, total, page, pageSize);
|
||||
}
|
||||
}
|
||||
+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);
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
using Baya.Application.Contracts.Persistence;
|
||||
using Baya.Application.Models.Common;
|
||||
using Baya.Application.Models.Geography;
|
||||
using Baya.Domain.Entities.Geography;
|
||||
using Baya.Infrastructure.Persistence.Repositories.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Repositories;
|
||||
|
||||
internal sealed class NurseServiceAreaRepository : BaseAsyncRepository<NurseServiceArea>, INurseServiceAreaRepository
|
||||
{
|
||||
public NurseServiceAreaRepository(ApplicationDbContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
|
||||
public Task AddAsync(NurseServiceArea area, CancellationToken cancellationToken)
|
||||
=> base.AddAsync(area);
|
||||
|
||||
public Task<NurseServiceArea> GetOwnedAsync(long id, long nurseId, CancellationToken cancellationToken)
|
||||
=> Table.FirstOrDefaultAsync(a => a.Id == id && a.NurseId == nurseId, cancellationToken);
|
||||
|
||||
public Task<bool> DuplicateExistsAsync(long nurseId, long cityId, long? districtId, CancellationToken cancellationToken)
|
||||
=> TableNoTracking.AnyAsync(
|
||||
a => a.NurseId == nurseId && a.CityId == cityId && a.DistrictId == districtId,
|
||||
cancellationToken);
|
||||
|
||||
public async Task<PagedResult<NurseServiceAreaDto>> ListAsync(long nurseId, int page, int pageSize, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = TableNoTracking.Where(a => a.NurseId == nurseId);
|
||||
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
var items = await query
|
||||
// Whole-city rows (district_id NULL) first, then by id.
|
||||
.OrderByDescending(a => a.DistrictId == null)
|
||||
.ThenBy(a => a.Id)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(a => new NurseServiceAreaDto(
|
||||
a.Id,
|
||||
a.CityId,
|
||||
a.City.NameFa,
|
||||
a.City.NameEn,
|
||||
a.DistrictId,
|
||||
a.DistrictId == null ? null : a.District.NameFa,
|
||||
a.DistrictId == null ? null : a.District.NameEn,
|
||||
a.DistrictId == null,
|
||||
a.IsActive))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new PagedResult<NurseServiceAreaDto>(items, total, page, pageSize);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user