backend phase 1: config, reference & platform signals

Lay the cross-cutting platform backbone every later phase reads from. Adds
the first marketplace EF migration baseline (new `ops` schema) and the
mechanisms b2..b15 reuse: typed runtime config, an append-only audit trail,
an analytics event log, the holiday/bank-closure calendar, in-app
notifications, and the internal support-alert worklist.

Schema & migration
- New `ops` schema + migration InitialMarketplaceBaseline with 6 tables:
  PlatformConfigs (IAuditable), AuditLogs (append-only), SystemEvents,
  IranianHolidays, Notifications, SupportAlerts — with indexes/uniques and
  FKs to usr.Users. Seeded 12 config keys + 7 sample holidays via HasData.

Domain / Application
- IAuditable marker + [AuditRedacted] attribute; entities + string-code
  constant holders (config data_type, holiday type, alert type/severity/status).
- Facade contracts: IPlatformConfig, IHolidayCalendar, IAnalyticsSink,
  IAuditLogger, INotificationService, ISupportAlertService; DTOs +
  PagedResult<T>; evolved the INotificationDispatcher.Notification record to
  carry Type + DataJson; Pagination helper.
- 14 CQRS commands/queries (+ validators) wiring the endpoints to the facades.

Infrastructure
- DB-backed facade implementations in Persistence/Services/; real in-app
  INotificationDispatcher (removes the b0 log stub); notification-retention
  hosted service (purge is_read=1 AND age>90d).
- Extended AuditFieldInterceptor to also append an old/new-diff audit_logs row
  for every IAuditable change in the same transaction (PII redacted).
- Registered all facades + hosted service in AddPersistenceServices; removed
  the dispatcher registration from AddCrossCuttingSeams.

API
- 5 controllers: admin PlatformConfig/Holidays/Audit/SupportAlerts
  ([Authorize(DynamicPermission)]) + current-user Notifications ([Authorize]),
  all tenant-scoped and paginated. 16 Swagger paths total.

Money-correctness & safety rules honoured
- Config read at compute time (cached, parsed by data_type), never hardcoded;
  every config change is audited in the same transaction; audit_logs is
  append-only (no update/delete path); support alerts are admin-only;
  notifications are tenant-scoped; analytics is fire-and-forget.

Tests & docs
- 18 new foundation tests over in-memory SQLite (config typing + audit,
  holidays, notifications + tenancy + retention, support alerts, analytics);
  build clean (0 new code warnings), 22 tests green; migration applied to the
  dev DB and swagger.v1.json refreshed.
- Updated server Project map + CONVENTIONS, product data-model doc 12 (seeded
  config defaults), config-reference contract, mock registry, backend handoff/
  STATUS/report.

Follow-ups: add FK constraints for SupportAlerts.BookingId (b9) and ReviewId
(b14) when those tables land.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hamid
2026-07-02 01:18:00 +03:30
parent aae1ce971f
commit 2f2aec61a2
99 changed files with 6172 additions and 52 deletions
@@ -0,0 +1,871 @@
// <auto-generated />
using System;
using Baya.Infrastructure.Persistence;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
#nullable disable
namespace Baya.Infrastructure.Persistence.Migrations
{
[DbContext(typeof(ApplicationDbContext))]
[Migration("20260701193257_InitialMarketplaceBaseline")]
partial class InitialMarketplaceBaseline
{
/// <inheritdoc />
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "10.0.9")
.HasAnnotation("Relational:MaxIdentifierLength", 128);
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Baya.Domain.Entities.Analytics.SystemEvent", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTimeOffset>("OccurredAt")
.HasColumnType("datetimeoffset");
b.Property<string>("PropsJson")
.HasColumnType("nvarchar(max)");
b.Property<int?>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Name");
b.HasIndex("OccurredAt");
b.HasIndex("UserId");
b.ToTable("SystemEvents", "ops");
});
modelBuilder.Entity("Baya.Domain.Entities.Audit.AuditLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<int?>("ActorUserId")
.HasColumnType("int");
b.Property<string>("ChangedFieldsJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("EntityId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("EntityType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTimeOffset>("OccurredAt")
.HasColumnType("datetimeoffset");
b.HasKey("Id");
b.HasIndex("ActorUserId");
b.HasIndex("OccurredAt");
b.HasIndex("EntityType", "EntityId");
b.ToTable("AuditLogs", "ops");
});
modelBuilder.Entity("Baya.Domain.Entities.Configuration.PlatformConfig", 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>("DataType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTimeOffset?>("ModifiedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("ModifiedById")
.HasColumnType("int");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("PlatformConfigs", "ops");
b.HasData(
new
{
Id = 1L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "decimal",
Description = "Balinyaar commission rate on the booking gross (fraction).",
Key = "platform_fee_rate",
Value = "0.15"
},
new
{
Id = 2L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "decimal",
Description = "VAT rate applied to the commission line only (fraction).",
Key = "vat_rate",
Value = "0.10"
},
new
{
Id = 3L,
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 after check-out a booking can be disputed.",
Key = "dispute_window_hours",
Value = "72"
},
new
{
Id = 4L,
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 a family has to pay before a pending booking expires.",
Key = "booking_payment_deadline_minutes",
Value = "30"
},
new
{
Id = 5L,
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 a nurse has to accept/decline a booking request.",
Key = "nurse_response_deadline_hours",
Value = "24"
},
new
{
Id = 6L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "int",
Description = "Weekly payout cadence in days.",
Key = "nurse_payout_interval_days",
Value = "7"
},
new
{
Id = 7L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "int",
Description = "Allowed EVV check-in distance from the care address.",
Key = "evv_location_tolerance_meters",
Value = "200"
},
new
{
Id = 8L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "decimal",
Description = "A review at or below this rating raises a support alert.",
Key = "min_rating_for_support_alert",
Value = "2"
},
new
{
Id = 9L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "string",
Description = "Who is merchant of record for BNPL orders (platform|nurse).",
Key = "bnpl_merchant_of_record",
Value = "platform"
},
new
{
Id = 10L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "decimal",
Description = "BNPL provider commission rate (fraction).",
Key = "bnpl_provider_commission_rate",
Value = "0.07"
},
new
{
Id = 11L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "string",
Description = "When BNPL settles funds to the platform (immediate|deferred).",
Key = "bnpl_settlement_timing",
Value = "immediate"
},
new
{
Id = 12L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "json",
Description = "Tiered cancellation refund policy: refund_percent by hours before the visit.",
Key = "cancellation_tiers",
Value = "[{\"min_hours_before\":48,\"refund_percent\":100},{\"min_hours_before\":24,\"refund_percent\":50},{\"min_hours_before\":0,\"refund_percent\":0}]"
});
});
modelBuilder.Entity("Baya.Domain.Entities.Holidays.IranianHoliday", 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<DateOnly>("HolidayDate")
.HasColumnType("date");
b.Property<bool>("IsBankClosed")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("ModifiedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("ModifiedById")
.HasColumnType("int");
b.Property<string>("NameFa")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.HasKey("Id");
b.HasIndex("HolidayDate")
.IsUnique();
b.ToTable("IranianHolidays", "ops");
b.HasData(
new
{
Id = 1L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
HolidayDate = new DateOnly(2026, 2, 11),
IsBankClosed = true,
NameFa = "پیروزی انقلاب اسلامی",
Type = "national"
},
new
{
Id = 2L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
HolidayDate = new DateOnly(2026, 3, 21),
IsBankClosed = true,
NameFa = "نوروز",
Type = "national"
},
new
{
Id = 3L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
HolidayDate = new DateOnly(2026, 3, 22),
IsBankClosed = true,
NameFa = "نوروز",
Type = "national"
},
new
{
Id = 4L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
HolidayDate = new DateOnly(2026, 3, 23),
IsBankClosed = true,
NameFa = "نوروز",
Type = "national"
},
new
{
Id = 5L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
HolidayDate = new DateOnly(2026, 3, 24),
IsBankClosed = true,
NameFa = "نوروز",
Type = "national"
},
new
{
Id = 6L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
HolidayDate = new DateOnly(2026, 4, 1),
IsBankClosed = true,
NameFa = "روز طبیعت (سیزده‌به‌در)",
Type = "official"
},
new
{
Id = 7L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
HolidayDate = new DateOnly(2026, 6, 26),
IsBankClosed = true,
NameFa = "عید سعید قربان",
Type = "religious"
});
});
modelBuilder.Entity("Baya.Domain.Entities.Notifications.Notification", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Body")
.HasColumnType("nvarchar(max)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("datetimeoffset");
b.Property<string>("DataJson")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsRead")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<DateTimeOffset?>("ReadAt")
.HasColumnType("datetimeoffset");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId", "IsRead", "CreatedAt");
b.ToTable("Notifications", "ops");
});
modelBuilder.Entity("Baya.Domain.Entities.SupportAlerts.SupportAlert", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<long?>("BookingId")
.HasColumnType("bigint");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("CreatedById")
.HasColumnType("int");
b.Property<string>("EntityId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("EntityType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTimeOffset?>("ModifiedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("ModifiedById")
.HasColumnType("int");
b.Property<int?>("OwnerUserId")
.HasColumnType("int");
b.Property<string>("ResolutionNote")
.HasColumnType("nvarchar(max)");
b.Property<DateTimeOffset?>("ResolvedAt")
.HasColumnType("datetimeoffset");
b.Property<long?>("ReviewId")
.HasColumnType("bigint");
b.Property<string>("Severity")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("nvarchar(40)");
b.HasKey("Id");
b.HasIndex("OwnerUserId");
b.HasIndex("Status");
b.HasIndex("Type");
b.ToTable("SupportAlerts", "ops");
});
modelBuilder.Entity("Baya.Domain.Entities.User.Role", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedDate")
.HasColumnType("datetime2");
b.Property<string>("DisplayName")
.HasColumnType("nvarchar(max)");
b.Property<string>("Name")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedName")
.IsUnique()
.HasDatabaseName("RoleNameIndex")
.HasFilter("[NormalizedName] IS NOT NULL");
b.ToTable("Roles", "usr");
});
modelBuilder.Entity("Baya.Domain.Entities.User.RoleClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<DateTime>("CreatedClaim")
.HasColumnType("datetime2");
b.Property<int>("RoleId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("RoleId");
b.ToTable("RoleClaims", "usr");
});
modelBuilder.Entity("Baya.Domain.Entities.User.User", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("UserId");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<int>("AccessFailedCount")
.HasColumnType("int");
b.Property<string>("ConcurrencyStamp")
.IsConcurrencyToken()
.HasColumnType("nvarchar(max)");
b.Property<string>("Email")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<bool>("EmailConfirmed")
.HasColumnType("bit");
b.Property<string>("FamilyName")
.HasColumnType("nvarchar(max)");
b.Property<string>("GeneratedCode")
.HasColumnType("nvarchar(max)");
b.Property<bool>("LockoutEnabled")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("LockoutEnd")
.HasColumnType("datetimeoffset");
b.Property<string>("Name")
.HasColumnType("nvarchar(max)");
b.Property<string>("NormalizedEmail")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("NormalizedUserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.Property<string>("PasswordHash")
.HasColumnType("nvarchar(max)");
b.Property<string>("PhoneNumber")
.HasColumnType("nvarchar(max)");
b.Property<bool>("PhoneNumberConfirmed")
.HasColumnType("bit");
b.Property<string>("SecurityStamp")
.HasColumnType("nvarchar(max)");
b.Property<bool>("TwoFactorEnabled")
.HasColumnType("bit");
b.Property<string>("UserName")
.HasMaxLength(256)
.HasColumnType("nvarchar(256)");
b.HasKey("Id");
b.HasIndex("NormalizedEmail")
.HasDatabaseName("EmailIndex");
b.HasIndex("NormalizedUserName")
.IsUnique()
.HasDatabaseName("UserNameIndex")
.HasFilter("[NormalizedUserName] IS NOT NULL");
b.ToTable("Users", "usr");
});
modelBuilder.Entity("Baya.Domain.Entities.User.UserClaim", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<int>("Id"));
b.Property<string>("ClaimType")
.HasColumnType("nvarchar(max)");
b.Property<string>("ClaimValue")
.HasColumnType("nvarchar(max)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserClaims", "usr");
});
modelBuilder.Entity("Baya.Domain.Entities.User.UserLogin", b =>
{
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("ProviderKey")
.HasColumnType("nvarchar(450)");
b.Property<DateTime>("LoggedOn")
.HasColumnType("datetime2");
b.Property<string>("ProviderDisplayName")
.HasColumnType("nvarchar(max)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("LoginProvider", "ProviderKey");
b.HasIndex("UserId");
b.ToTable("UserLogins", "usr");
});
modelBuilder.Entity("Baya.Domain.Entities.User.UserRefreshToken", b =>
{
b.Property<Guid>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("uniqueidentifier");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("CreatedById")
.HasColumnType("int");
b.Property<bool>("IsValid")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("ModifiedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("ModifiedById")
.HasColumnType("int");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId");
b.ToTable("UserRefreshTokens", "usr");
});
modelBuilder.Entity("Baya.Domain.Entities.User.UserRole", b =>
{
b.Property<int>("UserId")
.HasColumnType("int");
b.Property<int>("RoleId")
.HasColumnType("int");
b.Property<DateTime>("CreatedUserRoleDate")
.HasColumnType("datetime2");
b.HasKey("UserId", "RoleId");
b.HasIndex("RoleId");
b.ToTable("UserRoles", "usr");
});
modelBuilder.Entity("Baya.Domain.Entities.User.UserToken", b =>
{
b.Property<int>("UserId")
.HasColumnType("int");
b.Property<string>("LoginProvider")
.HasColumnType("nvarchar(450)");
b.Property<string>("Name")
.HasColumnType("nvarchar(450)");
b.Property<DateTime>("GeneratedTime")
.HasColumnType("datetime2");
b.Property<string>("Value")
.HasColumnType("nvarchar(max)");
b.HasKey("UserId", "LoginProvider", "Name");
b.ToTable("UserTokens", "usr");
});
modelBuilder.Entity("Baya.Domain.Entities.Analytics.SystemEvent", b =>
{
b.HasOne("Baya.Domain.Entities.User.User", null)
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Baya.Domain.Entities.Audit.AuditLog", b =>
{
b.HasOne("Baya.Domain.Entities.User.User", null)
.WithMany()
.HasForeignKey("ActorUserId");
});
modelBuilder.Entity("Baya.Domain.Entities.Notifications.Notification", b =>
{
b.HasOne("Baya.Domain.Entities.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Baya.Domain.Entities.SupportAlerts.SupportAlert", b =>
{
b.HasOne("Baya.Domain.Entities.User.User", null)
.WithMany()
.HasForeignKey("OwnerUserId");
});
modelBuilder.Entity("Baya.Domain.Entities.User.RoleClaim", b =>
{
b.HasOne("Baya.Domain.Entities.User.Role", "Role")
.WithMany("Claims")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Role");
});
modelBuilder.Entity("Baya.Domain.Entities.User.UserClaim", b =>
{
b.HasOne("Baya.Domain.Entities.User.User", "User")
.WithMany("Claims")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Baya.Domain.Entities.User.UserLogin", b =>
{
b.HasOne("Baya.Domain.Entities.User.User", "User")
.WithMany("Logins")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Baya.Domain.Entities.User.UserRefreshToken", b =>
{
b.HasOne("Baya.Domain.Entities.User.User", "User")
.WithMany("UserRefreshTokens")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Baya.Domain.Entities.User.UserRole", b =>
{
b.HasOne("Baya.Domain.Entities.User.Role", "Role")
.WithMany("Users")
.HasForeignKey("RoleId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.HasOne("Baya.Domain.Entities.User.User", "User")
.WithMany("UserRoles")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("Role");
b.Navigation("User");
});
modelBuilder.Entity("Baya.Domain.Entities.User.UserToken", b =>
{
b.HasOne("Baya.Domain.Entities.User.User", "User")
.WithMany("Tokens")
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
b.Navigation("User");
});
modelBuilder.Entity("Baya.Domain.Entities.User.Role", b =>
{
b.Navigation("Claims");
b.Navigation("Users");
});
modelBuilder.Entity("Baya.Domain.Entities.User.User", b =>
{
b.Navigation("Claims");
b.Navigation("Logins");
b.Navigation("Tokens");
b.Navigation("UserRefreshTokens");
b.Navigation("UserRoles");
});
#pragma warning restore 612, 618
}
}
}
@@ -0,0 +1,308 @@
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 InitialMarketplaceBaseline : Migration
{
/// <inheritdoc />
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.EnsureSchema(
name: "ops");
migrationBuilder.CreateTable(
name: "AuditLogs",
schema: "ops",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
EntityType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
EntityId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Action = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
ChangedFieldsJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
ActorUserId = table.Column<int>(type: "int", nullable: true),
OccurredAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_AuditLogs", x => x.Id);
table.ForeignKey(
name: "FK_AuditLogs_Users_ActorUserId",
column: x => x.ActorUserId,
principalSchema: "usr",
principalTable: "Users",
principalColumn: "UserId");
});
migrationBuilder.CreateTable(
name: "IranianHolidays",
schema: "ops",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
HolidayDate = table.Column<DateOnly>(type: "date", nullable: false),
NameFa = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
Type = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
IsBankClosed = table.Column<bool>(type: "bit", nullable: false),
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_IranianHolidays", x => x.Id);
});
migrationBuilder.CreateTable(
name: "Notifications",
schema: "ops",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
UserId = table.Column<int>(type: "int", nullable: false),
Type = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
Body = table.Column<string>(type: "nvarchar(max)", nullable: true),
DataJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
IsRead = table.Column<bool>(type: "bit", nullable: false, defaultValue: false),
ReadAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: true),
CreatedAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Notifications", x => x.Id);
table.ForeignKey(
name: "FK_Notifications_Users_UserId",
column: x => x.UserId,
principalSchema: "usr",
principalTable: "Users",
principalColumn: "UserId",
onDelete: ReferentialAction.Restrict);
});
migrationBuilder.CreateTable(
name: "PlatformConfigs",
schema: "ops",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Key = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
Value = table.Column<string>(type: "nvarchar(max)", nullable: false),
DataType = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
Description = table.Column<string>(type: "nvarchar(500)", maxLength: 500, 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_PlatformConfigs", x => x.Id);
});
migrationBuilder.CreateTable(
name: "SupportAlerts",
schema: "ops",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Type = table.Column<string>(type: "nvarchar(40)", maxLength: 40, nullable: false),
Severity = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
Status = table.Column<string>(type: "nvarchar(20)", maxLength: 20, nullable: false),
EntityType = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
EntityId = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
BookingId = table.Column<long>(type: "bigint", nullable: true),
ReviewId = table.Column<long>(type: "bigint", nullable: true),
OwnerUserId = table.Column<int>(type: "int", nullable: true),
ResolutionNote = table.Column<string>(type: "nvarchar(max)", nullable: true),
ResolvedAt = 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_SupportAlerts", x => x.Id);
table.ForeignKey(
name: "FK_SupportAlerts_Users_OwnerUserId",
column: x => x.OwnerUserId,
principalSchema: "usr",
principalTable: "Users",
principalColumn: "UserId");
});
migrationBuilder.CreateTable(
name: "SystemEvents",
schema: "ops",
columns: table => new
{
Id = table.Column<long>(type: "bigint", nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(type: "nvarchar(100)", maxLength: 100, nullable: false),
PropsJson = table.Column<string>(type: "nvarchar(max)", nullable: true),
UserId = table.Column<int>(type: "int", nullable: true),
OccurredAt = table.Column<DateTimeOffset>(type: "datetimeoffset", nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_SystemEvents", x => x.Id);
table.ForeignKey(
name: "FK_SystemEvents_Users_UserId",
column: x => x.UserId,
principalSchema: "usr",
principalTable: "Users",
principalColumn: "UserId");
});
migrationBuilder.InsertData(
schema: "ops",
table: "IranianHolidays",
columns: new[] { "Id", "CreatedAt", "CreatedById", "HolidayDate", "IsBankClosed", "ModifiedAt", "ModifiedById", "NameFa", "Type" },
values: new object[,]
{
{ 1L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, new DateOnly(2026, 2, 11), true, null, null, "پیروزی انقلاب اسلامی", "national" },
{ 2L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, new DateOnly(2026, 3, 21), true, null, null, "نوروز", "national" },
{ 3L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, new DateOnly(2026, 3, 22), true, null, null, "نوروز", "national" },
{ 4L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, new DateOnly(2026, 3, 23), true, null, null, "نوروز", "national" },
{ 5L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, new DateOnly(2026, 3, 24), true, null, null, "نوروز", "national" },
{ 6L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, new DateOnly(2026, 4, 1), true, null, null, "روز طبیعت (سیزده‌به‌در)", "official" },
{ 7L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, new DateOnly(2026, 6, 26), true, null, null, "عید سعید قربان", "religious" }
});
migrationBuilder.InsertData(
schema: "ops",
table: "PlatformConfigs",
columns: new[] { "Id", "CreatedAt", "CreatedById", "DataType", "Description", "Key", "ModifiedAt", "ModifiedById", "Value" },
values: new object[,]
{
{ 1L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "decimal", "Balinyaar commission rate on the booking gross (fraction).", "platform_fee_rate", null, null, "0.15" },
{ 2L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "decimal", "VAT rate applied to the commission line only (fraction).", "vat_rate", null, null, "0.10" },
{ 3L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "int", "Hours after check-out a booking can be disputed.", "dispute_window_hours", null, null, "72" },
{ 4L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "int", "Minutes a family has to pay before a pending booking expires.", "booking_payment_deadline_minutes", null, null, "30" },
{ 5L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "int", "Hours a nurse has to accept/decline a booking request.", "nurse_response_deadline_hours", null, null, "24" },
{ 6L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "int", "Weekly payout cadence in days.", "nurse_payout_interval_days", null, null, "7" },
{ 7L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "int", "Allowed EVV check-in distance from the care address.", "evv_location_tolerance_meters", null, null, "200" },
{ 8L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "decimal", "A review at or below this rating raises a support alert.", "min_rating_for_support_alert", null, null, "2" },
{ 9L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "string", "Who is merchant of record for BNPL orders (platform|nurse).", "bnpl_merchant_of_record", null, null, "platform" },
{ 10L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "decimal", "BNPL provider commission rate (fraction).", "bnpl_provider_commission_rate", null, null, "0.07" },
{ 11L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "string", "When BNPL settles funds to the platform (immediate|deferred).", "bnpl_settlement_timing", null, null, "immediate" },
{ 12L, new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)), null, "json", "Tiered cancellation refund policy: refund_percent by hours before the visit.", "cancellation_tiers", null, null, "[{\"min_hours_before\":48,\"refund_percent\":100},{\"min_hours_before\":24,\"refund_percent\":50},{\"min_hours_before\":0,\"refund_percent\":0}]" }
});
migrationBuilder.CreateIndex(
name: "IX_AuditLogs_ActorUserId",
schema: "ops",
table: "AuditLogs",
column: "ActorUserId");
migrationBuilder.CreateIndex(
name: "IX_AuditLogs_EntityType_EntityId",
schema: "ops",
table: "AuditLogs",
columns: new[] { "EntityType", "EntityId" });
migrationBuilder.CreateIndex(
name: "IX_AuditLogs_OccurredAt",
schema: "ops",
table: "AuditLogs",
column: "OccurredAt");
migrationBuilder.CreateIndex(
name: "IX_IranianHolidays_HolidayDate",
schema: "ops",
table: "IranianHolidays",
column: "HolidayDate",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_Notifications_UserId_IsRead_CreatedAt",
schema: "ops",
table: "Notifications",
columns: new[] { "UserId", "IsRead", "CreatedAt" });
migrationBuilder.CreateIndex(
name: "IX_PlatformConfigs_Key",
schema: "ops",
table: "PlatformConfigs",
column: "Key",
unique: true);
migrationBuilder.CreateIndex(
name: "IX_SupportAlerts_OwnerUserId",
schema: "ops",
table: "SupportAlerts",
column: "OwnerUserId");
migrationBuilder.CreateIndex(
name: "IX_SupportAlerts_Status",
schema: "ops",
table: "SupportAlerts",
column: "Status");
migrationBuilder.CreateIndex(
name: "IX_SupportAlerts_Type",
schema: "ops",
table: "SupportAlerts",
column: "Type");
migrationBuilder.CreateIndex(
name: "IX_SystemEvents_Name",
schema: "ops",
table: "SystemEvents",
column: "Name");
migrationBuilder.CreateIndex(
name: "IX_SystemEvents_OccurredAt",
schema: "ops",
table: "SystemEvents",
column: "OccurredAt");
migrationBuilder.CreateIndex(
name: "IX_SystemEvents_UserId",
schema: "ops",
table: "SystemEvents",
column: "UserId");
}
/// <inheritdoc />
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "AuditLogs",
schema: "ops");
migrationBuilder.DropTable(
name: "IranianHolidays",
schema: "ops");
migrationBuilder.DropTable(
name: "Notifications",
schema: "ops");
migrationBuilder.DropTable(
name: "PlatformConfigs",
schema: "ops");
migrationBuilder.DropTable(
name: "SupportAlerts",
schema: "ops");
migrationBuilder.DropTable(
name: "SystemEvents",
schema: "ops");
}
}
}
@@ -22,6 +22,463 @@ namespace Baya.Infrastructure.Persistence.Migrations
SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder);
modelBuilder.Entity("Baya.Domain.Entities.Analytics.SystemEvent", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Name")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTimeOffset>("OccurredAt")
.HasColumnType("datetimeoffset");
b.Property<string>("PropsJson")
.HasColumnType("nvarchar(max)");
b.Property<int?>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("Name");
b.HasIndex("OccurredAt");
b.HasIndex("UserId");
b.ToTable("SystemEvents", "ops");
});
modelBuilder.Entity("Baya.Domain.Entities.Audit.AuditLog", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Action")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<int?>("ActorUserId")
.HasColumnType("int");
b.Property<string>("ChangedFieldsJson")
.HasColumnType("nvarchar(max)");
b.Property<string>("EntityId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("EntityType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTimeOffset>("OccurredAt")
.HasColumnType("datetimeoffset");
b.HasKey("Id");
b.HasIndex("ActorUserId");
b.HasIndex("OccurredAt");
b.HasIndex("EntityType", "EntityId");
b.ToTable("AuditLogs", "ops");
});
modelBuilder.Entity("Baya.Domain.Entities.Configuration.PlatformConfig", 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>("DataType")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("Description")
.HasMaxLength(500)
.HasColumnType("nvarchar(500)");
b.Property<string>("Key")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTimeOffset?>("ModifiedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("ModifiedById")
.HasColumnType("int");
b.Property<string>("Value")
.IsRequired()
.HasColumnType("nvarchar(max)");
b.HasKey("Id");
b.HasIndex("Key")
.IsUnique();
b.ToTable("PlatformConfigs", "ops");
b.HasData(
new
{
Id = 1L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "decimal",
Description = "Balinyaar commission rate on the booking gross (fraction).",
Key = "platform_fee_rate",
Value = "0.15"
},
new
{
Id = 2L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "decimal",
Description = "VAT rate applied to the commission line only (fraction).",
Key = "vat_rate",
Value = "0.10"
},
new
{
Id = 3L,
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 after check-out a booking can be disputed.",
Key = "dispute_window_hours",
Value = "72"
},
new
{
Id = 4L,
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 a family has to pay before a pending booking expires.",
Key = "booking_payment_deadline_minutes",
Value = "30"
},
new
{
Id = 5L,
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 a nurse has to accept/decline a booking request.",
Key = "nurse_response_deadline_hours",
Value = "24"
},
new
{
Id = 6L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "int",
Description = "Weekly payout cadence in days.",
Key = "nurse_payout_interval_days",
Value = "7"
},
new
{
Id = 7L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "int",
Description = "Allowed EVV check-in distance from the care address.",
Key = "evv_location_tolerance_meters",
Value = "200"
},
new
{
Id = 8L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "decimal",
Description = "A review at or below this rating raises a support alert.",
Key = "min_rating_for_support_alert",
Value = "2"
},
new
{
Id = 9L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "string",
Description = "Who is merchant of record for BNPL orders (platform|nurse).",
Key = "bnpl_merchant_of_record",
Value = "platform"
},
new
{
Id = 10L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "decimal",
Description = "BNPL provider commission rate (fraction).",
Key = "bnpl_provider_commission_rate",
Value = "0.07"
},
new
{
Id = 11L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "string",
Description = "When BNPL settles funds to the platform (immediate|deferred).",
Key = "bnpl_settlement_timing",
Value = "immediate"
},
new
{
Id = 12L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
DataType = "json",
Description = "Tiered cancellation refund policy: refund_percent by hours before the visit.",
Key = "cancellation_tiers",
Value = "[{\"min_hours_before\":48,\"refund_percent\":100},{\"min_hours_before\":24,\"refund_percent\":50},{\"min_hours_before\":0,\"refund_percent\":0}]"
});
});
modelBuilder.Entity("Baya.Domain.Entities.Holidays.IranianHoliday", 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<DateOnly>("HolidayDate")
.HasColumnType("date");
b.Property<bool>("IsBankClosed")
.HasColumnType("bit");
b.Property<DateTimeOffset?>("ModifiedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("ModifiedById")
.HasColumnType("int");
b.Property<string>("NameFa")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.HasKey("Id");
b.HasIndex("HolidayDate")
.IsUnique();
b.ToTable("IranianHolidays", "ops");
b.HasData(
new
{
Id = 1L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
HolidayDate = new DateOnly(2026, 2, 11),
IsBankClosed = true,
NameFa = "پیروزی انقلاب اسلامی",
Type = "national"
},
new
{
Id = 2L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
HolidayDate = new DateOnly(2026, 3, 21),
IsBankClosed = true,
NameFa = "نوروز",
Type = "national"
},
new
{
Id = 3L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
HolidayDate = new DateOnly(2026, 3, 22),
IsBankClosed = true,
NameFa = "نوروز",
Type = "national"
},
new
{
Id = 4L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
HolidayDate = new DateOnly(2026, 3, 23),
IsBankClosed = true,
NameFa = "نوروز",
Type = "national"
},
new
{
Id = 5L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
HolidayDate = new DateOnly(2026, 3, 24),
IsBankClosed = true,
NameFa = "نوروز",
Type = "national"
},
new
{
Id = 6L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
HolidayDate = new DateOnly(2026, 4, 1),
IsBankClosed = true,
NameFa = "روز طبیعت (سیزده‌به‌در)",
Type = "official"
},
new
{
Id = 7L,
CreatedAt = new DateTimeOffset(new DateTime(2026, 1, 1, 0, 0, 0, 0, DateTimeKind.Unspecified), new TimeSpan(0, 0, 0, 0, 0)),
HolidayDate = new DateOnly(2026, 6, 26),
IsBankClosed = true,
NameFa = "عید سعید قربان",
Type = "religious"
});
});
modelBuilder.Entity("Baya.Domain.Entities.Notifications.Notification", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<string>("Body")
.HasColumnType("nvarchar(max)");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("datetimeoffset");
b.Property<string>("DataJson")
.HasColumnType("nvarchar(max)");
b.Property<bool>("IsRead")
.ValueGeneratedOnAdd()
.HasColumnType("bit")
.HasDefaultValue(false);
b.Property<DateTimeOffset?>("ReadAt")
.HasColumnType("datetimeoffset");
b.Property<string>("Title")
.IsRequired()
.HasMaxLength(200)
.HasColumnType("nvarchar(200)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<int>("UserId")
.HasColumnType("int");
b.HasKey("Id");
b.HasIndex("UserId", "IsRead", "CreatedAt");
b.ToTable("Notifications", "ops");
});
modelBuilder.Entity("Baya.Domain.Entities.SupportAlerts.SupportAlert", b =>
{
b.Property<long>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("bigint");
SqlServerPropertyBuilderExtensions.UseIdentityColumn(b.Property<long>("Id"));
b.Property<long?>("BookingId")
.HasColumnType("bigint");
b.Property<DateTimeOffset>("CreatedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("CreatedById")
.HasColumnType("int");
b.Property<string>("EntityId")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<string>("EntityType")
.IsRequired()
.HasMaxLength(100)
.HasColumnType("nvarchar(100)");
b.Property<DateTimeOffset?>("ModifiedAt")
.HasColumnType("datetimeoffset");
b.Property<int?>("ModifiedById")
.HasColumnType("int");
b.Property<int?>("OwnerUserId")
.HasColumnType("int");
b.Property<string>("ResolutionNote")
.HasColumnType("nvarchar(max)");
b.Property<DateTimeOffset?>("ResolvedAt")
.HasColumnType("datetimeoffset");
b.Property<long?>("ReviewId")
.HasColumnType("bigint");
b.Property<string>("Severity")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("Status")
.IsRequired()
.HasMaxLength(20)
.HasColumnType("nvarchar(20)");
b.Property<string>("Type")
.IsRequired()
.HasMaxLength(40)
.HasColumnType("nvarchar(40)");
b.HasKey("Id");
b.HasIndex("OwnerUserId");
b.HasIndex("Status");
b.HasIndex("Type");
b.ToTable("SupportAlerts", "ops");
});
modelBuilder.Entity("Baya.Domain.Entities.User.Role", b =>
{
b.Property<int>("Id")
@@ -282,6 +739,36 @@ namespace Baya.Infrastructure.Persistence.Migrations
b.ToTable("UserTokens", "usr");
});
modelBuilder.Entity("Baya.Domain.Entities.Analytics.SystemEvent", b =>
{
b.HasOne("Baya.Domain.Entities.User.User", null)
.WithMany()
.HasForeignKey("UserId");
});
modelBuilder.Entity("Baya.Domain.Entities.Audit.AuditLog", b =>
{
b.HasOne("Baya.Domain.Entities.User.User", null)
.WithMany()
.HasForeignKey("ActorUserId");
});
modelBuilder.Entity("Baya.Domain.Entities.Notifications.Notification", b =>
{
b.HasOne("Baya.Domain.Entities.User.User", null)
.WithMany()
.HasForeignKey("UserId")
.OnDelete(DeleteBehavior.Restrict)
.IsRequired();
});
modelBuilder.Entity("Baya.Domain.Entities.SupportAlerts.SupportAlert", b =>
{
b.HasOne("Baya.Domain.Entities.User.User", null)
.WithMany()
.HasForeignKey("OwnerUserId");
});
modelBuilder.Entity("Baya.Domain.Entities.User.RoleClaim", b =>
{
b.HasOne("Baya.Domain.Entities.User.Role", "Role")