@
backend phase 3: identity profiles, patients & nurse bank accounts Add the role-attached identity layer on top of the b2 auth spine: nurse seller profiles (guarded is_verified, read-only aggregates), thin customer payer profiles, first-class patients (tenancy-scoped), and nurse payout bank accounts hardened with an iban_hash uniqueness guard and an automated استعلام شبا IBAN-ownership inquiry. - Four usr tables via one migration (1:1 uniques, UNIQUE(iban_hash), filtered UNIQUE(nurse_id) WHERE is_primary=1, guarded is_verified, encrypted PII, soft-delete on nurse_profiles) - 15 CQRS slices + 4 role-scoped controllers; reads projected + paginated, IBAN masked (last-4); ownership-inquiry endpoints rate-limited - New IBankAccountOwnershipVerifier seam (mock deterministic شبا match) + per-domain repositories on IUnitOfWork + encrypted-PII value converters - Activate FluentValidation repo-wide (validators were never registered) - Handler unit tests + WebApplicationFactory integration tests (76 pass); contract identity-profiles.md + swagger snapshot; docs, handoff & report Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> @
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
#nullable enable
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using Baya.Application.Contracts.Common;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Baya.Infrastructure.CrossCutting.Seams;
|
||||
|
||||
/// <summary>
|
||||
/// Mock <see cref="IBankAccountOwnershipVerifier"/>: a deterministic fake استعلام شبا inquiry — no real
|
||||
/// bank/KYC call and no money moves. Every IBAN returns a match except the configured
|
||||
/// <see cref="BankOwnershipOptions.MismatchIban"/>, which returns <c>MatchedNationalId = false</c> so the
|
||||
/// ownership-mismatch path is testable. The vendor ref is derived from the IBAN, so re-running the same
|
||||
/// inquiry is idempotent. The real implementation (Finnotech/banking-bridge) swaps in via a registration
|
||||
/// change — callers are unchanged.
|
||||
/// </summary>
|
||||
public sealed class MockBankAccountOwnershipVerifier(IOptions<SeamOptions> options) : IBankAccountOwnershipVerifier
|
||||
{
|
||||
private readonly BankOwnershipOptions _options = options.Value.BankOwnership;
|
||||
|
||||
// nurseNationalId is part of the real vendor contract (owner ↔ national-id match); the mock decides
|
||||
// the outcome from the IBAN alone so both paths are deterministically testable.
|
||||
public Task<OwnershipInquiryResult> VerifyOwnershipAsync(string iban, string? nurseNationalId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalized = Normalize(iban);
|
||||
var matched = !string.Equals(normalized, Normalize(_options.MismatchIban), StringComparison.OrdinalIgnoreCase);
|
||||
var holder = matched ? _options.MatchedHolderName : _options.MismatchHolderName;
|
||||
var vendorRef = $"MOCK-SHEBA-{Token(normalized)}";
|
||||
|
||||
return Task.FromResult(new OwnershipInquiryResult(matched, holder, vendorRef));
|
||||
}
|
||||
|
||||
private static string Normalize(string iban)
|
||||
=> string.IsNullOrEmpty(iban) ? string.Empty : iban.Replace(" ", string.Empty).ToUpperInvariant();
|
||||
|
||||
private static string Token(string value)
|
||||
=> Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(value)))[..12];
|
||||
}
|
||||
@@ -10,6 +10,24 @@ public sealed class SeamOptions
|
||||
|
||||
public FieldEncryptionOptions FieldEncryption { get; set; } = new();
|
||||
public ObjectStorageOptions ObjectStorage { get; set; } = new();
|
||||
public BankOwnershipOptions BankOwnership { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tunes the mock <c>IBankAccountOwnershipVerifier</c> (استعلام شبا). A submitted IBAN equal to
|
||||
/// <see cref="MismatchIban"/> returns an ownership mismatch so the payout-gating path is testable; every
|
||||
/// other IBAN returns a match. The real vendor implementation ignores these.
|
||||
/// </summary>
|
||||
public sealed class BankOwnershipOptions
|
||||
{
|
||||
/// <summary>The designated test IBAN that returns <c>matched_national_id = false</c>.</summary>
|
||||
public string MismatchIban { get; set; } = "IR000000000000000000000000";
|
||||
|
||||
/// <summary>The account-holder name the mock echoes back for a matching inquiry.</summary>
|
||||
public string MatchedHolderName { get; set; } = "Verified Account Holder";
|
||||
|
||||
/// <summary>The account-holder name the mock returns for the mismatch IBAN.</summary>
|
||||
public string MismatchHolderName { get; set; } = "Unmatched Account Holder";
|
||||
}
|
||||
|
||||
public sealed class FieldEncryptionOptions
|
||||
|
||||
+4
@@ -28,6 +28,10 @@ public static class ServiceCollectionExtension
|
||||
// (Kavenegar/Ghasedak/SMS.ir) replaces this registration only.
|
||||
services.AddSingleton<ISmsSender, LoggingSmsSender>();
|
||||
|
||||
// استعلام شبا IBAN-owner ↔ national-id inquiry (backend-phase-3). The mock returns a deterministic
|
||||
// fake match; a real Finnotech/banking-bridge client replaces this registration only.
|
||||
services.AddSingleton<IBankAccountOwnershipVerifier, MockBankAccountOwnershipVerifier>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Reflection;
|
||||
using Baya.Application.Contracts.Common;
|
||||
using Baya.Domain.Common;
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Baya.Domain.Entities.User;
|
||||
using Baya.Infrastructure.Persistence.ValueConversion;
|
||||
using Baya.SharedKernel.Extensions;
|
||||
@@ -103,5 +104,22 @@ public class ApplicationDbContext: IdentityDbContext<User, Role, int, UserClaim,
|
||||
builder.Property(u => u.NormalizedEmail).HasConversion(encrypted);
|
||||
builder.Property(u => u.NationalId).HasConversion(encrypted);
|
||||
});
|
||||
|
||||
// b3 PII: emergency contacts, clinical notes, IBAN and the account-holder name are encrypted at
|
||||
// rest through the same seam. The IBAN's deterministic lookup uses the iban_hash column instead.
|
||||
modelBuilder.Entity<CustomerProfile>(builder =>
|
||||
{
|
||||
builder.Property(c => c.DefaultEmergencyContactName).HasConversion(encrypted);
|
||||
builder.Property(c => c.DefaultEmergencyContactPhone).HasConversion(encrypted);
|
||||
});
|
||||
modelBuilder.Entity<Patient>(builder =>
|
||||
{
|
||||
builder.Property(p => p.InitialMedicalNotes).HasConversion(encrypted);
|
||||
});
|
||||
modelBuilder.Entity<NurseBankAccount>(builder =>
|
||||
{
|
||||
builder.Property(a => a.AccountHolderName).HasConversion(encrypted);
|
||||
builder.Property(a => a.Iban).HasConversion(encrypted);
|
||||
});
|
||||
}
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.IdentityConfig;
|
||||
|
||||
internal sealed class CustomerProfileConfig : IEntityTypeConfiguration<CustomerProfile>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<CustomerProfile> builder)
|
||||
{
|
||||
builder.ToTable("CustomerProfiles", "usr");
|
||||
|
||||
// Emergency-contact columns are encrypted at rest (converter wired in ApplicationDbContext) —
|
||||
// left as nvarchar(max) since ciphertext is longer than the plaintext it carries.
|
||||
|
||||
builder.HasIndex(c => c.UserId).IsUnique();
|
||||
builder.HasOne(c => c.User)
|
||||
.WithOne()
|
||||
.HasForeignKey<CustomerProfile>(c => c.UserId)
|
||||
.IsRequired();
|
||||
}
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Baya.Domain.Entities.User;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.IdentityConfig;
|
||||
|
||||
internal sealed class NurseBankAccountConfig : IEntityTypeConfiguration<NurseBankAccount>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<NurseBankAccount> builder)
|
||||
{
|
||||
builder.ToTable("NurseBankAccounts", "usr");
|
||||
|
||||
builder.Property(a => a.BankName).HasMaxLength(100);
|
||||
builder.Property(a => a.IbanHash).HasMaxLength(64).IsRequired();
|
||||
builder.Property(a => a.AccountHolderFromBank).HasMaxLength(200);
|
||||
builder.Property(a => a.OwnershipVendorRef).HasMaxLength(200);
|
||||
builder.Property(a => a.IsPrimary).HasDefaultValue(false);
|
||||
builder.Property(a => a.IsVerified).HasDefaultValue(false);
|
||||
|
||||
// account_holder_name and iban are encrypted at rest (converters wired in ApplicationDbContext).
|
||||
|
||||
// One IBAN can't silently serve two nurses — the authoritative duplicate backstop.
|
||||
builder.HasIndex(a => a.IbanHash).IsUnique();
|
||||
|
||||
// Exactly one primary account per nurse — the filtered-unique backstop the set-primary
|
||||
// transaction must never trip.
|
||||
builder.HasIndex(a => a.NurseId)
|
||||
.IsUnique()
|
||||
.HasFilter("[IsPrimary] = 1")
|
||||
.HasDatabaseName("UX_NurseBankAccounts_NurseId_Primary");
|
||||
|
||||
builder.HasOne(a => a.Nurse)
|
||||
.WithMany(n => n.BankAccounts)
|
||||
.HasForeignKey(a => a.NurseId)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasOne<User>()
|
||||
.WithMany()
|
||||
.HasForeignKey(a => a.VerifiedByAdminId)
|
||||
.IsRequired(false);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Baya.Domain.Entities.User;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.IdentityConfig;
|
||||
|
||||
internal sealed class NurseProfileConfig : IEntityTypeConfiguration<NurseProfile>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<NurseProfile> builder)
|
||||
{
|
||||
builder.ToTable("NurseProfiles", "usr");
|
||||
|
||||
builder.Property(p => p.Bio).HasMaxLength(2000);
|
||||
builder.Property(p => p.EducationLevel).HasMaxLength(100);
|
||||
builder.Property(p => p.EducationField).HasMaxLength(150);
|
||||
builder.Property(p => p.IsVerified).HasDefaultValue(false);
|
||||
builder.Property(p => p.IsAcceptingBookings).HasDefaultValue(false);
|
||||
|
||||
// Read-only quality aggregates — default 0, recomputed by reviews/bookings phases.
|
||||
builder.Property(p => p.AverageRating).HasPrecision(3, 2).HasDefaultValue(0m);
|
||||
builder.Property(p => p.TotalReviews).HasDefaultValue(0);
|
||||
builder.Property(p => p.TotalCompletedBookings).HasDefaultValue(0);
|
||||
|
||||
// 1:1 with the owning user.
|
||||
builder.HasIndex(p => p.UserId).IsUnique();
|
||||
builder.HasOne(p => p.User)
|
||||
.WithOne()
|
||||
.HasForeignKey<NurseProfile>(p => p.UserId)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasQueryFilter(p => p.DeletedAt == null);
|
||||
}
|
||||
}
|
||||
+29
@@ -0,0 +1,29 @@
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.IdentityConfig;
|
||||
|
||||
internal sealed class PatientConfig : IEntityTypeConfiguration<Patient>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Patient> builder)
|
||||
{
|
||||
builder.ToTable("Patients", "usr");
|
||||
|
||||
builder.Property(p => p.DisplayName).HasMaxLength(200);
|
||||
builder.Property(p => p.FirstName).HasMaxLength(100);
|
||||
builder.Property(p => p.LastName).HasMaxLength(100);
|
||||
builder.Property(p => p.Gender).HasMaxLength(10).IsRequired();
|
||||
builder.Property(p => p.BloodType).HasMaxLength(10);
|
||||
builder.Property(p => p.IsActive).HasDefaultValue(true);
|
||||
|
||||
// initial_medical_notes is encrypted at rest (converter wired in ApplicationDbContext).
|
||||
|
||||
// Tenancy anchor: every list/get is scoped by CustomerId.
|
||||
builder.HasIndex(p => p.CustomerId);
|
||||
builder.HasOne(p => p.Customer)
|
||||
.WithMany(c => c.Patients)
|
||||
.HasForeignKey(p => p.CustomerId)
|
||||
.IsRequired();
|
||||
}
|
||||
}
|
||||
+1333
File diff suppressed because it is too large
Load Diff
+215
@@ -0,0 +1,215 @@
|
||||
using System;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public partial class IdentityProfilesPatientsBankAccounts : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CustomerProfiles",
|
||||
schema: "usr",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
UserId = table.Column<int>(type: "int", nullable: false),
|
||||
DefaultEmergencyContactName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
DefaultEmergencyContactPhone = table.Column<string>(type: "nvarchar(max)", 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_CustomerProfiles", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_CustomerProfiles_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "usr",
|
||||
principalTable: "Users",
|
||||
principalColumn: "UserId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NurseProfiles",
|
||||
schema: "usr",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
UserId = table.Column<int>(type: "int", nullable: false),
|
||||
PartnerCenterId = table.Column<long>(type: "bigint", nullable: true),
|
||||
Bio = table.Column<string>(type: "nvarchar(2000)", maxLength: 2000, nullable: true),
|
||||
YearsOfExperience = table.Column<int>(type: "int", nullable: false),
|
||||
EducationLevel = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
EducationField = table.Column<string>(type: "nvarchar(150)", maxLength: 150, nullable: true),
|
||||
SpecializationsJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsVerified = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
IsAcceptingBookings = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
AverageRating = table.Column<decimal>(type: "decimal(3,2)", precision: 3, scale: 2, nullable: false, defaultValue: 0m),
|
||||
TotalReviews = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
TotalCompletedBookings = table.Column<int>(type: "int", nullable: false, defaultValue: 0),
|
||||
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_NurseProfiles", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_NurseProfiles_Users_UserId",
|
||||
column: x => x.UserId,
|
||||
principalSchema: "usr",
|
||||
principalTable: "Users",
|
||||
principalColumn: "UserId",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Patients",
|
||||
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),
|
||||
DisplayName = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
FirstName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
LastName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
BirthDate = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
Gender = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: false),
|
||||
BloodType = table.Column<string>(type: "nvarchar(10)", maxLength: 10, nullable: true),
|
||||
InitialMedicalNotes = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false, defaultValue: 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_Patients", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_Patients_CustomerProfiles_CustomerId",
|
||||
column: x => x.CustomerId,
|
||||
principalSchema: "usr",
|
||||
principalTable: "CustomerProfiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "NurseBankAccounts",
|
||||
schema: "usr",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
NurseId = table.Column<long>(type: "bigint", nullable: false),
|
||||
BankName = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: true),
|
||||
AccountHolderName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Iban = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
IbanHash = table.Column<string>(type: "nvarchar(64)", maxLength: 64, nullable: false),
|
||||
IsPrimary = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
MatchedNationalId = table.Column<bool>(type: "bit", nullable: true),
|
||||
AccountHolderFromBank = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
OwnershipVendorRef = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: true),
|
||||
IsVerified = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
|
||||
VerifiedByAdminId = table.Column<int>(type: "int", nullable: true),
|
||||
VerifiedAt = 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_NurseBankAccounts", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_NurseBankAccounts_NurseProfiles_NurseId",
|
||||
column: x => x.NurseId,
|
||||
principalSchema: "usr",
|
||||
principalTable: "NurseProfiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_NurseBankAccounts_Users_VerifiedByAdminId",
|
||||
column: x => x.VerifiedByAdminId,
|
||||
principalSchema: "usr",
|
||||
principalTable: "Users",
|
||||
principalColumn: "UserId");
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CustomerProfiles_UserId",
|
||||
schema: "usr",
|
||||
table: "CustomerProfiles",
|
||||
column: "UserId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NurseBankAccounts_IbanHash",
|
||||
schema: "usr",
|
||||
table: "NurseBankAccounts",
|
||||
column: "IbanHash",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NurseBankAccounts_VerifiedByAdminId",
|
||||
schema: "usr",
|
||||
table: "NurseBankAccounts",
|
||||
column: "VerifiedByAdminId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "UX_NurseBankAccounts_NurseId_Primary",
|
||||
schema: "usr",
|
||||
table: "NurseBankAccounts",
|
||||
column: "NurseId",
|
||||
unique: true,
|
||||
filter: "[IsPrimary] = 1");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_NurseProfiles_UserId",
|
||||
schema: "usr",
|
||||
table: "NurseProfiles",
|
||||
column: "UserId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Patients_CustomerId",
|
||||
schema: "usr",
|
||||
table: "Patients",
|
||||
column: "CustomerId");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "NurseBankAccounts",
|
||||
schema: "usr");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Patients",
|
||||
schema: "usr");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "NurseProfiles",
|
||||
schema: "usr");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CustomerProfiles",
|
||||
schema: "usr");
|
||||
}
|
||||
}
|
||||
}
|
||||
+318
@@ -390,6 +390,266 @@ namespace Baya.Infrastructure.Persistence.Migrations
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerProfile", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("CreatedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("DefaultEmergencyContactName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("DefaultEmergencyContactPhone")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTimeOffset?>("ModifiedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("ModifiedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("CustomerProfiles", "usr");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseBankAccount", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("AccountHolderFromBank")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("AccountHolderName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("BankName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("CreatedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Iban")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("IbanHash")
|
||||
.IsRequired()
|
||||
.HasMaxLength(64)
|
||||
.HasColumnType("nvarchar(64)");
|
||||
|
||||
b.Property<bool>("IsPrimary")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("IsVerified")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool?>("MatchedNationalId")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("ModifiedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("ModifiedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("NurseId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("OwnershipVendorRef")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<DateTimeOffset?>("VerifiedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("VerifiedByAdminId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("IbanHash")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("NurseId")
|
||||
.IsUnique()
|
||||
.HasDatabaseName("UX_NurseBankAccounts_NurseId_Primary")
|
||||
.HasFilter("[IsPrimary] = 1");
|
||||
|
||||
b.HasIndex("VerifiedByAdminId");
|
||||
|
||||
b.ToTable("NurseBankAccounts", "usr");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseProfile", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<decimal>("AverageRating")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasPrecision(3, 2)
|
||||
.HasColumnType("decimal(3,2)")
|
||||
.HasDefaultValue(0m);
|
||||
|
||||
b.Property<string>("Bio")
|
||||
.HasMaxLength(2000)
|
||||
.HasColumnType("nvarchar(2000)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("CreatedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("EducationField")
|
||||
.HasMaxLength(150)
|
||||
.HasColumnType("nvarchar(150)");
|
||||
|
||||
b.Property<string>("EducationLevel")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<bool>("IsAcceptingBookings")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<bool>("IsVerified")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(false);
|
||||
|
||||
b.Property<DateTimeOffset?>("ModifiedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("ModifiedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long?>("PartnerCenterId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("SpecializationsJson")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<int>("TotalCompletedBookings")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<int>("TotalReviews")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("int")
|
||||
.HasDefaultValue(0);
|
||||
|
||||
b.Property<int>("UserId")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int>("YearsOfExperience")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("UserId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("NurseProfiles", "usr");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Identity.Patient", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<DateOnly>("BirthDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<string>("BloodType")
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("CreatedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("CustomerId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("DisplayName")
|
||||
.HasMaxLength(200)
|
||||
.HasColumnType("nvarchar(200)");
|
||||
|
||||
b.Property<string>("FirstName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<string>("Gender")
|
||||
.IsRequired()
|
||||
.HasMaxLength(10)
|
||||
.HasColumnType("nvarchar(10)");
|
||||
|
||||
b.Property<string>("InitialMedicalNotes")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bit")
|
||||
.HasDefaultValue(true);
|
||||
|
||||
b.Property<string>("LastName")
|
||||
.HasMaxLength(100)
|
||||
.HasColumnType("nvarchar(100)");
|
||||
|
||||
b.Property<DateTimeOffset?>("ModifiedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("ModifiedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("CustomerId");
|
||||
|
||||
b.ToTable("Patients", "usr");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Notifications.Notification", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -880,6 +1140,54 @@ namespace Baya.Infrastructure.Persistence.Migrations
|
||||
.HasForeignKey("ActorUserId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerProfile", b =>
|
||||
{
|
||||
b.HasOne("Baya.Domain.Entities.User.User", "User")
|
||||
.WithOne()
|
||||
.HasForeignKey("Baya.Domain.Entities.Identity.CustomerProfile", "UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseBankAccount", b =>
|
||||
{
|
||||
b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse")
|
||||
.WithMany("BankAccounts")
|
||||
.HasForeignKey("NurseId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Baya.Domain.Entities.User.User", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("VerifiedByAdminId");
|
||||
|
||||
b.Navigation("Nurse");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseProfile", b =>
|
||||
{
|
||||
b.HasOne("Baya.Domain.Entities.User.User", "User")
|
||||
.WithOne()
|
||||
.HasForeignKey("Baya.Domain.Entities.Identity.NurseProfile", "UserId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Identity.Patient", b =>
|
||||
{
|
||||
b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", "Customer")
|
||||
.WithMany("Patients")
|
||||
.HasForeignKey("CustomerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Customer");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Notifications.Notification", b =>
|
||||
{
|
||||
b.HasOne("Baya.Domain.Entities.User.User", null)
|
||||
@@ -985,6 +1293,16 @@ namespace Baya.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("User");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Identity.CustomerProfile", b =>
|
||||
{
|
||||
b.Navigation("Patients");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Identity.NurseProfile", b =>
|
||||
{
|
||||
b.Navigation("BankAccounts");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.User.Role", b =>
|
||||
{
|
||||
b.Navigation("Claims");
|
||||
|
||||
+10
-2
@@ -9,6 +9,10 @@ public class UnitOfWork : IUnitOfWork
|
||||
public IUserRefreshTokenRepository UserRefreshTokenRepository { get; }
|
||||
public IUserSessionRepository UserSessionRepository { get; }
|
||||
public IUserAccountRepository UserAccountRepository { get; }
|
||||
public INurseProfileRepository NurseProfileRepository { get; }
|
||||
public ICustomerProfileRepository CustomerProfileRepository { get; }
|
||||
public IPatientRepository PatientRepository { get; }
|
||||
public INurseBankAccountRepository NurseBankAccountRepository { get; }
|
||||
|
||||
public UnitOfWork(ApplicationDbContext db)
|
||||
{
|
||||
@@ -16,9 +20,13 @@ public class UnitOfWork : IUnitOfWork
|
||||
UserRefreshTokenRepository = new UserRefreshTokenRepository(_db);
|
||||
UserSessionRepository = new UserSessionRepository(_db);
|
||||
UserAccountRepository = new UserAccountRepository(_db);
|
||||
NurseProfileRepository = new NurseProfileRepository(_db);
|
||||
CustomerProfileRepository = new CustomerProfileRepository(_db);
|
||||
PatientRepository = new PatientRepository(_db);
|
||||
NurseBankAccountRepository = new NurseBankAccountRepository(_db);
|
||||
}
|
||||
|
||||
public Task CommitAsync()
|
||||
public Task CommitAsync()
|
||||
{
|
||||
return _db.SaveChangesAsync();
|
||||
}
|
||||
@@ -28,4 +36,4 @@ public class UnitOfWork : IUnitOfWork
|
||||
_db.ChangeTracker.Clear();
|
||||
return ValueTask.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
using Baya.Application.Contracts.Persistence;
|
||||
using Baya.Application.Models.Identity;
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Baya.Infrastructure.Persistence.Repositories.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Repositories;
|
||||
|
||||
internal sealed class CustomerProfileRepository : BaseAsyncRepository<CustomerProfile>, ICustomerProfileRepository
|
||||
{
|
||||
public CustomerProfileRepository(ApplicationDbContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
|
||||
public Task<CustomerProfile> GetByUserIdAsync(int userId, CancellationToken cancellationToken)
|
||||
=> Table.FirstOrDefaultAsync(c => c.UserId == userId, cancellationToken);
|
||||
|
||||
public Task AddAsync(CustomerProfile profile, CancellationToken cancellationToken)
|
||||
=> base.AddAsync(profile);
|
||||
|
||||
public Task<CustomerProfileDto> GetMineAsync(int userId, CancellationToken cancellationToken)
|
||||
=> TableNoTracking
|
||||
.Where(c => c.UserId == userId)
|
||||
.Select(c => new CustomerProfileDto(c.Id, c.DefaultEmergencyContactName, c.DefaultEmergencyContactPhone))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
public Task<long?> GetProfileIdByUserIdAsync(int userId, CancellationToken cancellationToken)
|
||||
=> TableNoTracking
|
||||
.Where(c => c.UserId == userId)
|
||||
.Select(c => (long?)c.Id)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
using Baya.Application.Common;
|
||||
using Baya.Application.Contracts.Persistence;
|
||||
using Baya.Application.Models.Identity;
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Baya.Infrastructure.Persistence.Repositories.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Repositories;
|
||||
|
||||
internal sealed class NurseBankAccountRepository : BaseAsyncRepository<NurseBankAccount>, INurseBankAccountRepository
|
||||
{
|
||||
public NurseBankAccountRepository(ApplicationDbContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
|
||||
public Task AddAsync(NurseBankAccount account, CancellationToken cancellationToken)
|
||||
=> base.AddAsync(account);
|
||||
|
||||
public Task<NurseBankAccount> GetOwnedAsync(long id, long nurseId, CancellationToken cancellationToken)
|
||||
=> Table.FirstOrDefaultAsync(a => a.Id == id && a.NurseId == nurseId, cancellationToken);
|
||||
|
||||
public async Task SetPrimaryAsync(long nurseId, long accountId, CancellationToken cancellationToken)
|
||||
{
|
||||
// Clear-then-set inside one transaction: two ordered statements so the filtered unique index is
|
||||
// never momentarily violated (setting the new primary while the old one is still primary).
|
||||
await using var transaction = await DbContext.Database.BeginTransactionAsync(cancellationToken);
|
||||
|
||||
await Entities
|
||||
.Where(a => a.NurseId == nurseId && a.IsPrimary)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(a => a.IsPrimary, false), cancellationToken);
|
||||
|
||||
await Entities
|
||||
.Where(a => a.Id == accountId && a.NurseId == nurseId)
|
||||
.ExecuteUpdateAsync(setters => setters.SetProperty(a => a.IsPrimary, true), cancellationToken);
|
||||
|
||||
await transaction.CommitAsync(cancellationToken);
|
||||
}
|
||||
|
||||
public Task<bool> IbanHashExistsAsync(string ibanHash, CancellationToken cancellationToken)
|
||||
=> TableNoTracking.AnyAsync(a => a.IbanHash == ibanHash, cancellationToken);
|
||||
|
||||
public Task<bool> HasAnyAsync(long nurseId, CancellationToken cancellationToken)
|
||||
=> TableNoTracking.AnyAsync(a => a.NurseId == nurseId, cancellationToken);
|
||||
|
||||
public async Task<IReadOnlyList<NurseBankAccountDto>> ListAsync(long nurseId, CancellationToken cancellationToken)
|
||||
{
|
||||
// Decrypt the IBAN in memory, then mask to last-4 — the full value never leaves the repository.
|
||||
var rows = await TableNoTracking
|
||||
.Where(a => a.NurseId == nurseId)
|
||||
.OrderByDescending(a => a.IsPrimary)
|
||||
.ThenByDescending(a => a.Id)
|
||||
.Select(a => new { a.Id, a.BankName, a.Iban, a.IsPrimary, a.IsVerified, a.MatchedNationalId })
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return rows
|
||||
.Select(a => new NurseBankAccountDto(a.Id, a.BankName, Mask.IbanTail(a.Iban), a.IsPrimary, a.IsVerified, a.MatchedNationalId))
|
||||
.ToList();
|
||||
}
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
using Baya.Application.Contracts.Persistence;
|
||||
using Baya.Application.Models.Identity;
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Baya.Infrastructure.Persistence.Repositories.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Repositories;
|
||||
|
||||
internal sealed class NurseProfileRepository : BaseAsyncRepository<NurseProfile>, INurseProfileRepository
|
||||
{
|
||||
public NurseProfileRepository(ApplicationDbContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
|
||||
public Task<NurseProfile> GetByUserIdAsync(int userId, CancellationToken cancellationToken)
|
||||
=> Table.FirstOrDefaultAsync(p => p.UserId == userId, cancellationToken);
|
||||
|
||||
public Task AddAsync(NurseProfile profile, CancellationToken cancellationToken)
|
||||
=> base.AddAsync(profile);
|
||||
|
||||
public Task<NurseProfileDto> GetMineAsync(int userId, CancellationToken cancellationToken)
|
||||
=> TableNoTracking
|
||||
.Where(p => p.UserId == userId)
|
||||
.Select(p => new NurseProfileDto(
|
||||
p.Id,
|
||||
p.Bio,
|
||||
p.YearsOfExperience,
|
||||
p.EducationLevel,
|
||||
p.EducationField,
|
||||
p.SpecializationsJson,
|
||||
p.IsVerified,
|
||||
p.IsAcceptingBookings,
|
||||
p.AverageRating,
|
||||
p.TotalReviews,
|
||||
p.TotalCompletedBookings))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
public Task<long?> GetProfileIdByUserIdAsync(int userId, CancellationToken cancellationToken)
|
||||
=> TableNoTracking
|
||||
.Where(p => p.UserId == userId)
|
||||
.Select(p => (long?)p.Id)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
public Task<NurseIdentityContext> GetIdentityContextByUserIdAsync(int userId, CancellationToken cancellationToken)
|
||||
=> TableNoTracking
|
||||
.Where(p => p.UserId == userId)
|
||||
.Select(p => new NurseIdentityContext(p.Id, p.User.NationalId))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
using Baya.Application.Contracts.Persistence;
|
||||
using Baya.Application.Models.Common;
|
||||
using Baya.Application.Models.Identity;
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Baya.Infrastructure.Persistence.Repositories.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Repositories;
|
||||
|
||||
internal sealed class PatientRepository : BaseAsyncRepository<Patient>, IPatientRepository
|
||||
{
|
||||
public PatientRepository(ApplicationDbContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
|
||||
public Task AddAsync(Patient patient, CancellationToken cancellationToken)
|
||||
=> base.AddAsync(patient);
|
||||
|
||||
public Task<Patient> GetOwnedAsync(long id, long customerId, CancellationToken cancellationToken)
|
||||
=> Table.FirstOrDefaultAsync(p => p.Id == id && p.CustomerId == customerId, cancellationToken);
|
||||
|
||||
public async Task<PagedResult<PatientDto>> ListAsync(long customerId, int page, int pageSize, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = TableNoTracking.Where(p => p.CustomerId == customerId);
|
||||
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
var items = await query
|
||||
.OrderByDescending(p => p.Id)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(p => new PatientDto(
|
||||
p.Id,
|
||||
p.DisplayName,
|
||||
p.FirstName,
|
||||
p.LastName,
|
||||
p.BirthDate,
|
||||
p.Gender,
|
||||
p.BloodType,
|
||||
p.InitialMedicalNotes,
|
||||
p.IsActive))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new PagedResult<PatientDto>(items, total, page, pageSize);
|
||||
}
|
||||
|
||||
public Task<PatientDto> GetOwnedProjectedAsync(long id, long customerId, CancellationToken cancellationToken)
|
||||
=> TableNoTracking
|
||||
.Where(p => p.Id == id && p.CustomerId == customerId)
|
||||
.Select(p => new PatientDto(
|
||||
p.Id,
|
||||
p.DisplayName,
|
||||
p.FirstName,
|
||||
p.LastName,
|
||||
p.BirthDate,
|
||||
p.Gender,
|
||||
p.BloodType,
|
||||
p.InitialMedicalNotes,
|
||||
p.IsActive))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
}
|
||||
Reference in New Issue
Block a user