backend phase 9
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
#nullable enable
|
||||
using Baya.Application.Contracts.Common;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
namespace Baya.Infrastructure.CrossCutting.Seams;
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic mock <see cref="IPaymentCaptureSimulator"/> — the temporary conversion trigger until b10's
|
||||
/// real card capture. It returns a <i>succeeded</i> capture with a stable fake gateway reference and the
|
||||
/// configured PSP fee, so <c>ConvertRequestToBookingCommand</c> can be exercised end-to-end now. Set
|
||||
/// <see cref="PaymentCaptureOptions.ForceFailure"/> to exercise the "capture failed → no booking" path.
|
||||
/// The real capture replaces this registration in b10 — no mock behaviour is baked into any handler.
|
||||
/// </summary>
|
||||
public sealed class MockPaymentCaptureSimulator(IOptions<SeamOptions> options) : IPaymentCaptureSimulator
|
||||
{
|
||||
private readonly PaymentCaptureOptions _options = options.Value.PaymentCapture;
|
||||
|
||||
public ValueTask<PaymentCaptureResult> ConfirmCaptureAsync(long bookingRequestId, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_options.ForceFailure)
|
||||
return ValueTask.FromResult(new PaymentCaptureResult(false, string.Empty, null));
|
||||
|
||||
return ValueTask.FromResult(new PaymentCaptureResult(
|
||||
Succeeded: true,
|
||||
GatewayReference: $"mock-capture-{bookingRequestId}",
|
||||
PspFeeAmount: _options.PspFeeAmount));
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,21 @@ public sealed class SeamOptions
|
||||
public GeocodingOptions Geocoding { get; set; } = new();
|
||||
public ShahkarOptions Shahkar { get; set; } = new();
|
||||
public IdentityKycOptions IdentityKyc { get; set; } = new();
|
||||
public PaymentCaptureOptions PaymentCapture { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tunes the mock <c>IPaymentCaptureSimulator</c> (backend-phase-9). By default every capture succeeds with
|
||||
/// a fake gateway reference and <see cref="PspFeeAmount"/>. Set <see cref="ForceFailure"/> to exercise the
|
||||
/// "capture failed → no booking" path. The real b10 card capture ignores these.
|
||||
/// </summary>
|
||||
public sealed class PaymentCaptureOptions
|
||||
{
|
||||
/// <summary>When true, every capture returns a failed result so conversion refuses to create a booking.</summary>
|
||||
public bool ForceFailure { get; set; }
|
||||
|
||||
/// <summary>The PSP/gateway fee (IRR) the mock reports on a successful capture.</summary>
|
||||
public long? PspFeeAmount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
+5
@@ -43,6 +43,11 @@ public static class ServiceCollectionExtension
|
||||
services.AddSingleton<IIdentityKycProvider, MockIdentityKycProvider>();
|
||||
services.AddSingleton<ICredentialVerifier, MockCredentialVerifier>();
|
||||
|
||||
// Payment-capture trigger (backend-phase-9). The mock returns a deterministic succeeded capture so
|
||||
// ConvertRequestToBooking is testable now; in b10 the real card capture replaces this registration
|
||||
// and calls ConvertRequestToBooking directly on a real payment_transactions.succeeded.
|
||||
services.AddSingleton<IPaymentCaptureSimulator, MockPaymentCaptureSimulator>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Reflection;
|
||||
using Baya.Application.Contracts.Common;
|
||||
using Baya.Domain.Common;
|
||||
using Baya.Domain.Entities.Booking;
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Baya.Domain.Entities.User;
|
||||
using Baya.Domain.Entities.Verification;
|
||||
@@ -139,5 +140,22 @@ public class ApplicationDbContext: IdentityDbContext<User, Role, int, UserClaim,
|
||||
{
|
||||
builder.Property(c => c.CredentialNumber).HasConversion(encrypted);
|
||||
});
|
||||
|
||||
// b9 snapshot + clinical PII: the frozen address snapshot and every booking_care_instructions field
|
||||
// are encrypted at rest through the same seam. The care fields are the stage-2 clinical disclosure —
|
||||
// decrypted only in the gated care-instructions read (assigned nurse + admin, post-confirmation).
|
||||
modelBuilder.Entity<Booking>(builder =>
|
||||
{
|
||||
builder.Property(b => b.AddressSnapshotJson).HasConversion(encrypted);
|
||||
});
|
||||
modelBuilder.Entity<BookingCareInstruction>(builder =>
|
||||
{
|
||||
builder.Property(c => c.CurrentConditions).HasConversion(encrypted);
|
||||
builder.Property(c => c.Medications).HasConversion(encrypted);
|
||||
builder.Property(c => c.Allergies).HasConversion(encrypted);
|
||||
builder.Property(c => c.SpecialInstructions).HasConversion(encrypted);
|
||||
builder.Property(c => c.EmergencyContactName).HasConversion(encrypted);
|
||||
builder.Property(c => c.EmergencyContactPhone).HasConversion(encrypted);
|
||||
});
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
using Baya.Domain.Entities.Booking;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.BookingConfig;
|
||||
|
||||
internal sealed class BookingCareInstructionConfig : IEntityTypeConfiguration<BookingCareInstruction>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BookingCareInstruction> builder)
|
||||
{
|
||||
builder.ToTable("BookingCareInstructions", "booking");
|
||||
|
||||
// All clinical fields are encrypted at rest (converters wired in ApplicationDbContext) and are
|
||||
// nvarchar(max); shorter contact fields still ride the same seam, so no length cap here.
|
||||
|
||||
// 1:1 with the booking (UNIQUE index created automatically).
|
||||
builder.HasOne(c => c.Booking)
|
||||
.WithOne(b => b.CareInstructions)
|
||||
.HasForeignKey<BookingCareInstruction>(c => c.BookingId)
|
||||
.IsRequired();
|
||||
|
||||
builder.HasQueryFilter(c => c.DeletedAt == null);
|
||||
}
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
using Baya.Domain.Entities.Booking;
|
||||
using Baya.Domain.Entities.Catalog;
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.BookingConfig;
|
||||
|
||||
internal sealed class BookingConfig : IEntityTypeConfiguration<Booking>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<Booking> builder)
|
||||
{
|
||||
// The three-amount split is a DB CHECK (not handler-only): gross = commission + payout, all ≥ 0.
|
||||
builder.ToTable("Bookings", "booking", t => t.HasCheckConstraint(
|
||||
"CK_Bookings_AmountSplit",
|
||||
"[GrossPriceIrr] = [BalinyaarCommissionIrr] + [NursePayoutAmount] " +
|
||||
"AND [GrossPriceIrr] >= 0 AND [BalinyaarCommissionIrr] >= 0 AND [NursePayoutAmount] >= 0"));
|
||||
|
||||
// Snapshots freeze history. AddressSnapshotJson is encrypted at rest (converter wired in
|
||||
// ApplicationDbContext); both are nvarchar(max) so no length cap.
|
||||
builder.Property(b => b.VariantSnapshotJson).IsRequired();
|
||||
builder.Property(b => b.AddressSnapshotJson).IsRequired();
|
||||
|
||||
builder.Property(b => b.PlatformFeeRate).HasPrecision(5, 4);
|
||||
builder.Property(b => b.CancellationRefundPercentage).HasPrecision(5, 2);
|
||||
|
||||
builder.Property(b => b.Status).HasMaxLength(30).IsRequired();
|
||||
builder.Property(b => b.CancellationReason).HasMaxLength(500);
|
||||
builder.Property(b => b.CancelledBy).HasMaxLength(20);
|
||||
builder.Property(b => b.CancellationPolicyCode).HasMaxLength(50);
|
||||
|
||||
builder.HasIndex(b => new { b.CustomerId, b.Status });
|
||||
builder.HasIndex(b => new { b.NurseId, b.Status });
|
||||
// b13 selects payout-eligible bookings through the dispute-window close.
|
||||
builder.HasIndex(b => b.DisputeWindowEndsAt);
|
||||
|
||||
// 1:1 with the request that created it (UNIQUE index created automatically). No navigation either
|
||||
// side — the request is read by id at conversion time.
|
||||
builder.HasOne<BookingRequest>()
|
||||
.WithOne()
|
||||
.HasForeignKey<Booking>(b => b.BookingRequestId)
|
||||
.IsRequired();
|
||||
|
||||
// Denormalized FKs for query performance (no navigations on Booking). partner_center_id is left
|
||||
// FK-less and nullable — partner_centers is DEFERRED to b15.
|
||||
builder.HasOne<CustomerProfile>().WithMany().HasForeignKey(b => b.CustomerId).IsRequired();
|
||||
builder.HasOne<NurseProfile>().WithMany().HasForeignKey(b => b.NurseId).IsRequired();
|
||||
builder.HasOne<Patient>().WithMany().HasForeignKey(b => b.PatientId).IsRequired();
|
||||
builder.HasOne<NurseServiceVariant>().WithMany().HasForeignKey(b => b.VariantId).IsRequired();
|
||||
builder.HasOne<CustomerAddress>().WithMany().HasForeignKey(b => b.CustomerAddressId).IsRequired();
|
||||
|
||||
builder.HasMany(b => b.Sessions).WithOne(s => s.Booking).HasForeignKey(s => s.BookingId);
|
||||
|
||||
builder.HasQueryFilter(b => b.DeletedAt == null);
|
||||
}
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
using Baya.Domain.Entities.Booking;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.BookingConfig;
|
||||
|
||||
internal sealed class BookingSessionConfig : IEntityTypeConfiguration<BookingSession>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<BookingSession> builder)
|
||||
{
|
||||
builder.ToTable("BookingSessions", "booking");
|
||||
|
||||
builder.Property(s => s.Status).HasMaxLength(20).IsRequired();
|
||||
|
||||
// The nurse's "today" view and the no-show sweep both read (status, scheduled_date).
|
||||
builder.HasIndex(s => new { s.BookingId, s.SessionIndex });
|
||||
builder.HasIndex(s => new { s.Status, s.ScheduledDate });
|
||||
|
||||
// 1:1 with its EVV record (the FK is on the verification side, on the session).
|
||||
builder.HasOne(s => s.Verification)
|
||||
.WithOne(v => v.Session)
|
||||
.HasForeignKey<VisitVerification>(v => v.BookingSessionId);
|
||||
|
||||
builder.HasQueryFilter(s => s.DeletedAt == null);
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
using Baya.Domain.Entities.Booking;
|
||||
using Baya.Infrastructure.Persistence.Configuration;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.BookingConfig;
|
||||
|
||||
internal sealed class CancellationPolicyConfig : IEntityTypeConfiguration<CancellationPolicy>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<CancellationPolicy> builder)
|
||||
{
|
||||
builder.ToTable("CancellationPolicies", "booking");
|
||||
|
||||
builder.Property(p => p.Code).HasMaxLength(50).IsRequired();
|
||||
builder.Property(p => p.AppliesTo).HasMaxLength(20).IsRequired();
|
||||
builder.Property(p => p.RefundPercentage).HasPrecision(5, 2);
|
||||
builder.Property(p => p.FeeRate).HasPrecision(5, 4);
|
||||
|
||||
builder.HasIndex(p => p.Code).IsUnique();
|
||||
builder.HasIndex(p => new { p.AppliesTo, p.IsActive });
|
||||
|
||||
builder.HasQueryFilter(p => p.DeletedAt == null);
|
||||
|
||||
builder.HasData(Seed());
|
||||
}
|
||||
|
||||
// Baseline tiers, seeded via HasData so they land with this phase's migration on a fresh DB. Numbers the
|
||||
// product doc leaves open (the nurse penalty) default safe and are flagged in the phase report — the
|
||||
// nurse-no-show penalty is modelled (FeeRate) but posting it is deferred to payouts (b13).
|
||||
private static object[] Seed()
|
||||
{
|
||||
var ts = SeedConstants.Timestamp;
|
||||
|
||||
(long Id, string Code, string AppliesTo, int? Min, int? Max, decimal Refund, long Fee, decimal? Rate)[] rows =
|
||||
[
|
||||
(1, CancellationPolicyCode.Standard24h, CancellationActor.Customer, 24, null, 100m, 0L, null),
|
||||
// Open lower bound so a cancel < 24h before start (or after it) always resolves a customer tier.
|
||||
(2, CancellationPolicyCode.StandardInside24h, CancellationActor.Customer, null, 24, 50m, 0L, null),
|
||||
(3, CancellationPolicyCode.NurseNoShow, CancellationActor.Nurse, null, null, 100m, 0L, 0m),
|
||||
(4, CancellationPolicyCode.AdminCancellation, CancellationActor.Admin, null, null, 100m, 0L, null),
|
||||
];
|
||||
|
||||
return rows
|
||||
.Select(r => (object)new
|
||||
{
|
||||
r.Id,
|
||||
r.Code,
|
||||
r.AppliesTo,
|
||||
HoursBeforeStartMin = r.Min,
|
||||
HoursBeforeStartMax = r.Max,
|
||||
RefundPercentage = r.Refund,
|
||||
FeeAmountIrr = r.Fee,
|
||||
FeeRate = r.Rate,
|
||||
IsActive = true,
|
||||
CreatedAt = ts
|
||||
})
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
using Baya.Domain.Entities.Booking;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Configuration.BookingConfig;
|
||||
|
||||
internal sealed class VisitVerificationConfig : IEntityTypeConfiguration<VisitVerification>
|
||||
{
|
||||
public void Configure(EntityTypeBuilder<VisitVerification> builder)
|
||||
{
|
||||
builder.ToTable("VisitVerifications", "booking");
|
||||
|
||||
builder.Property(v => v.Status).HasMaxLength(20).IsRequired();
|
||||
|
||||
builder.Property(v => v.CheckInLat).HasPrecision(9, 6);
|
||||
builder.Property(v => v.CheckInLng).HasPrecision(9, 6);
|
||||
builder.Property(v => v.CheckOutLat).HasPrecision(9, 6);
|
||||
builder.Property(v => v.CheckOutLng).HasPrecision(9, 6);
|
||||
builder.Property(v => v.CheckInDistanceMeters).HasPrecision(10, 2);
|
||||
|
||||
// The FK/1:1 to the session is configured on BookingSessionConfig; the UNIQUE index on
|
||||
// BookingSessionId is created automatically. The admin mismatch queue reads on the advisory flag.
|
||||
builder.HasIndex(v => v.CheckInAddressMatch);
|
||||
|
||||
builder.HasQueryFilter(v => v.DeletedAt == null);
|
||||
}
|
||||
}
|
||||
+2
@@ -44,6 +44,8 @@ internal sealed class PlatformConfigConfig : IEntityTypeConfiguration<PlatformCo
|
||||
(14, "auth_otp_max_attempts", "5", ConfigDataType.Int, "Wrong-code attempts allowed before OTP verification is refused until a fresh code."),
|
||||
(15, "auth_session_ttl_days", "30", ConfigDataType.Int, "Refresh-token session lifetime in days."),
|
||||
(16, "verification_expiry_scan_cadence_hours", "24", ConfigDataType.Int, "Hours between credential-expiry scans (the scheduled cron is deferred; the scan is admin-triggered today)."),
|
||||
(17, "no_show_threshold_minutes", "60", ConfigDataType.Int, "Minutes after a session's scheduled start with no EVV check-in before it is flagged a no-show."),
|
||||
(18, "no_show_scan_cadence_hours", "1", ConfigDataType.Int, "Hours between no-show sweeps (the scheduled cron is deferred; the sweep is admin-triggered today)."),
|
||||
];
|
||||
|
||||
return rows
|
||||
|
||||
+4192
File diff suppressed because it is too large
Load Diff
+379
@@ -0,0 +1,379 @@
|
||||
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 BookingsSessionsEvvCancellation : Migration
|
||||
{
|
||||
/// <inheritdoc />
|
||||
protected override void Up(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.CreateTable(
|
||||
name: "Bookings",
|
||||
schema: "booking",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
BookingRequestId = table.Column<long>(type: "bigint", nullable: false),
|
||||
CustomerId = table.Column<long>(type: "bigint", nullable: false),
|
||||
NurseId = table.Column<long>(type: "bigint", nullable: false),
|
||||
PatientId = table.Column<long>(type: "bigint", nullable: false),
|
||||
VariantId = table.Column<long>(type: "bigint", nullable: false),
|
||||
CustomerAddressId = table.Column<long>(type: "bigint", nullable: false),
|
||||
PartnerCenterId = table.Column<long>(type: "bigint", nullable: true),
|
||||
VariantSnapshotJson = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
AddressSnapshotJson = table.Column<string>(type: "nvarchar(max)", nullable: false),
|
||||
GrossPriceIrr = table.Column<long>(type: "bigint", nullable: false),
|
||||
BalinyaarCommissionIrr = table.Column<long>(type: "bigint", nullable: false),
|
||||
PlatformFeeRate = table.Column<decimal>(type: "decimal(5,4)", precision: 5, scale: 4, nullable: false),
|
||||
NursePayoutAmount = table.Column<long>(type: "bigint", nullable: false),
|
||||
PspFeeAmount = table.Column<long>(type: "bigint", nullable: true),
|
||||
SessionCount = table.Column<short>(type: "smallint", nullable: false),
|
||||
ScheduledDate = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
ScheduledTimeStart = table.Column<TimeOnly>(type: "time", nullable: false),
|
||||
ScheduledTimeEnd = table.Column<TimeOnly>(type: "time", nullable: false),
|
||||
Status = table.Column<string>(type: "nvarchar(30)", maxLength: 30, nullable: false),
|
||||
ConfirmedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
CancelledAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
CancellationReason = table.Column<string>(type: "nvarchar(500)", maxLength: 500, nullable: true),
|
||||
CancelledBy = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: true),
|
||||
CancellationPolicyCode = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: true),
|
||||
CancellationRefundPercentage = table.Column<decimal>(type: "decimal(5,2)", precision: 5, scale: 2, nullable: true),
|
||||
RefundableAmountIrr = table.Column<long>(type: "bigint", nullable: true),
|
||||
CompletedAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
DisputeWindowEndsAt = table.Column<DateTime>(type: "datetime2", 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_Bookings", x => x.Id);
|
||||
table.CheckConstraint("CK_Bookings_AmountSplit", "[GrossPriceIrr] = [BalinyaarCommissionIrr] + [NursePayoutAmount] AND [GrossPriceIrr] >= 0 AND [BalinyaarCommissionIrr] >= 0 AND [NursePayoutAmount] >= 0");
|
||||
table.ForeignKey(
|
||||
name: "FK_Bookings_BookingRequests_BookingRequestId",
|
||||
column: x => x.BookingRequestId,
|
||||
principalSchema: "booking",
|
||||
principalTable: "BookingRequests",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_Bookings_CustomerAddresses_CustomerAddressId",
|
||||
column: x => x.CustomerAddressId,
|
||||
principalSchema: "usr",
|
||||
principalTable: "CustomerAddresses",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_Bookings_CustomerProfiles_CustomerId",
|
||||
column: x => x.CustomerId,
|
||||
principalSchema: "usr",
|
||||
principalTable: "CustomerProfiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_Bookings_NurseProfiles_NurseId",
|
||||
column: x => x.NurseId,
|
||||
principalSchema: "usr",
|
||||
principalTable: "NurseProfiles",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_Bookings_NurseServiceVariants_VariantId",
|
||||
column: x => x.VariantId,
|
||||
principalSchema: "catalog",
|
||||
principalTable: "NurseServiceVariants",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
table.ForeignKey(
|
||||
name: "FK_Bookings_Patients_PatientId",
|
||||
column: x => x.PatientId,
|
||||
principalSchema: "usr",
|
||||
principalTable: "Patients",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "CancellationPolicies",
|
||||
schema: "booking",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
Code = table.Column<string>(type: "nvarchar(50)", maxLength: 50, nullable: false),
|
||||
AppliesTo = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
|
||||
HoursBeforeStartMin = table.Column<int>(type: "int", nullable: true),
|
||||
HoursBeforeStartMax = table.Column<int>(type: "int", nullable: true),
|
||||
RefundPercentage = table.Column<decimal>(type: "decimal(5,2)", precision: 5, scale: 2, nullable: false),
|
||||
FeeAmountIrr = table.Column<long>(type: "bigint", nullable: false),
|
||||
FeeRate = table.Column<decimal>(type: "decimal(5,4)", precision: 5, scale: 4, nullable: true),
|
||||
IsActive = table.Column<bool>(type: "bit", nullable: false),
|
||||
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_CancellationPolicies", x => x.Id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BookingCareInstructions",
|
||||
schema: "booking",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
BookingId = table.Column<long>(type: "bigint", nullable: false),
|
||||
CurrentConditions = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Medications = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
Allergies = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
SpecialInstructions = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
EmergencyContactName = table.Column<string>(type: "nvarchar(max)", nullable: true),
|
||||
EmergencyContactPhone = 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_BookingCareInstructions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_BookingCareInstructions_Bookings_BookingId",
|
||||
column: x => x.BookingId,
|
||||
principalSchema: "booking",
|
||||
principalTable: "Bookings",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "BookingSessions",
|
||||
schema: "booking",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
BookingId = table.Column<long>(type: "bigint", nullable: false),
|
||||
SessionIndex = table.Column<int>(type: "int", nullable: false),
|
||||
ScheduledDate = table.Column<DateOnly>(type: "date", nullable: false),
|
||||
ScheduledTimeStart = table.Column<TimeOnly>(type: "time", nullable: false),
|
||||
ScheduledTimeEnd = table.Column<TimeOnly>(type: "time", nullable: false),
|
||||
VisitPayoutAmount = table.Column<long>(type: "bigint", nullable: false),
|
||||
Status = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
|
||||
PayoutEligibleAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
CancellationEventId = table.Column<long>(type: "bigint", 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_BookingSessions", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_BookingSessions_Bookings_BookingId",
|
||||
column: x => x.BookingId,
|
||||
principalSchema: "booking",
|
||||
principalTable: "Bookings",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "VisitVerifications",
|
||||
schema: "booking",
|
||||
columns: table => new
|
||||
{
|
||||
Id = table.Column<long>(type: "bigint", nullable: false)
|
||||
.Annotation("SqlServer:Identity", "1, 1"),
|
||||
BookingSessionId = table.Column<long>(type: "bigint", nullable: false),
|
||||
CheckInAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
CheckInLat = table.Column<decimal>(type: "decimal(9,6)", precision: 9, scale: 6, nullable: true),
|
||||
CheckInLng = table.Column<decimal>(type: "decimal(9,6)", precision: 9, scale: 6, nullable: true),
|
||||
CheckOutAt = table.Column<DateTime>(type: "datetime2", nullable: true),
|
||||
CheckOutLat = table.Column<decimal>(type: "decimal(9,6)", precision: 9, scale: 6, nullable: true),
|
||||
CheckOutLng = table.Column<decimal>(type: "decimal(9,6)", precision: 9, scale: 6, nullable: true),
|
||||
CheckInAddressMatch = table.Column<bool>(type: "bit", nullable: true),
|
||||
CheckInDistanceMeters = table.Column<decimal>(type: "decimal(10,2)", precision: 10, scale: 2, nullable: true),
|
||||
Status = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
|
||||
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_VisitVerifications", x => x.Id);
|
||||
table.ForeignKey(
|
||||
name: "FK_VisitVerifications_BookingSessions_BookingSessionId",
|
||||
column: x => x.BookingSessionId,
|
||||
principalSchema: "booking",
|
||||
principalTable: "BookingSessions",
|
||||
principalColumn: "Id",
|
||||
onDelete: ReferentialAction.Restrict);
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
schema: "booking",
|
||||
table: "CancellationPolicies",
|
||||
columns: new[] { "Id", "AppliesTo", "Code", "CreatedAt", "CreatedById", "DeletedAt", "FeeAmountIrr", "FeeRate", "HoursBeforeStartMax", "HoursBeforeStartMin", "IsActive", "ModifiedAt", "ModifiedById", "RefundPercentage" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 1L, "customer", "standard_24h", new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, 0L, null, null, 24, true, null, null, 100m },
|
||||
{ 2L, "customer", "standard_inside_24h", new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, 0L, null, 24, null, true, null, null, 50m },
|
||||
{ 3L, "nurse", "nurse_no_show", new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, 0L, 0m, null, null, true, null, null, 100m },
|
||||
{ 4L, "admin", "admin_cancellation", new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, null, 0L, null, null, null, true, null, null, 100m }
|
||||
});
|
||||
|
||||
migrationBuilder.InsertData(
|
||||
schema: "ops",
|
||||
table: "PlatformConfigs",
|
||||
columns: new[] { "Id", "CreatedAt", "CreatedById", "DataType", "Description", "Key", "ModifiedAt", "ModifiedById", "Value" },
|
||||
values: new object[,]
|
||||
{
|
||||
{ 17L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "int", "Minutes after a session's scheduled start with no EVV check-in before it is flagged a no-show.", "no_show_threshold_minutes", null, null, "60" },
|
||||
{ 18L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "int", "Hours between no-show sweeps (the scheduled cron is deferred; the sweep is admin-triggered today).", "no_show_scan_cadence_hours", null, null, "1" }
|
||||
});
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookingCareInstructions_BookingId",
|
||||
schema: "booking",
|
||||
table: "BookingCareInstructions",
|
||||
column: "BookingId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Bookings_BookingRequestId",
|
||||
schema: "booking",
|
||||
table: "Bookings",
|
||||
column: "BookingRequestId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Bookings_CustomerAddressId",
|
||||
schema: "booking",
|
||||
table: "Bookings",
|
||||
column: "CustomerAddressId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Bookings_CustomerId_Status",
|
||||
schema: "booking",
|
||||
table: "Bookings",
|
||||
columns: new[] { "CustomerId", "Status" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Bookings_DisputeWindowEndsAt",
|
||||
schema: "booking",
|
||||
table: "Bookings",
|
||||
column: "DisputeWindowEndsAt");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Bookings_NurseId_Status",
|
||||
schema: "booking",
|
||||
table: "Bookings",
|
||||
columns: new[] { "NurseId", "Status" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Bookings_PatientId",
|
||||
schema: "booking",
|
||||
table: "Bookings",
|
||||
column: "PatientId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_Bookings_VariantId",
|
||||
schema: "booking",
|
||||
table: "Bookings",
|
||||
column: "VariantId");
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookingSessions_BookingId_SessionIndex",
|
||||
schema: "booking",
|
||||
table: "BookingSessions",
|
||||
columns: new[] { "BookingId", "SessionIndex" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_BookingSessions_Status_ScheduledDate",
|
||||
schema: "booking",
|
||||
table: "BookingSessions",
|
||||
columns: new[] { "Status", "ScheduledDate" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CancellationPolicies_AppliesTo_IsActive",
|
||||
schema: "booking",
|
||||
table: "CancellationPolicies",
|
||||
columns: new[] { "AppliesTo", "IsActive" });
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_CancellationPolicies_Code",
|
||||
schema: "booking",
|
||||
table: "CancellationPolicies",
|
||||
column: "Code",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VisitVerifications_BookingSessionId",
|
||||
schema: "booking",
|
||||
table: "VisitVerifications",
|
||||
column: "BookingSessionId",
|
||||
unique: true);
|
||||
|
||||
migrationBuilder.CreateIndex(
|
||||
name: "IX_VisitVerifications_CheckInAddressMatch",
|
||||
schema: "booking",
|
||||
table: "VisitVerifications",
|
||||
column: "CheckInAddressMatch");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void Down(MigrationBuilder migrationBuilder)
|
||||
{
|
||||
migrationBuilder.DropTable(
|
||||
name: "BookingCareInstructions",
|
||||
schema: "booking");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "CancellationPolicies",
|
||||
schema: "booking");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "VisitVerifications",
|
||||
schema: "booking");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "BookingSessions",
|
||||
schema: "booking");
|
||||
|
||||
migrationBuilder.DropTable(
|
||||
name: "Bookings",
|
||||
schema: "booking");
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
schema: "ops",
|
||||
table: "PlatformConfigs",
|
||||
keyColumn: "Id",
|
||||
keyValue: 17L);
|
||||
|
||||
migrationBuilder.DeleteData(
|
||||
schema: "ops",
|
||||
table: "PlatformConfigs",
|
||||
keyColumn: "Id",
|
||||
keyValue: 18L);
|
||||
}
|
||||
}
|
||||
}
|
||||
+532
@@ -98,6 +98,197 @@ namespace Baya.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("AuditLogs", "ops");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Booking.Booking", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("AddressSnapshotJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long>("BalinyaarCommissionIrr")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("BookingRequestId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("CancellationPolicyCode")
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<string>("CancellationReason")
|
||||
.HasMaxLength(500)
|
||||
.HasColumnType("nvarchar(500)");
|
||||
|
||||
b.Property<decimal?>("CancellationRefundPercentage")
|
||||
.HasPrecision(5, 2)
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
b.Property<DateTime?>("CancelledAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<string>("CancelledBy")
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<DateTime?>("CompletedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTime?>("ConfirmedAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("CreatedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("CustomerAddressId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("CustomerId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<DateTime?>("DisputeWindowEndsAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<long>("GrossPriceIrr")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTimeOffset?>("ModifiedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("ModifiedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<long>("NurseId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("NursePayoutAmount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long?>("PartnerCenterId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long>("PatientId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal>("PlatformFeeRate")
|
||||
.HasPrecision(5, 4)
|
||||
.HasColumnType("decimal(5,4)");
|
||||
|
||||
b.Property<long?>("PspFeeAmount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long?>("RefundableAmountIrr")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateOnly>("ScheduledDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<TimeOnly>("ScheduledTimeEnd")
|
||||
.HasColumnType("time");
|
||||
|
||||
b.Property<TimeOnly>("ScheduledTimeStart")
|
||||
.HasColumnType("time");
|
||||
|
||||
b.Property<short>("SessionCount")
|
||||
.HasColumnType("smallint");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(30)
|
||||
.HasColumnType("nvarchar(30)");
|
||||
|
||||
b.Property<long>("VariantId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<string>("VariantSnapshotJson")
|
||||
.IsRequired()
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BookingRequestId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("CustomerAddressId");
|
||||
|
||||
b.HasIndex("DisputeWindowEndsAt");
|
||||
|
||||
b.HasIndex("PatientId");
|
||||
|
||||
b.HasIndex("VariantId");
|
||||
|
||||
b.HasIndex("CustomerId", "Status");
|
||||
|
||||
b.HasIndex("NurseId", "Status");
|
||||
|
||||
b.ToTable("Bookings", "booking", t =>
|
||||
{
|
||||
t.HasCheckConstraint("CK_Bookings_AmountSplit", "[GrossPriceIrr] = [BalinyaarCommissionIrr] + [NursePayoutAmount] AND [GrossPriceIrr] >= 0 AND [BalinyaarCommissionIrr] >= 0 AND [NursePayoutAmount] >= 0");
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingCareInstruction", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("Allergies")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<long>("BookingId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("CreatedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("CurrentConditions")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<string>("EmergencyContactName")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("EmergencyContactPhone")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<string>("Medications")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.Property<DateTimeOffset?>("ModifiedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("ModifiedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("SpecialInstructions")
|
||||
.HasColumnType("nvarchar(max)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BookingId")
|
||||
.IsUnique();
|
||||
|
||||
b.ToTable("BookingCareInstructions", "booking");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingRequest", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -187,6 +378,245 @@ namespace Baya.Infrastructure.Persistence.Migrations
|
||||
b.ToTable("BookingRequests", "booking");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingSession", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("BookingId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<long?>("CancellationEventId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("CreatedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<DateTimeOffset?>("ModifiedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("ModifiedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTime?>("PayoutEligibleAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<DateOnly>("ScheduledDate")
|
||||
.HasColumnType("date");
|
||||
|
||||
b.Property<TimeOnly>("ScheduledTimeEnd")
|
||||
.HasColumnType("time");
|
||||
|
||||
b.Property<TimeOnly>("ScheduledTimeStart")
|
||||
.HasColumnType("time");
|
||||
|
||||
b.Property<int>("SessionIndex")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<long>("VisitPayoutAmount")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BookingId", "SessionIndex");
|
||||
|
||||
b.HasIndex("Status", "ScheduledDate");
|
||||
|
||||
b.ToTable("BookingSessions", "booking");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Booking.CancellationPolicy", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<string>("AppliesTo")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.Property<string>("Code")
|
||||
.IsRequired()
|
||||
.HasMaxLength(50)
|
||||
.HasColumnType("nvarchar(50)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("CreatedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<long>("FeeAmountIrr")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<decimal?>("FeeRate")
|
||||
.HasPrecision(5, 4)
|
||||
.HasColumnType("decimal(5,4)");
|
||||
|
||||
b.Property<int?>("HoursBeforeStartMax")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<int?>("HoursBeforeStartMin")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<bool>("IsActive")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTimeOffset?>("ModifiedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("ModifiedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<decimal>("RefundPercentage")
|
||||
.HasPrecision(5, 2)
|
||||
.HasColumnType("decimal(5,2)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("Code")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("AppliesTo", "IsActive");
|
||||
|
||||
b.ToTable("CancellationPolicies", "booking");
|
||||
|
||||
b.HasData(
|
||||
new
|
||||
{
|
||||
Id = 1L,
|
||||
AppliesTo = "customer",
|
||||
Code = "standard_24h",
|
||||
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
FeeAmountIrr = 0L,
|
||||
HoursBeforeStartMin = 24,
|
||||
IsActive = true,
|
||||
RefundPercentage = 100m
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 2L,
|
||||
AppliesTo = "customer",
|
||||
Code = "standard_inside_24h",
|
||||
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
FeeAmountIrr = 0L,
|
||||
HoursBeforeStartMax = 24,
|
||||
IsActive = true,
|
||||
RefundPercentage = 50m
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 3L,
|
||||
AppliesTo = "nurse",
|
||||
Code = "nurse_no_show",
|
||||
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
FeeAmountIrr = 0L,
|
||||
FeeRate = 0m,
|
||||
IsActive = true,
|
||||
RefundPercentage = 100m
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 4L,
|
||||
AppliesTo = "admin",
|
||||
Code = "admin_cancellation",
|
||||
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
FeeAmountIrr = 0L,
|
||||
IsActive = true,
|
||||
RefundPercentage = 100m
|
||||
});
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Booking.VisitVerification", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnType("bigint");
|
||||
|
||||
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
|
||||
|
||||
b.Property<long>("BookingSessionId")
|
||||
.HasColumnType("bigint");
|
||||
|
||||
b.Property<bool?>("CheckInAddressMatch")
|
||||
.HasColumnType("bit");
|
||||
|
||||
b.Property<DateTime?>("CheckInAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal?>("CheckInDistanceMeters")
|
||||
.HasPrecision(10, 2)
|
||||
.HasColumnType("decimal(10,2)");
|
||||
|
||||
b.Property<decimal?>("CheckInLat")
|
||||
.HasPrecision(9, 6)
|
||||
.HasColumnType("decimal(9,6)");
|
||||
|
||||
b.Property<decimal?>("CheckInLng")
|
||||
.HasPrecision(9, 6)
|
||||
.HasColumnType("decimal(9,6)");
|
||||
|
||||
b.Property<DateTime?>("CheckOutAt")
|
||||
.HasColumnType("datetime2");
|
||||
|
||||
b.Property<decimal?>("CheckOutLat")
|
||||
.HasPrecision(9, 6)
|
||||
.HasColumnType("decimal(9,6)");
|
||||
|
||||
b.Property<decimal?>("CheckOutLng")
|
||||
.HasPrecision(9, 6)
|
||||
.HasColumnType("decimal(9,6)");
|
||||
|
||||
b.Property<DateTimeOffset>("CreatedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("CreatedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<DateTimeOffset?>("DeletedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<DateTimeOffset?>("ModifiedAt")
|
||||
.HasColumnType("datetimeoffset");
|
||||
|
||||
b.Property<int?>("ModifiedById")
|
||||
.HasColumnType("int");
|
||||
|
||||
b.Property<string>("Status")
|
||||
.IsRequired()
|
||||
.HasMaxLength(20)
|
||||
.HasColumnType("nvarchar(20)");
|
||||
|
||||
b.HasKey("Id");
|
||||
|
||||
b.HasIndex("BookingSessionId")
|
||||
.IsUnique();
|
||||
|
||||
b.HasIndex("CheckInAddressMatch");
|
||||
|
||||
b.ToTable("VisitVerifications", "booking");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariant", b =>
|
||||
{
|
||||
b.Property<long>("Id")
|
||||
@@ -707,6 +1137,24 @@ namespace Baya.Infrastructure.Persistence.Migrations
|
||||
Description = "Hours between credential-expiry scans (the scheduled cron is deferred; the scan is admin-triggered today).",
|
||||
Key = "verification_expiry_scan_cadence_hours",
|
||||
Value = "24"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 17L,
|
||||
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
DataType = "int",
|
||||
Description = "Minutes after a session's scheduled start with no EVV check-in before it is flagged a no-show.",
|
||||
Key = "no_show_threshold_minutes",
|
||||
Value = "60"
|
||||
},
|
||||
new
|
||||
{
|
||||
Id = 18L,
|
||||
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
|
||||
DataType = "int",
|
||||
Description = "Hours between no-show sweeps (the scheduled cron is deferred; the sweep is admin-triggered today).",
|
||||
Key = "no_show_scan_cadence_hours",
|
||||
Value = "1"
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3165,6 +3613,56 @@ namespace Baya.Infrastructure.Persistence.Migrations
|
||||
.HasForeignKey("ActorUserId");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Booking.Booking", b =>
|
||||
{
|
||||
b.HasOne("Baya.Domain.Entities.Booking.BookingRequest", null)
|
||||
.WithOne()
|
||||
.HasForeignKey("Baya.Domain.Entities.Booking.Booking", "BookingRequestId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Baya.Domain.Entities.Identity.CustomerAddress", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("CustomerAddressId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Baya.Domain.Entities.Identity.CustomerProfile", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("CustomerId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("NurseId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Baya.Domain.Entities.Identity.Patient", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("PatientId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.HasOne("Baya.Domain.Entities.Catalog.NurseServiceVariant", null)
|
||||
.WithMany()
|
||||
.HasForeignKey("VariantId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingCareInstruction", b =>
|
||||
{
|
||||
b.HasOne("Baya.Domain.Entities.Booking.Booking", "Booking")
|
||||
.WithOne("CareInstructions")
|
||||
.HasForeignKey("Baya.Domain.Entities.Booking.BookingCareInstruction", "BookingId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Booking");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingRequest", b =>
|
||||
{
|
||||
b.HasOne("Baya.Domain.Entities.Identity.CustomerAddress", "CustomerAddress")
|
||||
@@ -3208,6 +3706,28 @@ namespace Baya.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("Variant");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingSession", b =>
|
||||
{
|
||||
b.HasOne("Baya.Domain.Entities.Booking.Booking", "Booking")
|
||||
.WithMany("Sessions")
|
||||
.HasForeignKey("BookingId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Booking");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Booking.VisitVerification", b =>
|
||||
{
|
||||
b.HasOne("Baya.Domain.Entities.Booking.BookingSession", "Session")
|
||||
.WithOne("Verification")
|
||||
.HasForeignKey("Baya.Domain.Entities.Booking.VisitVerification", "BookingSessionId")
|
||||
.OnDelete(DeleteBehavior.Restrict)
|
||||
.IsRequired();
|
||||
|
||||
b.Navigation("Session");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariant", b =>
|
||||
{
|
||||
b.HasOne("Baya.Domain.Entities.Identity.NurseProfile", "Nurse")
|
||||
@@ -3578,6 +4098,18 @@ namespace Baya.Infrastructure.Persistence.Migrations
|
||||
b.Navigation("StepType");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Booking.Booking", b =>
|
||||
{
|
||||
b.Navigation("CareInstructions");
|
||||
|
||||
b.Navigation("Sessions");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Booking.BookingSession", b =>
|
||||
{
|
||||
b.Navigation("Verification");
|
||||
});
|
||||
|
||||
modelBuilder.Entity("Baya.Domain.Entities.Catalog.NurseServiceVariant", b =>
|
||||
{
|
||||
b.Navigation("Options");
|
||||
|
||||
+272
@@ -0,0 +1,272 @@
|
||||
#nullable enable
|
||||
using Baya.Application.Contracts.Persistence;
|
||||
using Baya.Application.Models.Booking;
|
||||
using Baya.Application.Models.Common;
|
||||
using Baya.Domain.Entities.Booking;
|
||||
using Baya.Domain.Entities.Identity;
|
||||
using Baya.Infrastructure.Persistence.Repositories.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Repositories;
|
||||
|
||||
internal sealed class BookingRepository : BaseAsyncRepository<Booking>, IBookingRepository
|
||||
{
|
||||
public BookingRepository(ApplicationDbContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
|
||||
public Task AddAsync(Booking booking, CancellationToken cancellationToken)
|
||||
=> base.AddAsync(booking);
|
||||
|
||||
public Task<long?> GetBookingIdByRequestIdAsync(long bookingRequestId, CancellationToken cancellationToken)
|
||||
=> TableNoTracking
|
||||
.Where(b => b.BookingRequestId == bookingRequestId)
|
||||
.Select(b => (long?)b.Id)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
public async Task<BookingDetailProjection?> GetDetailAsync(long id, CancellationToken cancellationToken)
|
||||
{
|
||||
var row = await (
|
||||
from b in TableNoTracking
|
||||
where b.Id == id
|
||||
join n in DbContext.Set<NurseProfile>() on b.NurseId equals n.Id
|
||||
join p in DbContext.Set<Patient>() on b.PatientId equals p.Id
|
||||
select new
|
||||
{
|
||||
Booking = b,
|
||||
NurseName = n.User.Name,
|
||||
NurseFamily = n.User.FamilyName,
|
||||
PatientName = p.DisplayName,
|
||||
Sessions = b.Sessions
|
||||
.OrderBy(s => s.SessionIndex)
|
||||
.Select(s => new BookingSessionProjection(
|
||||
s.Id,
|
||||
s.SessionIndex,
|
||||
s.ScheduledDate,
|
||||
s.ScheduledTimeStart,
|
||||
s.ScheduledTimeEnd,
|
||||
s.Status,
|
||||
s.VisitPayoutAmount,
|
||||
s.PayoutEligibleAt,
|
||||
s.Verification == null ? null : s.Verification.Status,
|
||||
s.Verification == null ? null : s.Verification.CheckInAt,
|
||||
s.Verification == null ? null : s.Verification.CheckOutAt,
|
||||
s.Verification == null ? null : s.Verification.CheckInAddressMatch))
|
||||
.ToList()
|
||||
})
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (row is null)
|
||||
return null;
|
||||
|
||||
var bk = row.Booking;
|
||||
return new BookingDetailProjection(
|
||||
bk.Id, bk.BookingRequestId, bk.Status, bk.CustomerId, bk.NurseId,
|
||||
ComposeName(row.NurseName, row.NurseFamily), bk.PatientId, row.PatientName,
|
||||
bk.VariantId, bk.VariantSnapshotJson, bk.CustomerAddressId, bk.AddressSnapshotJson,
|
||||
bk.GrossPriceIrr, bk.BalinyaarCommissionIrr, bk.PlatformFeeRate, bk.NursePayoutAmount, bk.PspFeeAmount,
|
||||
bk.SessionCount, bk.ScheduledDate, bk.ScheduledTimeStart, bk.ScheduledTimeEnd,
|
||||
bk.ConfirmedAt, bk.CompletedAt, bk.CancelledAt, bk.CancelledBy, bk.CancellationReason,
|
||||
bk.CancellationPolicyCode, bk.CancellationRefundPercentage, bk.RefundableAmountIrr,
|
||||
bk.DisputeWindowEndsAt, bk.CreatedAt, row.Sessions);
|
||||
}
|
||||
|
||||
public Task<PagedResult<BookingListItemDto>> ListForCustomerAsync(long customerId, string? status, int page, int pageSize, CancellationToken cancellationToken)
|
||||
=> ListAsync(TableNoTracking.Where(b => b.CustomerId == customerId), status, forNurse: false, page, pageSize, cancellationToken);
|
||||
|
||||
public Task<PagedResult<BookingListItemDto>> ListForNurseAsync(long nurseId, string? status, int page, int pageSize, CancellationToken cancellationToken)
|
||||
=> ListAsync(TableNoTracking.Where(b => b.NurseId == nurseId), status, forNurse: true, page, pageSize, cancellationToken);
|
||||
|
||||
public Task<PagedResult<BookingListItemDto>> ListAllAsync(string? status, int page, int pageSize, CancellationToken cancellationToken)
|
||||
=> ListAsync(TableNoTracking, status, forNurse: false, page, pageSize, cancellationToken);
|
||||
|
||||
private async Task<PagedResult<BookingListItemDto>> ListAsync(
|
||||
IQueryable<Booking> source, string? status, bool forNurse, int page, int pageSize, CancellationToken cancellationToken)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(status))
|
||||
source = source.Where(b => b.Status == status);
|
||||
|
||||
var total = await source.CountAsync(cancellationToken);
|
||||
|
||||
var rows = await (
|
||||
from b in source
|
||||
join n in DbContext.Set<NurseProfile>() on b.NurseId equals n.Id
|
||||
join p in DbContext.Set<Patient>() on b.PatientId equals p.Id
|
||||
orderby b.Id descending
|
||||
select new
|
||||
{
|
||||
b.Id,
|
||||
b.Status,
|
||||
NurseName = n.User.Name,
|
||||
NurseFamily = n.User.FamilyName,
|
||||
PatientName = p.DisplayName,
|
||||
b.ScheduledDate,
|
||||
b.SessionCount,
|
||||
b.GrossPriceIrr,
|
||||
b.NursePayoutAmount,
|
||||
b.DisputeWindowEndsAt,
|
||||
b.CreatedAt
|
||||
})
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var items = rows
|
||||
.Select(r => new BookingListItemDto(
|
||||
r.Id,
|
||||
r.Status,
|
||||
forNurse ? r.PatientName : ComposeName(r.NurseName, r.NurseFamily),
|
||||
r.ScheduledDate,
|
||||
r.SessionCount,
|
||||
(forNurse ? r.NursePayoutAmount : r.GrossPriceIrr).ToString(),
|
||||
r.DisputeWindowEndsAt,
|
||||
r.CreatedAt))
|
||||
.ToList();
|
||||
|
||||
return new PagedResult<BookingListItemDto>(items, total, page, pageSize);
|
||||
}
|
||||
|
||||
public Task<BookingParticipants?> GetParticipantsAsync(long bookingId, CancellationToken cancellationToken)
|
||||
=> (
|
||||
from b in TableNoTracking
|
||||
where b.Id == bookingId
|
||||
join c in DbContext.Set<CustomerProfile>() on b.CustomerId equals c.Id
|
||||
join n in DbContext.Set<NurseProfile>() on b.NurseId equals n.Id
|
||||
select new BookingParticipants(c.UserId, n.UserId))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
public Task<Booking?> GetTrackedWithSessionsAsync(long id, CancellationToken cancellationToken)
|
||||
=> Table.Include(b => b.Sessions).FirstOrDefaultAsync(b => b.Id == id, cancellationToken);
|
||||
|
||||
public Task<Booking?> GetTrackedWithCareAsync(long id, CancellationToken cancellationToken)
|
||||
=> Table.Include(b => b.CareInstructions).FirstOrDefaultAsync(b => b.Id == id, cancellationToken);
|
||||
|
||||
public async Task<Booking?> GetTrackedBookingBySessionAsync(long sessionId, CancellationToken cancellationToken)
|
||||
{
|
||||
var bookingId = await DbContext.Set<BookingSession>()
|
||||
.Where(s => s.Id == sessionId)
|
||||
.Select(s => (long?)s.BookingId)
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
if (bookingId is not { } bid)
|
||||
return null;
|
||||
|
||||
return await Table
|
||||
.Include(b => b.Sessions).ThenInclude(s => s.Verification)
|
||||
.FirstOrDefaultAsync(b => b.Id == bid, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<CareInstructionsGate?> GetCareInstructionsGateAsync(long bookingId, CancellationToken cancellationToken)
|
||||
=> TableNoTracking
|
||||
.Where(b => b.Id == bookingId)
|
||||
.Select(b => new CareInstructionsGate(
|
||||
b.Status,
|
||||
b.NurseId,
|
||||
b.CustomerId,
|
||||
b.CareInstructions == null
|
||||
? null
|
||||
: new CareInstructionsDto(
|
||||
b.Id,
|
||||
b.CareInstructions.CurrentConditions,
|
||||
b.CareInstructions.Medications,
|
||||
b.CareInstructions.Allergies,
|
||||
b.CareInstructions.SpecialInstructions,
|
||||
b.CareInstructions.EmergencyContactName,
|
||||
b.CareInstructions.EmergencyContactPhone)))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
public async Task<PagedResult<BookingSessionListItemDto>> ListSessionsForNurseAsync(
|
||||
long nurseId, DateOnly? date, int page, int pageSize, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = DbContext.Set<BookingSession>().AsNoTracking()
|
||||
.Where(s => s.Booking.NurseId == nurseId);
|
||||
|
||||
if (date is { } d)
|
||||
query = query.Where(s => s.ScheduledDate == d);
|
||||
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
|
||||
var items = await (
|
||||
from s in query
|
||||
join p in DbContext.Set<Patient>() on s.Booking.PatientId equals p.Id
|
||||
orderby s.ScheduledDate, s.ScheduledTimeStart
|
||||
select new BookingSessionListItemDto(
|
||||
s.Id,
|
||||
s.BookingId,
|
||||
s.SessionIndex,
|
||||
p.DisplayName,
|
||||
s.ScheduledDate,
|
||||
s.ScheduledTimeStart,
|
||||
s.ScheduledTimeEnd,
|
||||
s.Status,
|
||||
s.Verification == null ? VisitVerificationStatus.Pending : s.Verification.Status))
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new PagedResult<BookingSessionListItemDto>(items, total, page, pageSize);
|
||||
}
|
||||
|
||||
public Task<EvvGate?> GetEvvForSessionAsync(long sessionId, CancellationToken cancellationToken)
|
||||
=> DbContext.Set<BookingSession>().AsNoTracking()
|
||||
.Where(s => s.Id == sessionId)
|
||||
.Select(s => new EvvGate(
|
||||
s.Booking.NurseId,
|
||||
s.Booking.CustomerId,
|
||||
s.Verification == null
|
||||
? null
|
||||
: new VisitVerificationDto(
|
||||
s.Verification.Id,
|
||||
s.Id,
|
||||
s.Verification.Status,
|
||||
s.Verification.CheckInAt,
|
||||
s.Verification.CheckInLat,
|
||||
s.Verification.CheckInLng,
|
||||
s.Verification.CheckOutAt,
|
||||
s.Verification.CheckOutLat,
|
||||
s.Verification.CheckOutLng,
|
||||
s.Verification.CheckInAddressMatch,
|
||||
s.Verification.CheckInDistanceMeters)))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
public async Task<PagedResult<AdminEvvItemDto>> ListAdminEvvAsync(string type, int page, int pageSize, CancellationToken cancellationToken)
|
||||
{
|
||||
var query = DbContext.Set<BookingSession>().AsNoTracking().AsQueryable();
|
||||
|
||||
// The two admin review lanes: an advisory location mismatch, or a no-show (missed) session.
|
||||
query = type == "no_show"
|
||||
? query.Where(s => s.Status == BookingSessionStatus.Missed)
|
||||
: query.Where(s => s.Verification != null && s.Verification.CheckInAddressMatch == false);
|
||||
|
||||
var total = await query.CountAsync(cancellationToken);
|
||||
|
||||
var items = await query
|
||||
.OrderByDescending(s => s.Id)
|
||||
.Skip((page - 1) * pageSize)
|
||||
.Take(pageSize)
|
||||
.Select(s => new AdminEvvItemDto(
|
||||
s.Id,
|
||||
s.BookingId,
|
||||
s.Booking.NurseId,
|
||||
s.Status,
|
||||
s.ScheduledDate,
|
||||
s.ScheduledTimeStart,
|
||||
s.Verification == null ? null : s.Verification.CheckInAt,
|
||||
s.Verification == null ? null : s.Verification.CheckInAddressMatch,
|
||||
s.Verification == null ? null : s.Verification.CheckInDistanceMeters))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
return new PagedResult<AdminEvvItemDto>(items, total, page, pageSize);
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<BookingSession>> GetNoShowCandidatesAsync(DateOnly today, int batchSize, CancellationToken cancellationToken)
|
||||
=> await DbContext.Set<BookingSession>()
|
||||
.Include(s => s.Booking)
|
||||
.Where(s => s.Status == BookingSessionStatus.Scheduled && s.ScheduledDate <= today)
|
||||
.OrderBy(s => s.Id)
|
||||
.Take(batchSize)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
private static string ComposeName(string? name, string? familyName)
|
||||
=> string.Join(' ', new[] { name, familyName }.Where(s => !string.IsNullOrWhiteSpace(s))).Trim();
|
||||
}
|
||||
+56
@@ -1,6 +1,7 @@
|
||||
#nullable enable
|
||||
using Baya.Application.Contracts.Persistence;
|
||||
using Baya.Application.Models.Booking;
|
||||
using Baya.Application.Models.Catalog;
|
||||
using Baya.Application.Models.Common;
|
||||
using Baya.Domain.Entities.Booking;
|
||||
using Baya.Infrastructure.Persistence.Repositories.Common;
|
||||
@@ -166,6 +167,61 @@ internal sealed class BookingRequestRepository : BaseAsyncRepository<BookingRequ
|
||||
row.NurseRejectionReason, row.CreatedAt);
|
||||
}
|
||||
|
||||
public Task<BookingRequest?> GetTrackedByIdAsync(long id, CancellationToken cancellationToken)
|
||||
=> Table.FirstOrDefaultAsync(r => r.Id == id, cancellationToken);
|
||||
|
||||
public Task<BookingConversionSource?> GetConversionSourceAsync(long id, CancellationToken cancellationToken)
|
||||
=> TableNoTracking
|
||||
.Where(r => r.Id == id)
|
||||
.Select(r => new BookingConversionSource(
|
||||
r.Id,
|
||||
r.Status,
|
||||
r.CustomerId,
|
||||
r.Customer.UserId,
|
||||
r.NurseId,
|
||||
r.Nurse.UserId,
|
||||
r.PatientId,
|
||||
r.Patient.DisplayName,
|
||||
r.VariantId,
|
||||
r.CustomerAddressId,
|
||||
r.RequestedDate,
|
||||
r.RequestedTimeStart,
|
||||
r.RequestedTimeEnd,
|
||||
new VariantSnapshot(
|
||||
r.Variant.Id,
|
||||
r.Variant.ServiceCategoryId,
|
||||
r.Variant.ServiceCategory.NameFa,
|
||||
r.Variant.ServiceCategory.NameEn,
|
||||
r.Variant.Price,
|
||||
r.Variant.PriceUnit,
|
||||
r.Variant.SessionCount,
|
||||
r.Variant.DisplayName,
|
||||
r.Variant.Options
|
||||
.Select(o => new VariantOptionDto(
|
||||
o.OptionGroupId,
|
||||
o.OptionGroup.NameFa,
|
||||
o.OptionGroup.NameEn,
|
||||
o.OptionValueId,
|
||||
o.OptionValue.NameFa,
|
||||
o.OptionValue.NameEn))
|
||||
.ToList()),
|
||||
new AddressSnapshot(
|
||||
r.CustomerAddress.Id,
|
||||
r.CustomerAddress.Title,
|
||||
r.CustomerAddress.CityId,
|
||||
r.CustomerAddress.City.NameFa,
|
||||
r.CustomerAddress.City.NameEn,
|
||||
r.CustomerAddress.DistrictId,
|
||||
r.CustomerAddress.DistrictId == null ? null : r.CustomerAddress.District.NameFa,
|
||||
r.CustomerAddress.DistrictId == null ? null : r.CustomerAddress.District.NameEn,
|
||||
r.CustomerAddress.AddressLine,
|
||||
r.CustomerAddress.PostalCode,
|
||||
r.CustomerAddress.RecipientName,
|
||||
r.CustomerAddress.RecipientPhone,
|
||||
r.CustomerAddress.Latitude,
|
||||
r.CustomerAddress.Longitude)))
|
||||
.FirstOrDefaultAsync(cancellationToken);
|
||||
|
||||
// Actionable (awaiting a party's action) rows float above terminal ones, then most-recent first. Recency
|
||||
// (id) rather than the deadline is the secondary key: for a given status the deadline tracks creation
|
||||
// time anyway, and DateTimeOffset is not sortable on the SQLite test provider.
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
#nullable enable
|
||||
using Baya.Application.Contracts.Persistence;
|
||||
using Baya.Application.Models.Booking;
|
||||
using Baya.Domain.Entities.Booking;
|
||||
using Baya.Infrastructure.Persistence.Repositories.Common;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Baya.Infrastructure.Persistence.Repositories;
|
||||
|
||||
internal sealed class CancellationPolicyRepository : BaseAsyncRepository<CancellationPolicy>, ICancellationPolicyRepository
|
||||
{
|
||||
public CancellationPolicyRepository(ApplicationDbContext dbContext) : base(dbContext)
|
||||
{
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyList<CancellationPolicy>> GetActiveForActorAsync(string appliesTo, CancellationToken cancellationToken)
|
||||
=> await TableNoTracking
|
||||
.Where(p => p.AppliesTo == appliesTo && p.IsActive)
|
||||
// Tighter (bounded) buckets first so the resolver prefers the most specific covering tier.
|
||||
.OrderBy(p => p.HoursBeforeStartMin ?? int.MinValue)
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
public async Task<IReadOnlyList<CancellationPolicyDto>> ListAsync(CancellationToken cancellationToken)
|
||||
=> await TableNoTracking
|
||||
.OrderBy(p => p.AppliesTo).ThenBy(p => p.Code)
|
||||
.Select(p => new CancellationPolicyDto(
|
||||
p.Id, p.Code, p.AppliesTo, p.HoursBeforeStartMin, p.HoursBeforeStartMax,
|
||||
p.RefundPercentage, p.FeeAmountIrr.ToString(), p.FeeRate, p.IsActive))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
public Task<CancellationPolicy?> GetTrackedByCodeAsync(string code, CancellationToken cancellationToken)
|
||||
=> Table.FirstOrDefaultAsync(p => p.Code == code, cancellationToken);
|
||||
|
||||
public Task AddAsync(CancellationPolicy policy, CancellationToken cancellationToken)
|
||||
=> base.AddAsync(policy);
|
||||
}
|
||||
+4
@@ -20,6 +20,8 @@ public class UnitOfWork : IUnitOfWork
|
||||
public INurseServiceVariantRepository NurseServiceVariantRepository { get; }
|
||||
public IVerificationRepository VerificationRepository { get; }
|
||||
public IBookingRequestRepository BookingRequestRepository { get; }
|
||||
public IBookingRepository BookingRepository { get; }
|
||||
public ICancellationPolicyRepository CancellationPolicyRepository { get; }
|
||||
|
||||
public UnitOfWork(ApplicationDbContext db)
|
||||
{
|
||||
@@ -38,6 +40,8 @@ public class UnitOfWork : IUnitOfWork
|
||||
NurseServiceVariantRepository = new NurseServiceVariantRepository(_db);
|
||||
VerificationRepository = new VerificationRepository(_db);
|
||||
BookingRequestRepository = new BookingRequestRepository(_db);
|
||||
BookingRepository = new BookingRepository(_db);
|
||||
CancellationPolicyRepository = new CancellationPolicyRepository(_db);
|
||||
}
|
||||
|
||||
public Task CommitAsync()
|
||||
|
||||
Reference in New Issue
Block a user