From fccdd63aba50b74d91f000de722021108d45dab8 Mon Sep 17 00:00:00 2001 From: Jerry Phillips Date: Sat, 25 Apr 2026 14:36:04 -0400 Subject: [PATCH 1/2] feat(api): [AB#97] org milestone tracking fields and endpoint --- JobFlow.API/Controllers/EstimateController.cs | 9 + .../Controllers/OrganizationController.cs | 10 + .../Controllers/SetupCompanionController.cs | 38 + JobFlow.API/Models/OrganizationDtos.cs | 3 + .../Models/DTOs/OrganizationDto.cs | 2 + JobFlow.Business/Services/EstimateService.cs | 65 + .../Services/OrganizationService.cs | 24 + .../ServiceInterfaces/IEstimateService.cs | 1 + .../ServiceInterfaces/IOrganizationService.cs | 1 + .../ISetupCompanionService.cs | 8 + .../Services/SetupCompanionService.cs | 33 + JobFlow.Domain/Models/Organization.cs | 4 + JobFlow.Domain/Models/SetupCompanionEvent.cs | 10 + .../JobFlowDbContext.cs | 1 + ..._Sprint4_MilestoneAndCompanion.Designer.cs | 6330 +++++++++++++++++ ...425182801_Sprint4_MilestoneAndCompanion.cs | 64 + .../JobFlowDbContextModelSnapshot.cs | 52 + 17 files changed, 6655 insertions(+) create mode 100644 JobFlow.API/Controllers/SetupCompanionController.cs create mode 100644 JobFlow.API/Models/OrganizationDtos.cs create mode 100644 JobFlow.Business/Services/ServiceInterfaces/ISetupCompanionService.cs create mode 100644 JobFlow.Business/Services/SetupCompanionService.cs create mode 100644 JobFlow.Domain/Models/SetupCompanionEvent.cs create mode 100644 JobFlow.Infrastructure.Persistence/Migrations/20260425182801_Sprint4_MilestoneAndCompanion.Designer.cs create mode 100644 JobFlow.Infrastructure.Persistence/Migrations/20260425182801_Sprint4_MilestoneAndCompanion.cs diff --git a/JobFlow.API/Controllers/EstimateController.cs b/JobFlow.API/Controllers/EstimateController.cs index 1a8ffdb..8bde22a 100644 --- a/JobFlow.API/Controllers/EstimateController.cs +++ b/JobFlow.API/Controllers/EstimateController.cs @@ -97,6 +97,15 @@ public async Task GetPublicPdf(string token) return File(result.Value, "application/pdf", $"estimate-{token}.pdf"); } + [HttpPost("first-win")] + [Authorize] + public async Task CreateFirstWin() + { + var organizationId = HttpContext.GetOrganizationId(); + var result = await estimateService.CreateFirstWinAsync(organizationId); + return result.IsSuccess ? Ok(result.Value) : ProblemFrom(result); + } + private ObjectResult ProblemFrom(Result result) { var problem = result.ToProblemDetails() as Microsoft.AspNetCore.Http.HttpResults.ProblemHttpResult; diff --git a/JobFlow.API/Controllers/OrganizationController.cs b/JobFlow.API/Controllers/OrganizationController.cs index 04918db..050ee43 100644 --- a/JobFlow.API/Controllers/OrganizationController.cs +++ b/JobFlow.API/Controllers/OrganizationController.cs @@ -333,4 +333,14 @@ private async Task CreateOwnerEmployeeAsync(OrganizationRegisterDto model, User model.Id, user.Id); } } + + [HttpPost] + [Route("milestones")] + [Authorize] + public async Task MarkMilestone([FromBody] MarkMilestoneRequest request) + { + var organizationId = HttpContext.GetOrganizationId(); + var result = await _organizationService.MarkMilestoneAsync(organizationId, request.Milestone); + return result.IsSuccess ? Results.Ok() : result.ToProblemDetails(); + } } \ No newline at end of file diff --git a/JobFlow.API/Controllers/SetupCompanionController.cs b/JobFlow.API/Controllers/SetupCompanionController.cs new file mode 100644 index 0000000..b625d87 --- /dev/null +++ b/JobFlow.API/Controllers/SetupCompanionController.cs @@ -0,0 +1,38 @@ +using JobFlow.API.Extensions; +using JobFlow.Business.Extensions; +using JobFlow.Business.Services.ServiceInterfaces; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; + +namespace JobFlow.API.Controllers; + +[ApiController] +[Route("api/setup-companion")] +[Authorize] +public class SetupCompanionController : ControllerBase +{ + private readonly ISetupCompanionService _setupCompanionService; + + public SetupCompanionController(ISetupCompanionService setupCompanionService) + { + _setupCompanionService = setupCompanionService; + } + + [HttpPost("events")] + public async Task TrackEvent([FromBody] TrackSetupCompanionEventRequest request) + { + var organizationId = HttpContext.GetOrganizationId(); + var result = await _setupCompanionService.TrackEventAsync( + organizationId, + request.SessionId, + request.QuestionKey, + request.AnswerKey); + + return result.IsSuccess ? Results.Ok() : result.ToProblemDetails(); + } +} + +public sealed record TrackSetupCompanionEventRequest( + string SessionId, + string QuestionKey, + string? AnswerKey); diff --git a/JobFlow.API/Models/OrganizationDtos.cs b/JobFlow.API/Models/OrganizationDtos.cs new file mode 100644 index 0000000..95b6fa7 --- /dev/null +++ b/JobFlow.API/Models/OrganizationDtos.cs @@ -0,0 +1,3 @@ +namespace JobFlow.API.Models; + +public sealed record MarkMilestoneRequest(string Milestone); diff --git a/JobFlow.Business/Models/DTOs/OrganizationDto.cs b/JobFlow.Business/Models/DTOs/OrganizationDto.cs index fa3504f..eba9cc7 100644 --- a/JobFlow.Business/Models/DTOs/OrganizationDto.cs +++ b/JobFlow.Business/Models/DTOs/OrganizationDto.cs @@ -54,4 +54,6 @@ public class OrganizationDto public bool IsSquareConnected { get; set; } public bool PaymentSetupDeferred { get; set; } public OrgSize OrgSize { get; set; } + public DateTimeOffset? FirstRealEstimateSentAt { get; set; } + public DateTimeOffset? ReferralCtaShownAt { get; set; } } \ No newline at end of file diff --git a/JobFlow.Business/Services/EstimateService.cs b/JobFlow.Business/Services/EstimateService.cs index d5b7d44..fc88036 100644 --- a/JobFlow.Business/Services/EstimateService.cs +++ b/JobFlow.Business/Services/EstimateService.cs @@ -330,6 +330,71 @@ private static string GeneratePublicToken() return Convert.ToHexString(bytes).ToLowerInvariant(); } + public async Task> CreateFirstWinAsync(Guid organizationId) + { + var org = await unitOfWork.RepositoryOf() + .Query() + .FirstOrDefaultAsync(o => o.Id == organizationId); + + if (org == null) + return Result.Failure(EstimateErrors.NotFound); + + var ownerEmail = org.EmailAddress ?? string.Empty; + + // Find or create a self-client record for the org owner + var selfClient = await clients.Query() + .FirstOrDefaultAsync(c => c.OrganizationId == organizationId && c.EmailAddress == ownerEmail); + + if (selfClient == null) + { + selfClient = new OrganizationClient + { + OrganizationId = organizationId, + FirstName = org.ContactFirstName ?? "Business", + LastName = org.ContactLastName ?? "Owner", + EmailAddress = ownerEmail + }; + await clients.AddAsync(selfClient); + await unitOfWork.SaveChangesAsync(); + } + + var estimateNumber = await _numberGenerator.GenerateAsync(organizationId); + + var demoLineItems = new List + { + new() { Name = "Initial Consultation", Description = "On-site assessment and project evaluation", Quantity = 1m, UnitPrice = 150m, Total = 150m }, + new() { Name = "Labor", Description = "Professional service — 2 hours", Quantity = 2m, UnitPrice = 75m, Total = 150m }, + new() { Name = "Materials & Supplies", Description = "Standard supplies for the project", Quantity = 1m, UnitPrice = 85m, Total = 85m } + }; + + var estimate = new Estimate + { + OrganizationId = organizationId, + OrganizationClientId = selfClient.Id, + EstimateNumber = estimateNumber, + Title = "Sample Estimate — First Win", + Status = EstimateStatus.Sent, + PublicToken = GeneratePublicToken(), + PublicTokenExpiresAt = DateTimeOffset.UtcNow.AddDays(30), + SentAt = DateTimeOffset.UtcNow, + LineItems = demoLineItems + }; + + RecalculateTotals(estimate); + await estimates.AddAsync(estimate); + await unitOfWork.SaveChangesAsync(); + + selfClient.Organization = org; + await notificationService.SendClientEstimateSentNotificationAsync(selfClient, estimate); + + var full = await estimates.Query() + .Include(x => x.OrganizationClient) + .Include(x => x.LineItems) + .FirstOrDefaultAsync(x => x.Id == estimate.Id); + + return Result.Success(ToDto(full!)); + } + private static EstimateDto ToDto(Estimate e) => new( e.Id, diff --git a/JobFlow.Business/Services/OrganizationService.cs b/JobFlow.Business/Services/OrganizationService.cs index d2ef686..8d34477 100644 --- a/JobFlow.Business/Services/OrganizationService.cs +++ b/JobFlow.Business/Services/OrganizationService.cs @@ -248,4 +248,28 @@ public async Task SetOrgSizeAsync(Guid organizationId, string? orgSizeVa await _unitOfWork.SaveChangesAsync(); return Result.Success(); } + + public async Task MarkMilestoneAsync(Guid organizationId, string milestone) + { + var org = await _unitOfWork.RepositoryOf().GetByIdAsync(organizationId); + if (org == null) + return Result.Failure(OrganizationErrors.OrganizationNotFound); + + var now = DateTimeOffset.UtcNow; + switch (milestone.Trim().ToLowerInvariant()) + { + case "firstrealestimatesentat": + org.FirstRealEstimateSentAt ??= now; + break; + case "referralctashownat": + org.ReferralCtaShownAt ??= now; + break; + default: + return Result.Failure(Error.Failure("Organization.Milestone", $"Unknown milestone: {milestone}")); + } + + _unitOfWork.RepositoryOf().Update(org); + await _unitOfWork.SaveChangesAsync(); + return Result.Success(); + } } \ No newline at end of file diff --git a/JobFlow.Business/Services/ServiceInterfaces/IEstimateService.cs b/JobFlow.Business/Services/ServiceInterfaces/IEstimateService.cs index 740657e..d346b1d 100644 --- a/JobFlow.Business/Services/ServiceInterfaces/IEstimateService.cs +++ b/JobFlow.Business/Services/ServiceInterfaces/IEstimateService.cs @@ -19,4 +19,5 @@ public interface IEstimateService Task> AcceptAsync(Guid id, Guid organizationId, Guid organizationClientId); Task> DeclineAsync(Guid id, Guid organizationId, Guid organizationClientId); + Task> CreateFirstWinAsync(Guid organizationId); } \ No newline at end of file diff --git a/JobFlow.Business/Services/ServiceInterfaces/IOrganizationService.cs b/JobFlow.Business/Services/ServiceInterfaces/IOrganizationService.cs index 75db9e7..48a49d0 100644 --- a/JobFlow.Business/Services/ServiceInterfaces/IOrganizationService.cs +++ b/JobFlow.Business/Services/ServiceInterfaces/IOrganizationService.cs @@ -17,4 +17,5 @@ public interface IOrganizationService Task UpdateSubscriptionStateAsync(Guid organizationId, string? subscriptionStatus, string? subscriptionPlanName = null, DateTime? subscriptionExpiresAt = null); Task DeleteOrganization(Guid organizationId); Task SetOrgSizeAsync(Guid organizationId, string? orgSizeValue); + Task MarkMilestoneAsync(Guid organizationId, string milestone); } \ No newline at end of file diff --git a/JobFlow.Business/Services/ServiceInterfaces/ISetupCompanionService.cs b/JobFlow.Business/Services/ServiceInterfaces/ISetupCompanionService.cs new file mode 100644 index 0000000..bb6e787 --- /dev/null +++ b/JobFlow.Business/Services/ServiceInterfaces/ISetupCompanionService.cs @@ -0,0 +1,8 @@ +using JobFlow.Business; + +namespace JobFlow.Business.Services.ServiceInterfaces; + +public interface ISetupCompanionService +{ + Task TrackEventAsync(Guid organizationId, string sessionId, string questionKey, string? answerKey); +} diff --git a/JobFlow.Business/Services/SetupCompanionService.cs b/JobFlow.Business/Services/SetupCompanionService.cs new file mode 100644 index 0000000..8479259 --- /dev/null +++ b/JobFlow.Business/Services/SetupCompanionService.cs @@ -0,0 +1,33 @@ +using JobFlow.Business.DI; +using JobFlow.Business.Services.ServiceInterfaces; +using JobFlow.Domain; +using JobFlow.Domain.Models; + +namespace JobFlow.Business.Services; + +[ScopedService] +public class SetupCompanionService : ISetupCompanionService +{ + private readonly IUnitOfWork _unitOfWork; + + public SetupCompanionService(IUnitOfWork unitOfWork) + { + _unitOfWork = unitOfWork; + } + + public async Task TrackEventAsync(Guid organizationId, string sessionId, string questionKey, string? answerKey) + { + var ev = new SetupCompanionEvent + { + OrganizationId = organizationId, + SessionId = sessionId, + QuestionKey = questionKey, + AnswerKey = answerKey, + OccurredAt = DateTimeOffset.UtcNow + }; + + await _unitOfWork.RepositoryOf().AddAsync(ev); + await _unitOfWork.SaveChangesAsync(); + return Result.Success(); + } +} diff --git a/JobFlow.Domain/Models/Organization.cs b/JobFlow.Domain/Models/Organization.cs index fb3ec6d..e1c6e37 100644 --- a/JobFlow.Domain/Models/Organization.cs +++ b/JobFlow.Domain/Models/Organization.cs @@ -35,6 +35,10 @@ public class Organization : Entity public DateTimeOffset? PaymentSetupSkippedAt { get; set; } public OrgSize OrgSize { get; set; } = OrgSize.Solo; + // Sprint 4 milestones + public DateTimeOffset? FirstRealEstimateSentAt { get; set; } + public DateTimeOffset? ReferralCtaShownAt { get; set; } + public bool CanAcceptPayments => (PaymentProvider == PaymentProvider.Stripe && !string.IsNullOrWhiteSpace(StripeConnectAccountId)) || diff --git a/JobFlow.Domain/Models/SetupCompanionEvent.cs b/JobFlow.Domain/Models/SetupCompanionEvent.cs new file mode 100644 index 0000000..9713c3d --- /dev/null +++ b/JobFlow.Domain/Models/SetupCompanionEvent.cs @@ -0,0 +1,10 @@ +namespace JobFlow.Domain.Models; + +public class SetupCompanionEvent : Entity +{ + public Guid OrganizationId { get; set; } + public string SessionId { get; set; } = string.Empty; + public string QuestionKey { get; set; } = string.Empty; + public string? AnswerKey { get; set; } + public DateTimeOffset OccurredAt { get; set; } = DateTimeOffset.UtcNow; +} diff --git a/JobFlow.Infrastructure.Persistence/JobFlowDbContext.cs b/JobFlow.Infrastructure.Persistence/JobFlowDbContext.cs index 01beda4..b676738 100644 --- a/JobFlow.Infrastructure.Persistence/JobFlowDbContext.cs +++ b/JobFlow.Infrastructure.Persistence/JobFlowDbContext.cs @@ -46,6 +46,7 @@ public JobFlowDbContext(DbContextOptions options) : base(options) public DbSet SupportChatSessions { get; set; } public DbSet SupportChatMessages { get; set; } public DbSet OrganizationOnboardingEvents { get; set; } + public DbSet SetupCompanionEvents { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/JobFlow.Infrastructure.Persistence/Migrations/20260425182801_Sprint4_MilestoneAndCompanion.Designer.cs b/JobFlow.Infrastructure.Persistence/Migrations/20260425182801_Sprint4_MilestoneAndCompanion.Designer.cs new file mode 100644 index 0000000..b1b3cc1 --- /dev/null +++ b/JobFlow.Infrastructure.Persistence/Migrations/20260425182801_Sprint4_MilestoneAndCompanion.Designer.cs @@ -0,0 +1,6330 @@ +// +using System; +using JobFlow.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 JobFlow.Infrastructure.Persistence.Migrations +{ + [DbContext(typeof(JobFlowDbContext))] + [Migration("20260425182801_Sprint4_MilestoneAndCompanion")] + partial class Sprint4_MilestoneAndCompanion + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.7") + .HasAnnotation("Relational:MaxIdentifierLength", 128); + + SqlServerModelBuilderExtensions.UseIdentityColumns(modelBuilder); + + modelBuilder.Entity("JobFlow.Domain.Models.Assignment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ActualEnd") + .HasColumnType("datetimeoffset"); + + b.Property("ActualStart") + .HasColumnType("datetimeoffset"); + + b.Property("Address1") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("uniqueidentifier"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("ScheduleType") + .HasColumnType("int"); + + b.Property("ScheduledEnd") + .HasColumnType("datetimeoffset"); + + b.Property("ScheduledStart") + .HasColumnType("datetimeoffset"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ZipCode") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("ScheduledStart"); + + b.ToTable("Assignment", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.AssignmentAssignee", b => + { + b.Property("AssignmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("EmployeeId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("IsLead") + .HasColumnType("bit"); + + b.HasKey("AssignmentId", "EmployeeId"); + + b.HasIndex("EmployeeId") + .HasDatabaseName("IX_AssignmentAssignee_EmployeeId"); + + b.ToTable("AssignmentAssignee", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.AssignmentHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssignmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("ChangedAt") + .HasColumnType("datetime2"); + + b.Property("ChangedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("EventType") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("NewScheduledEnd") + .HasColumnType("datetime2"); + + b.Property("NewScheduledStart") + .HasColumnType("datetime2"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OldScheduledEnd") + .HasColumnType("datetime2"); + + b.Property("OldScheduledStart") + .HasColumnType("datetime2"); + + b.Property("Reason") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AssignmentId") + .HasDatabaseName("IX_AssignmentHistory_AssignmentId"); + + b.ToTable("AssignmentHistory", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.AssignmentOrder", b => + { + b.Property("AssignmentId") + .HasColumnType("uniqueidentifier"); + + b.Property("OrderId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.HasKey("AssignmentId", "OrderId"); + + b.HasIndex("AssignmentId"); + + b.HasIndex("OrderId"); + + b.ToTable("AssignmentOrder", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.AuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Action") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("nvarchar(80)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DetailsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("IpAddress") + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Method") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Path") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("ResourceId") + .HasMaxLength(80) + .HasColumnType("nvarchar(80)"); + + b.Property("ResourceType") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("StatusCode") + .HasColumnType("int"); + + b.Property("Success") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UserAgent") + .HasMaxLength(512) + .HasColumnType("nvarchar(512)"); + + b.Property("UserId") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("OrganizationId", "CreatedAt"); + + b.HasIndex("UserId", "CreatedAt"); + + b.ToTable("AuditLog", "security"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.ChangelogEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("PublishedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Version") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.HasKey("Id"); + + b.HasIndex("IsPublished"); + + b.ToTable("ChangelogEntries"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.ClientImportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CompletedAtUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("FailedRows") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProcessedRows") + .HasColumnType("int"); + + b.Property("SourceSystem") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("StartedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("SucceededRows") + .HasColumnType("int"); + + b.Property("TotalRows") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Status"); + + b.HasIndex("OrganizationId", "CreatedAt"); + + b.ToTable("ClientImportJob", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.ClientImportJobError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ClientImportJobId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("RowNumber") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ClientImportJobId"); + + b.HasIndex("RowNumber"); + + b.ToTable("ClientImportJobError", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.ClientImportUploadRow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ClientImportUploadSessionId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("RowDataJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RowNumber") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ClientImportUploadSessionId", "RowNumber") + .IsUnique(); + + b.ToTable("ClientImportUploadRow", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.ClientImportUploadSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConsumedAtUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ExpiresAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SourceSystem") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("TotalRows") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Status", "ExpiresAtUtc"); + + b.ToTable("ClientImportUploadSession", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Conversation", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationClientId"); + + b.ToTable("Conversation", "messaging"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.ConversationParticipant", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConversationId") + .HasColumnType("uniqueidentifier"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.HasIndex("ConversationId", "UserId") + .IsUnique(); + + b.ToTable("ConversationParticipant", "messaging"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.CustomerPaymentProfile", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DefaultPaymentMethodId") + .HasColumnType("nvarchar(max)"); + + b.Property("EncryptedAccessToken") + .HasColumnType("nvarchar(max)"); + + b.Property("EncryptedRefreshToken") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsDelinquent") + .HasColumnType("bit"); + + b.Property("OrganizationClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("OwnerId") + .HasColumnType("uniqueidentifier"); + + b.Property("OwnerType") + .HasColumnType("int"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ProviderCustomerId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SquareLocationId") + .HasColumnType("nvarchar(max)"); + + b.Property("TokenExpiresAtUtc") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationClientId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("CustomerPaymentProfile", "payment"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.DataExportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CompletedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ContentType") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DownloadCount") + .HasColumnType("int"); + + b.Property("ErrorMessage") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("ExpiresAtUtc") + .HasColumnType("datetime2"); + + b.Property("FileContent") + .HasColumnType("varbinary(max)"); + + b.Property("FileName") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("RequestedByUserId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "CreatedAt"); + + b.HasIndex("OrganizationId", "Status"); + + b.ToTable("DataExportJob", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Employee", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("HireDate") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("JobTitle") + .HasColumnType("nvarchar(max)"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PhoneNumber") + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("ProfilePictureUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.Property("TerminationDate") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("RoleId"); + + b.HasIndex("UserId"); + + b.ToTable("Employees", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeImportJob", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CompletedAtUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ErrorMessage") + .HasColumnType("nvarchar(max)"); + + b.Property("FailedRows") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("ProcessedRows") + .HasColumnType("int"); + + b.Property("SourceSystem") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StartedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SucceededRows") + .HasColumnType("int"); + + b.Property("TotalRows") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("EmployeeImportJobs"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeImportJobError", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("EmployeeImportJobId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Message") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RowNumber") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeImportJobId"); + + b.ToTable("EmployeeImportJobErrors"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeImportUploadRow", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("EmployeeImportUploadSessionId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("RowDataJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("RowNumber") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeImportUploadSessionId"); + + b.ToTable("EmployeeImportUploadRows"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeImportUploadSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ConsumedAtUtc") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ExpiresAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SourceSystem") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TotalRows") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("EmployeeImportUploadSessions"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("AccessIpAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("AccessedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Email") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("ExpiresAt") + .HasColumnType("datetime2"); + + b.Property("FirstName") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("InviteToken") + .HasMaxLength(128) + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LastName") + .IsRequired() + .HasMaxLength(30) + .HasColumnType("nvarchar(30)"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PhoneNumber") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.Property("ShortCode") + .IsRequired() + .HasColumnType("nvarchar(450)"); + + b.Property("Status") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(1); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("InviteToken") + .IsUnique(); + + b.HasIndex("OrganizationId"); + + b.HasIndex("RoleId"); + + b.HasIndex("ShortCode") + .IsUnique() + .HasFilter("[ShortCode] IS NOT NULL"); + + b.HasIndex("OrganizationId", "Email") + .IsUnique() + .HasFilter("[Status] = 1"); + + b.HasIndex("Status", "ExpiresAt"); + + b.ToTable("EmployeeInvites", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Description") + .HasMaxLength(240) + .HasColumnType("nvarchar(240)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("EmployeeRoles", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeRolePreset", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(240) + .HasColumnType("nvarchar(240)"); + + b.Property("IndustryKey") + .HasMaxLength(80) + .HasColumnType("nvarchar(80)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("EmployeeRolePresets", (string)null); + + b.HasData( + new + { + Id = new Guid("1a2b3c4d-1111-1111-1111-111111111111"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Default roles for field service teams.", + IndustryKey = "home-services", + IsActive = true, + IsSystem = true, + Name = "Home services" + }, + new + { + Id = new Guid("1a2b3c4d-2222-2222-2222-222222222222"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Default roles for creative studios.", + IndustryKey = "creative", + IsActive = true, + IsSystem = true, + Name = "Creative" + }, + new + { + Id = new Guid("1a2b3c4d-3333-3333-3333-333333333333"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Default roles for consulting teams.", + IndustryKey = "consulting", + IsActive = true, + IsSystem = true, + Name = "Consulting" + }, + new + { + Id = new Guid("1a2b3c4d-4444-4444-4444-444444444444"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Default roles for repair shops.", + IndustryKey = "tech-repair", + IsActive = true, + IsSystem = true, + Name = "Tech repair" + }); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeRolePresetItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(240) + .HasColumnType("nvarchar(240)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("PresetId") + .HasColumnType("uniqueidentifier"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PresetId"); + + b.ToTable("EmployeeRolePresetItems", (string)null); + + b.HasData( + new + { + Id = new Guid("2a2b3c4d-1111-1111-1111-111111111111"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Field technician for on-site work.", + IsActive = true, + Name = "Technician", + PresetId = new Guid("1a2b3c4d-1111-1111-1111-111111111111"), + SortOrder = 1 + }, + new + { + Id = new Guid("2a2b3c4d-1111-1111-1111-111111111112"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Lead for quality checks and approvals.", + IsActive = true, + Name = "Supervisor", + PresetId = new Guid("1a2b3c4d-1111-1111-1111-111111111111"), + SortOrder = 2 + }, + new + { + Id = new Guid("2a2b3c4d-1111-1111-1111-111111111113"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Routes schedules and job assignments.", + IsActive = true, + Name = "Dispatcher", + PresetId = new Guid("1a2b3c4d-1111-1111-1111-111111111111"), + SortOrder = 3 + }, + new + { + Id = new Guid("2a2b3c4d-1111-1111-1111-111111111114"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Back-office support and billing.", + IsActive = true, + Name = "Admin", + PresetId = new Guid("1a2b3c4d-1111-1111-1111-111111111111"), + SortOrder = 4 + }, + new + { + Id = new Guid("2a2b3c4d-2222-2222-2222-222222222221"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Primary creator and deliverable owner.", + IsActive = true, + Name = "Designer", + PresetId = new Guid("1a2b3c4d-2222-2222-2222-222222222222"), + SortOrder = 1 + }, + new + { + Id = new Guid("2a2b3c4d-2222-2222-2222-222222222222"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Owns timelines, approvals, and client comms.", + IsActive = true, + Name = "Producer", + PresetId = new Guid("1a2b3c4d-2222-2222-2222-222222222222"), + SortOrder = 2 + }, + new + { + Id = new Guid("2a2b3c4d-2222-2222-2222-222222222223"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Schedules tasks and supports delivery.", + IsActive = true, + Name = "Coordinator", + PresetId = new Guid("1a2b3c4d-2222-2222-2222-222222222222"), + SortOrder = 3 + }, + new + { + Id = new Guid("2a2b3c4d-2222-2222-2222-222222222224"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Operations and billing support.", + IsActive = true, + Name = "Admin", + PresetId = new Guid("1a2b3c4d-2222-2222-2222-222222222222"), + SortOrder = 4 + }, + new + { + Id = new Guid("2a2b3c4d-3333-3333-3333-333333333331"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Client-facing delivery specialist.", + IsActive = true, + Name = "Consultant", + PresetId = new Guid("1a2b3c4d-3333-3333-3333-333333333333"), + SortOrder = 1 + }, + new + { + Id = new Guid("2a2b3c4d-3333-3333-3333-333333333332"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Owns engagement delivery and quality.", + IsActive = true, + Name = "Lead", + PresetId = new Guid("1a2b3c4d-3333-3333-3333-333333333333"), + SortOrder = 2 + }, + new + { + Id = new Guid("2a2b3c4d-3333-3333-3333-333333333333"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Plans meetings and follow-ups.", + IsActive = true, + Name = "Coordinator", + PresetId = new Guid("1a2b3c4d-3333-3333-3333-333333333333"), + SortOrder = 3 + }, + new + { + Id = new Guid("2a2b3c4d-3333-3333-3333-333333333334"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Back-office support and billing.", + IsActive = true, + Name = "Admin", + PresetId = new Guid("1a2b3c4d-3333-3333-3333-333333333333"), + SortOrder = 4 + }, + new + { + Id = new Guid("2a2b3c4d-4444-4444-4444-444444444441"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Executes diagnostics and repairs.", + IsActive = true, + Name = "Repair Tech", + PresetId = new Guid("1a2b3c4d-4444-4444-4444-444444444444"), + SortOrder = 1 + }, + new + { + Id = new Guid("2a2b3c4d-4444-4444-4444-444444444442"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Final testing and release approvals.", + IsActive = true, + Name = "QA", + PresetId = new Guid("1a2b3c4d-4444-4444-4444-444444444444"), + SortOrder = 2 + }, + new + { + Id = new Guid("2a2b3c4d-4444-4444-4444-444444444443"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Client intake and status updates.", + IsActive = true, + Name = "Service Advisor", + PresetId = new Guid("1a2b3c4d-4444-4444-4444-444444444444"), + SortOrder = 3 + }, + new + { + Id = new Guid("2a2b3c4d-4444-4444-4444-444444444444"), + CreatedAt = new DateTime(2026, 3, 23, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Operations and billing support.", + IsActive = true, + Name = "Admin", + PresetId = new Guid("1a2b3c4d-4444-4444-4444-444444444444"), + SortOrder = 4 + }); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Estimate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("EstimateNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Notes") + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PublicToken") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("PublicTokenExpiresAt") + .HasColumnType("datetimeoffset"); + + b.Property("SentAt") + .HasColumnType("datetimeoffset"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Subtotal") + .HasColumnType("decimal(18,2)"); + + b.Property("TaxTotal") + .HasColumnType("decimal(18,2)"); + + b.Property("Title") + .HasColumnType("nvarchar(max)"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationClientId"); + + b.HasIndex("PublicToken") + .IsUnique(); + + b.HasIndex("OrganizationId", "EstimateNumber") + .IsUnique(); + + b.ToTable("Estimates", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EstimateLineItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasColumnType("nvarchar(max)"); + + b.Property("EstimateId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("PriceBookItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("Quantity") + .HasColumnType("decimal(18,2)"); + + b.Property("Total") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("EstimateId"); + + b.HasIndex("PriceBookItemId"); + + b.ToTable("EstimateLineItems", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EstimateRevisionAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("EstimateRevisionRequestId") + .HasColumnType("uniqueidentifier"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("nvarchar(260)"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("EstimateRevisionRequestId"); + + b.ToTable("EstimateRevisionAttachments", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EstimateRevisionRequest", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("EstimateId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("OrganizationResponseMessage") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("RequestMessage") + .IsRequired() + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("RequestedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ResolvedAt") + .HasColumnType("datetimeoffset"); + + b.Property("ReviewedAt") + .HasColumnType("datetimeoffset"); + + b.Property("RevisionNumber") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationClientId"); + + b.HasIndex("EstimateId", "RevisionNumber") + .IsUnique(); + + b.ToTable("EstimateRevisionRequests", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EstimateSequence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Day") + .HasColumnType("int"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LastSequence") + .HasColumnType("int"); + + b.Property("Month") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("EstimateSequence", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.FollowUpExecutionLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AttemptedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Channel") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("FailureReason") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("FollowUpRunId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("ScheduledFor") + .HasColumnType("datetimeoffset"); + + b.Property("StepOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("WasSent") + .HasColumnType("bit"); + + b.HasKey("Id"); + + b.HasIndex("FollowUpRunId", "StepOrder"); + + b.ToTable("FollowUpExecutionLogs", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.FollowUpRun", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CompletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("FollowUpSequenceId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LastAttemptAt") + .HasColumnType("datetimeoffset"); + + b.Property("NextStepOrder") + .HasColumnType("int"); + + b.Property("OrganizationClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SequenceType") + .HasColumnType("int"); + + b.Property("StartedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("StopReason") + .HasColumnType("int"); + + b.Property("TriggerEntityId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("FollowUpSequenceId", "OrganizationClientId", "Status"); + + b.HasIndex("OrganizationId", "SequenceType", "TriggerEntityId", "Status"); + + b.ToTable("FollowUpRuns", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.FollowUpSequence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DefaultChannel") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsEnabled") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SequenceType") + .HasColumnType("int"); + + b.Property("StopOnClientReply") + .HasColumnType("bit"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "SequenceType", "IsActive"); + + b.ToTable("FollowUpSequences", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.FollowUpStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ChannelOverride") + .HasColumnType("int"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DelayHours") + .HasColumnType("int"); + + b.Property("FollowUpSequenceId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsEscalation") + .HasColumnType("bit"); + + b.Property("MessageTemplate") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("StepOrder") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("FollowUpSequenceId", "StepOrder") + .IsUnique(); + + b.ToTable("FollowUpSteps", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.HelpArticle", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ArticleType") + .HasColumnType("int"); + + b.Property("Category") + .HasColumnType("int"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsFeatured") + .HasColumnType("bit"); + + b.Property("IsPublished") + .HasColumnType("bit"); + + b.Property("PublishedAt") + .HasColumnType("datetimeoffset"); + + b.Property("PublishedBy") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Tags") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ArticleType"); + + b.HasIndex("Category"); + + b.HasIndex("IsPublished"); + + b.ToTable("HelpArticles"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.InventoryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("CostPerUnit") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("QuantityInStock") + .HasColumnType("int"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("InventoryItems", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Invoice", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AmountPaid") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("AmountRefunded") + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + b.Property("EstimateId") + .HasColumnType("uniqueidentifier"); + + b.Property("ExternalPaymentId") + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceDate") + .HasColumnType("datetime2"); + + b.Property("InvoiceNumber") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("uniqueidentifier"); + + b.Property("OrderId") + .HasColumnType("uniqueidentifier"); + + b.Property("OrganizationClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PaidAt") + .HasColumnType("datetimeoffset"); + + b.Property("PaymentProvider") + .HasColumnType("int"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("TotalAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobId"); + + b.HasIndex("OrderId"); + + b.HasIndex("OrganizationClientId"); + + b.HasIndex("OrganizationId", "CreatedAt"); + + b.HasIndex("OrganizationId", "Status"); + + b.ToTable("Invoice"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.InvoiceLineItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("PriceBookItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("Quantity") + .HasColumnType("decimal(18,2)"); + + b.Property("UnitPrice") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("PriceBookItemId"); + + b.ToTable("InvoiceLineItem", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.InvoiceSequence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LastSequence") + .HasColumnType("int"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Year") + .HasColumnType("int"); + + b.HasKey("Id"); + + b.ToTable("InvoiceSequence", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Job", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Comments") + .HasMaxLength(2000) + .HasColumnType("nvarchar(2000)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("EstimateId") + .HasColumnType("uniqueidentifier"); + + b.Property("InvoicingWorkflow") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Latitude") + .HasColumnType("float"); + + b.Property("LifecycleStatus") + .HasColumnType("int"); + + b.Property("Longitude") + .HasColumnType("float"); + + b.Property("OrganizationClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("Title") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("EstimateId"); + + b.HasIndex("OrganizationClientId"); + + b.ToTable("Job", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.JobRecurrence", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DaysOfWeekMask") + .HasColumnType("int"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Duration") + .HasColumnType("time"); + + b.Property("EndDate") + .HasColumnType("datetime2"); + + b.Property("Frequency") + .HasColumnType("int"); + + b.Property("GenerateDaysAhead") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("uniqueidentifier"); + + b.Property("ScheduleType") + .HasColumnType("int"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("StartTime") + .HasColumnType("time"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobId", "IsActive") + .HasDatabaseName("IX_JobRecurrence_JobId_IsActive"); + + b.ToTable("JobRecurrence", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.JobTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DefaultInvoicingWorkflow") + .HasColumnType("int"); + + b.Property("Description") + .HasMaxLength(240) + .HasColumnType("nvarchar(240)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsSystem") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("OrganizationTypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.HasIndex("OrganizationTypeId"); + + b.ToTable("JobTemplates", (string)null); + + b.HasData( + new + { + Id = new Guid("3a3b3c3d-0101-0101-0101-010101010101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Full or partial home remodel project.", + IsActive = true, + IsSystem = true, + Name = "Home renovation", + OrganizationTypeId = new Guid("bf489aa6-db19-42df-82bc-c116bd967e7e") + }, + new + { + Id = new Guid("3a3b3c3d-0101-0101-0101-010101010102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "New outdoor living space construction.", + IsActive = true, + IsSystem = true, + Name = "Deck / patio build", + OrganizationTypeId = new Guid("bf489aa6-db19-42df-82bc-c116bd967e7e") + }, + new + { + Id = new Guid("3a3b3c3d-0202-0202-0202-020202020201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Paint walls, ceilings, and trim for interior rooms.", + IsActive = true, + IsSystem = true, + Name = "Interior painting", + OrganizationTypeId = new Guid("393a5b3e-323e-4b76-aa86-0d4683ddcd49") + }, + new + { + Id = new Guid("3a3b3c3d-0202-0202-0202-020202020202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Prep and paint home exterior surfaces.", + IsActive = true, + IsSystem = true, + Name = "Exterior painting", + OrganizationTypeId = new Guid("393a5b3e-323e-4b76-aa86-0d4683ddcd49") + }, + new + { + Id = new Guid("3a3b3c3d-0303-0303-0303-030303030301"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 1, + Description = "Diagnose and fix leaks, clogs, or faults.", + IsActive = true, + IsSystem = true, + Name = "Plumbing repair", + OrganizationTypeId = new Guid("e01750b0-0d01-4e25-abf7-6efa23509035") + }, + new + { + Id = new Guid("3a3b3c3d-0303-0303-0303-030303030302"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Remove old unit and install replacement.", + IsActive = true, + IsSystem = true, + Name = "Water heater install", + OrganizationTypeId = new Guid("e01750b0-0d01-4e25-abf7-6efa23509035") + }, + new + { + Id = new Guid("3a3b3c3d-0404-0404-0404-040404040401"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Mow, edge, blow, and treat lawn.", + IsActive = true, + IsSystem = true, + Name = "Lawn maintenance", + OrganizationTypeId = new Guid("fbc6accf-0fb1-4908-b449-14f13b826f24") + }, + new + { + Id = new Guid("3a3b3c3d-0404-0404-0404-040404040402"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Design beds, select plants, and install.", + IsActive = true, + IsSystem = true, + Name = "Garden design & install", + OrganizationTypeId = new Guid("fbc6accf-0fb1-4908-b449-14f13b826f24") + }, + new + { + Id = new Guid("3a3b3c3d-0505-0505-0505-050505050501"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 1, + Description = "Troubleshoot and fix wiring or fixture issues.", + IsActive = true, + IsSystem = true, + Name = "Electrical repair", + OrganizationTypeId = new Guid("9362c957-0f41-4c20-9085-c01e449fdda2") + }, + new + { + Id = new Guid("3a3b3c3d-0505-0505-0505-050505050502"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Upgrade electrical panel for capacity or safety.", + IsActive = true, + IsSystem = true, + Name = "Panel upgrade", + OrganizationTypeId = new Guid("9362c957-0f41-4c20-9085-c01e449fdda2") + }, + new + { + Id = new Guid("3a3b3c3d-0606-0606-0606-060606060601"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Measure, build, and install custom shelves.", + IsActive = true, + IsSystem = true, + Name = "Custom shelving", + OrganizationTypeId = new Guid("8f0d3e93-425b-4a53-b4d2-4c5eb97e490f") + }, + new + { + Id = new Guid("3a3b3c3d-0606-0606-0606-060606060602"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Install baseboards, crown molding, and casings.", + IsActive = true, + IsSystem = true, + Name = "Trim & molding install", + OrganizationTypeId = new Guid("8f0d3e93-425b-4a53-b4d2-4c5eb97e490f") + }, + new + { + Id = new Guid("3a3b3c3d-0707-0707-0707-070707070701"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Seasonal heating/cooling system tune-up.", + IsActive = true, + IsSystem = true, + Name = "HVAC maintenance", + OrganizationTypeId = new Guid("37fc17e8-0a25-4119-9a71-7d160bb9c7b4") + }, + new + { + Id = new Guid("3a3b3c3d-0707-0707-0707-070707070702"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Remove old unit and install new AC system.", + IsActive = true, + IsSystem = true, + Name = "AC install / replacement", + OrganizationTypeId = new Guid("37fc17e8-0a25-4119-9a71-7d160bb9c7b4") + }, + new + { + Id = new Guid("3a3b3c3d-0808-0808-0808-080808080801"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Fell, section, and haul away tree.", + IsActive = true, + IsSystem = true, + Name = "Tree removal", + OrganizationTypeId = new Guid("f64f078f-ecfb-4f3e-8640-236219fcf01e") + }, + new + { + Id = new Guid("3a3b3c3d-0808-0808-0808-080808080802"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Prune branches for health and clearance.", + IsActive = true, + IsSystem = true, + Name = "Tree trimming", + OrganizationTypeId = new Guid("f64f078f-ecfb-4f3e-8640-236219fcf01e") + }, + new + { + Id = new Guid("3a3b3c3d-0909-0909-0909-090909090901"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Interior and exterior spray for common pests.", + IsActive = true, + IsSystem = true, + Name = "General pest treatment", + OrganizationTypeId = new Guid("bf3b9512-8a9c-4a73-9f88-cb914c1573cd") + }, + new + { + Id = new Guid("3a3b3c3d-0909-0909-0909-090909090902"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Full property inspection and treatment plan.", + IsActive = true, + IsSystem = true, + Name = "Termite inspection", + OrganizationTypeId = new Guid("bf3b9512-8a9c-4a73-9f88-cb914c1573cd") + }, + new + { + Id = new Guid("3a3b3c3d-1010-1010-1010-101010101001"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Standard whole-house cleaning visit.", + IsActive = true, + IsSystem = true, + Name = "Full home clean", + OrganizationTypeId = new Guid("1921d982-22f8-4ed5-b4e3-fca82c5767eb") + }, + new + { + Id = new Guid("3a3b3c3d-1010-1010-1010-101010101002"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Thorough clean for tenant turnover.", + IsActive = true, + IsSystem = true, + Name = "Move-out deep clean", + OrganizationTypeId = new Guid("1921d982-22f8-4ed5-b4e3-fca82c5767eb") + }, + new + { + Id = new Guid("3a3b3c3d-1111-1111-1111-111111111101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 1, + Description = "Remove all unwanted items from property.", + IsActive = true, + IsSystem = true, + Name = "Full property cleanout", + OrganizationTypeId = new Guid("408d2185-53b9-493d-8713-938114de90f5") + }, + new + { + Id = new Guid("3a3b3c3d-1111-1111-1111-111111111102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 1, + Description = "Clear and haul garage contents.", + IsActive = true, + IsSystem = true, + Name = "Garage cleanout", + OrganizationTypeId = new Guid("408d2185-53b9-493d-8713-938114de90f5") + }, + new + { + Id = new Guid("3a3b3c3d-1212-1212-1212-121212121201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 1, + Description = "Complete interior and exterior detail.", + IsActive = true, + IsSystem = true, + Name = "Full detail", + OrganizationTypeId = new Guid("33341b2d-957f-4efb-94f7-3a015ae1a718") + }, + new + { + Id = new Guid("3a3b3c3d-1212-1212-1212-121212121202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 1, + Description = "Deep clean seats, dash, carpet, and glass.", + IsActive = true, + IsSystem = true, + Name = "Interior only", + OrganizationTypeId = new Guid("33341b2d-957f-4efb-94f7-3a015ae1a718") + }, + new + { + Id = new Guid("3a3b3c3d-1313-1313-1313-131313131301"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Install and configure router, switches, and cabling.", + IsActive = true, + IsSystem = true, + Name = "Network setup", + OrganizationTypeId = new Guid("30530a32-a151-436d-a050-613eac4c22d5") + }, + new + { + Id = new Guid("3a3b3c3d-1313-1313-1313-131313131302"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Set up desktops, laptops, and peripherals.", + IsActive = true, + IsSystem = true, + Name = "Workstation deployment", + OrganizationTypeId = new Guid("30530a32-a151-436d-a050-613eac4c22d5") + }, + new + { + Id = new Guid("3a3b3c3d-1414-1414-1414-141414141401"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 1, + Description = "Fix miscellaneous issues around the home.", + IsActive = true, + IsSystem = true, + Name = "General repair", + OrganizationTypeId = new Guid("0f32e14a-5f70-45af-a647-04e59ad52e58") + }, + new + { + Id = new Guid("3a3b3c3d-1414-1414-1414-141414141402"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 1, + Description = "Mount and connect lights, fans, or hardware.", + IsActive = true, + IsSystem = true, + Name = "Fixture install", + OrganizationTypeId = new Guid("0f32e14a-5f70-45af-a647-04e59ad52e58") + }, + new + { + Id = new Guid("3a3b3c3d-1515-1515-1515-151515151501"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Measure, prep subfloor, and install hardwood.", + IsActive = true, + IsSystem = true, + Name = "Hardwood install", + OrganizationTypeId = new Guid("09786eab-d69f-45bf-bcec-5f368bd60be7") + }, + new + { + Id = new Guid("3a3b3c3d-1515-1515-1515-151515151502"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + DefaultInvoicingWorkflow = 0, + Description = "Lay tile with proper spacing and grout.", + IsActive = true, + IsSystem = true, + Name = "Tile install", + OrganizationTypeId = new Guid("09786eab-d69f-45bf-bcec-5f368bd60be7") + }); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.JobTemplateItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(240) + .HasColumnType("nvarchar(240)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("TemplateId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("TemplateId"); + + b.ToTable("JobTemplateItems", (string)null); + + b.HasData( + new + { + Id = new Guid("4a4b4c4d-0101-0101-0101-010101010101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Walk property and scope of work.", + IsActive = true, + Name = "Site assessment", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0101-0101-0101-010101010101") + }, + new + { + Id = new Guid("4a4b4c4d-0101-0101-0101-010101010102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Remove old materials and prep surfaces.", + IsActive = true, + Name = "Demo & prep", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0101-0101-0101-010101010101") + }, + new + { + Id = new Guid("4a4b4c4d-0101-0101-0101-010101010103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Construction, paint, and final touches.", + IsActive = true, + Name = "Build & finish", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0101-0101-0101-010101010101") + }, + new + { + Id = new Guid("4a4b4c4d-0101-0101-0101-010101010201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Draft plan and pull permits.", + IsActive = true, + Name = "Design & permits", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0101-0101-0101-010101010102") + }, + new + { + Id = new Guid("4a4b4c4d-0101-0101-0101-010101010202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Set footings, frame, and deck boards.", + IsActive = true, + Name = "Frame & build", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0101-0101-0101-010101010102") + }, + new + { + Id = new Guid("4a4b4c4d-0101-0101-0101-010101010203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Stain/seal and final walkthrough.", + IsActive = true, + Name = "Finish & inspect", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0101-0101-0101-010101010102") + }, + new + { + Id = new Guid("4a4b4c4d-0202-0202-0202-020202020101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Confirm colors, tape, and cover surfaces.", + IsActive = true, + Name = "Color consult & prep", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0202-0202-0202-020202020201") + }, + new + { + Id = new Guid("4a4b4c4d-0202-0202-0202-020202020102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Apply primer and two coats.", + IsActive = true, + Name = "Prime & paint", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0202-0202-0202-020202020201") + }, + new + { + Id = new Guid("4a4b4c4d-0202-0202-0202-020202020103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Detail edges and remove coverings.", + IsActive = true, + Name = "Touch-up & clean", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0202-0202-0202-020202020201") + }, + new + { + Id = new Guid("4a4b4c4d-0202-0202-0202-020202020201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Clean and prep exterior surfaces.", + IsActive = true, + Name = "Power wash & scrape", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0202-0202-0202-020202020202") + }, + new + { + Id = new Guid("4a4b4c4d-0202-0202-0202-020202020202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Coat siding, fascia, and trim.", + IsActive = true, + Name = "Prime & paint", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0202-0202-0202-020202020202") + }, + new + { + Id = new Guid("4a4b4c4d-0202-0202-0202-020202020203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Check coverage and clean site.", + IsActive = true, + Name = "Final inspection", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0202-0202-0202-020202020202") + }, + new + { + Id = new Guid("4a4b4c4d-0303-0303-0303-030303030101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Locate leak, blockage, or fault.", + IsActive = true, + Name = "Diagnose issue", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0303-0303-0303-030303030301") + }, + new + { + Id = new Guid("4a4b4c4d-0303-0303-0303-030303030102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Fix or replace the faulty component.", + IsActive = true, + Name = "Perform repair", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0303-0303-0303-030303030301") + }, + new + { + Id = new Guid("4a4b4c4d-0303-0303-0303-030303030103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Run water, check pressure, tidy area.", + IsActive = true, + Name = "Test & clean up", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0303-0303-0303-030303030301") + }, + new + { + Id = new Guid("4a4b4c4d-0303-0303-0303-030303030201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Shut off supply and drain tank.", + IsActive = true, + Name = "Disconnect old unit", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0303-0303-0303-030303030302") + }, + new + { + Id = new Guid("4a4b4c4d-0303-0303-0303-030303030202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Position, connect plumbing and power.", + IsActive = true, + Name = "Install new unit", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0303-0303-0303-030303030302") + }, + new + { + Id = new Guid("4a4b4c4d-0303-0303-0303-030303030203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Check for leaks and set temperature.", + IsActive = true, + Name = "Test & verify", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0303-0303-0303-030303030302") + }, + new + { + Id = new Guid("4a4b4c4d-0404-0404-0404-040404040101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Cut grass and trim borders.", + IsActive = true, + Name = "Mow & edge", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0404-0404-0404-040404040401") + }, + new + { + Id = new Guid("4a4b4c4d-0404-0404-0404-040404040102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Clear clippings from walks and drives.", + IsActive = true, + Name = "Blow & clean", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0404-0404-0404-040404040401") + }, + new + { + Id = new Guid("4a4b4c4d-0404-0404-0404-040404040103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Apply treatment as needed.", + IsActive = true, + Name = "Treat & fertilize", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0404-0404-0404-040404040401") + }, + new + { + Id = new Guid("4a4b4c4d-0404-0404-0404-040404040201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Plan beds, paths, and plant selection.", + IsActive = true, + Name = "Design layout", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0404-0404-0404-040404040402") + }, + new + { + Id = new Guid("4a4b4c4d-0404-0404-0404-040404040202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Amend soil and install plants.", + IsActive = true, + Name = "Prep & plant", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0404-0404-0404-040404040402") + }, + new + { + Id = new Guid("4a4b4c4d-0404-0404-0404-040404040203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Top-dress and initial watering.", + IsActive = true, + Name = "Mulch & water", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0404-0404-0404-040404040402") + }, + new + { + Id = new Guid("4a4b4c4d-0505-0505-0505-050505050101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Test circuits and locate issue.", + IsActive = true, + Name = "Diagnose fault", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0505-0505-0505-050505050501") + }, + new + { + Id = new Guid("4a4b4c4d-0505-0505-0505-050505050102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Replace or repair faulty components.", + IsActive = true, + Name = "Repair wiring", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0505-0505-0505-050505050501") + }, + new + { + Id = new Guid("4a4b4c4d-0505-0505-0505-050505050103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Confirm power and safety.", + IsActive = true, + Name = "Test & verify", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0505-0505-0505-050505050501") + }, + new + { + Id = new Guid("4a4b4c4d-0505-0505-0505-050505050201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Kill main and label circuits.", + IsActive = true, + Name = "Shut down & disconnect", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0505-0505-0505-050505050502") + }, + new + { + Id = new Guid("4a4b4c4d-0505-0505-0505-050505050202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Mount new box and reconnect breakers.", + IsActive = true, + Name = "Swap panel", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0505-0505-0505-050505050502") + }, + new + { + Id = new Guid("4a4b4c4d-0505-0505-0505-050505050203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Restore power and run tests.", + IsActive = true, + Name = "Energize & inspect", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0505-0505-0505-050505050502") + }, + new + { + Id = new Guid("4a4b4c4d-0606-0606-0606-060606060101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Take dimensions and plan layout.", + IsActive = true, + Name = "Measure & design", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0606-0606-0606-060606060601") + }, + new + { + Id = new Guid("4a4b4c4d-0606-0606-0606-060606060102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Build shelving units.", + IsActive = true, + Name = "Cut & assemble", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0606-0606-0606-060606060601") + }, + new + { + Id = new Guid("4a4b4c4d-0606-0606-0606-060606060103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Mount, sand, and apply finish.", + IsActive = true, + Name = "Install & finish", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0606-0606-0606-060606060601") + }, + new + { + Id = new Guid("4a4b4c4d-0606-0606-0606-060606060201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Measure runs and miter cut pieces.", + IsActive = true, + Name = "Measure & cut", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0606-0606-0606-060606060602") + }, + new + { + Id = new Guid("4a4b4c4d-0606-0606-0606-060606060202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Attach molding and set nails.", + IsActive = true, + Name = "Nail & set", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0606-0606-0606-060606060602") + }, + new + { + Id = new Guid("4a4b4c4d-0606-0606-0606-060606060203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Fill gaps, prime, and paint.", + IsActive = true, + Name = "Caulk & paint", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0606-0606-0606-060606060602") + }, + new + { + Id = new Guid("4a4b4c4d-0707-0707-0707-070707070101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Check airflow and swap filter.", + IsActive = true, + Name = "Inspect & replace filter", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0707-0707-0707-070707070701") + }, + new + { + Id = new Guid("4a4b4c4d-0707-0707-0707-070707070102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Remove buildup from outdoor unit.", + IsActive = true, + Name = "Clean condenser coils", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0707-0707-0707-070707070701") + }, + new + { + Id = new Guid("4a4b4c4d-0707-0707-0707-070707070103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Verify settings and calibration.", + IsActive = true, + Name = "Test thermostat", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0707-0707-0707-070707070701") + }, + new + { + Id = new Guid("4a4b4c4d-0707-0707-0707-070707070201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Disconnect and haul away.", + IsActive = true, + Name = "Remove old unit", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0707-0707-0707-070707070702") + }, + new + { + Id = new Guid("4a4b4c4d-0707-0707-0707-070707070202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Set unit, connect lines and electric.", + IsActive = true, + Name = "Install new system", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0707-0707-0707-070707070702") + }, + new + { + Id = new Guid("4a4b4c4d-0707-0707-0707-070707070203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Add refrigerant and run cycles.", + IsActive = true, + Name = "Charge & test", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0707-0707-0707-070707070702") + }, + new + { + Id = new Guid("4a4b4c4d-0808-0808-0808-080808080101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Evaluate drop zone and rigging.", + IsActive = true, + Name = "Assess & plan", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0808-0808-0808-080808080801") + }, + new + { + Id = new Guid("4a4b4c4d-0808-0808-0808-080808080102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Cut tree and process limbs.", + IsActive = true, + Name = "Fell & section", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0808-0808-0808-080808080801") + }, + new + { + Id = new Guid("4a4b4c4d-0808-0808-0808-080808080103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Remove debris and rake site.", + IsActive = true, + Name = "Haul & clean", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0808-0808-0808-080808080801") + }, + new + { + Id = new Guid("4a4b4c4d-0808-0808-0808-080808080201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Identify dead or problem branches.", + IsActive = true, + Name = "Inspect canopy", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0808-0808-0808-080808080802") + }, + new + { + Id = new Guid("4a4b4c4d-0808-0808-0808-080808080202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Cut for shape, clearance, and health.", + IsActive = true, + Name = "Prune branches", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0808-0808-0808-080808080802") + }, + new + { + Id = new Guid("4a4b4c4d-0808-0808-0808-080808080203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Chip or haul brush.", + IsActive = true, + Name = "Clean up", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0808-0808-0808-080808080802") + }, + new + { + Id = new Guid("4a4b4c4d-0909-0909-0909-090909090101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Identify pest activity and entry points.", + IsActive = true, + Name = "Inspect property", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0909-0909-0909-090909090901") + }, + new + { + Id = new Guid("4a4b4c4d-0909-0909-0909-090909090102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Spray interior and exterior perimeter.", + IsActive = true, + Name = "Apply treatment", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0909-0909-0909-090909090901") + }, + new + { + Id = new Guid("4a4b4c4d-0909-0909-0909-090909090103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Log findings and next visit.", + IsActive = true, + Name = "Document & recommend", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0909-0909-0909-090909090901") + }, + new + { + Id = new Guid("4a4b4c4d-0909-0909-0909-090909090201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Check foundation, crawl, and attic.", + IsActive = true, + Name = "Full property scan", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-0909-0909-0909-090909090902") + }, + new + { + Id = new Guid("4a4b4c4d-0909-0909-0909-090909090202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Tap and test for damage.", + IsActive = true, + Name = "Probe suspect areas", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-0909-0909-0909-090909090902") + }, + new + { + Id = new Guid("4a4b4c4d-0909-0909-0909-090909090203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Findings and treatment estimate.", + IsActive = true, + Name = "Deliver report", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-0909-0909-0909-090909090902") + }, + new + { + Id = new Guid("4a4b4c4d-1010-1010-1010-101010100101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Assess rooms and special requests.", + IsActive = true, + Name = "Walkthrough", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-1010-1010-1010-101010101001") + }, + new + { + Id = new Guid("4a4b4c4d-1010-1010-1010-101010100102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Scrub fixtures, counters, appliances.", + IsActive = true, + Name = "Deep clean kitchen & baths", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-1010-1010-1010-101010101001") + }, + new + { + Id = new Guid("4a4b4c4d-1010-1010-1010-101010100103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "All hard and carpeted surfaces.", + IsActive = true, + Name = "Vacuum & mop", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-1010-1010-1010-101010101001") + }, + new + { + Id = new Guid("4a4b4c4d-1010-1010-1010-101010100201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Clear remaining items, dust surfaces.", + IsActive = true, + Name = "Empty & prep", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-1010-1010-1010-101010101002") + }, + new + { + Id = new Guid("4a4b4c4d-1010-1010-1010-101010100202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Walls, baseboards, inside cabinets.", + IsActive = true, + Name = "Scrub all rooms", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-1010-1010-1010-101010101002") + }, + new + { + Id = new Guid("4a4b4c4d-1010-1010-1010-101010100203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Verify quality before handoff.", + IsActive = true, + Name = "Final walkthrough", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-1010-1010-1010-101010101002") + }, + new + { + Id = new Guid("4a4b4c4d-1111-1111-1111-111111110101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Tag items and plan truck loads.", + IsActive = true, + Name = "Walk & inventory", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-1111-1111-1111-111111111101") + }, + new + { + Id = new Guid("4a4b4c4d-1111-1111-1111-111111110102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Remove items to truck.", + IsActive = true, + Name = "Load & haul", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-1111-1111-1111-111111111101") + }, + new + { + Id = new Guid("4a4b4c4d-1111-1111-1111-111111110103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Clean site, dump or donate.", + IsActive = true, + Name = "Sweep & dispose", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-1111-1111-1111-111111111101") + }, + new + { + Id = new Guid("4a4b4c4d-1111-1111-1111-111111110201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Separate keep, donate, and trash.", + IsActive = true, + Name = "Sort items", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-1111-1111-1111-111111111102") + }, + new + { + Id = new Guid("4a4b4c4d-1111-1111-1111-111111110202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Move discards to truck.", + IsActive = true, + Name = "Load out", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-1111-1111-1111-111111111102") + }, + new + { + Id = new Guid("4a4b4c4d-1111-1111-1111-111111110203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Broom clean and organize keepers.", + IsActive = true, + Name = "Sweep garage", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-1111-1111-1111-111111111102") + }, + new + { + Id = new Guid("4a4b4c4d-1212-1212-1212-121212120101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Hand wash, clay bar, and dry.", + IsActive = true, + Name = "Exterior wash & clay", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-1212-1212-1212-121212121201") + }, + new + { + Id = new Guid("4a4b4c4d-1212-1212-1212-121212120102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Vacuum, shampoo, and condition.", + IsActive = true, + Name = "Interior deep clean", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-1212-1212-1212-121212121201") + }, + new + { + Id = new Guid("4a4b4c4d-1212-1212-1212-121212120103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Compound, wax, and dress tires.", + IsActive = true, + Name = "Polish & protect", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-1212-1212-1212-121212121201") + }, + new + { + Id = new Guid("4a4b4c4d-1212-1212-1212-121212120201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "All seats, dash, and crevices.", + IsActive = true, + Name = "Vacuum & dust", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-1212-1212-1212-121212121202") + }, + new + { + Id = new Guid("4a4b4c4d-1212-1212-1212-121212120202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Extract and clean fabric or leather.", + IsActive = true, + Name = "Shampoo upholstery", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-1212-1212-1212-121212121202") + }, + new + { + Id = new Guid("4a4b4c4d-1212-1212-1212-121212120203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Clean interior glass and surfaces.", + IsActive = true, + Name = "Glass & final wipe", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-1212-1212-1212-121212121202") + }, + new + { + Id = new Guid("4a4b4c4d-1313-1313-1313-131313130101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Map runs and plan equipment placement.", + IsActive = true, + Name = "Site survey", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-1313-1313-1313-131313131301") + }, + new + { + Id = new Guid("4a4b4c4d-1313-1313-1313-131313130102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Pull cable and install hardware.", + IsActive = true, + Name = "Run cable & mount", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-1313-1313-1313-131313131301") + }, + new + { + Id = new Guid("4a4b4c4d-1313-1313-1313-131313130103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Set up network and verify connectivity.", + IsActive = true, + Name = "Configure & test", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-1313-1313-1313-131313131301") + }, + new + { + Id = new Guid("4a4b4c4d-1313-1313-1313-131313130201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Prep hardware and install OS/software.", + IsActive = true, + Name = "Unbox & image", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-1313-1313-1313-131313131302") + }, + new + { + Id = new Guid("4a4b4c4d-1313-1313-1313-131313130202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Place at desk and join network.", + IsActive = true, + Name = "Deploy & connect", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-1313-1313-1313-131313131302") + }, + new + { + Id = new Guid("4a4b4c4d-1313-1313-1313-131313130203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Verify apps and orient user.", + IsActive = true, + Name = "User walkthrough", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-1313-1313-1313-131313131302") + }, + new + { + Id = new Guid("4a4b4c4d-1414-1414-1414-141414140101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Inspect problem area and plan fix.", + IsActive = true, + Name = "Assess issue", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-1414-1414-1414-141414141401") + }, + new + { + Id = new Guid("4a4b4c4d-1414-1414-1414-141414140102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Fix or replace components.", + IsActive = true, + Name = "Repair", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-1414-1414-1414-141414141401") + }, + new + { + Id = new Guid("4a4b4c4d-1414-1414-1414-141414140103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Test fix and tidy work area.", + IsActive = true, + Name = "Verify & clean up", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-1414-1414-1414-141414141401") + }, + new + { + Id = new Guid("4a4b4c4d-1414-1414-1414-141414140201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Mark placement and run wiring if needed.", + IsActive = true, + Name = "Prep location", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-1414-1414-1414-141414141402") + }, + new + { + Id = new Guid("4a4b4c4d-1414-1414-1414-141414140202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Secure and connect.", + IsActive = true, + Name = "Mount fixture", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-1414-1414-1414-141414141402") + }, + new + { + Id = new Guid("4a4b4c4d-1414-1414-1414-141414140203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Confirm operation and patch holes.", + IsActive = true, + Name = "Test & finish", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-1414-1414-1414-141414141402") + }, + new + { + Id = new Guid("4a4b4c4d-1515-1515-1515-151515150101"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Level, clean, and lay moisture barrier.", + IsActive = true, + Name = "Prep subfloor", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-1515-1515-1515-151515151501") + }, + new + { + Id = new Guid("4a4b4c4d-1515-1515-1515-151515150102"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Lay and nail or click into place.", + IsActive = true, + Name = "Install planks", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-1515-1515-1515-151515151501") + }, + new + { + Id = new Guid("4a4b4c4d-1515-1515-1515-151515150103"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Install transitions and clean.", + IsActive = true, + Name = "Trim & finish", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-1515-1515-1515-151515151501") + }, + new + { + Id = new Guid("4a4b4c4d-1515-1515-1515-151515150201"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Dry-fit pattern and mix thinset.", + IsActive = true, + Name = "Layout & prep", + SortOrder = 1, + TemplateId = new Guid("3a3b3c3d-1515-1515-1515-151515151502") + }, + new + { + Id = new Guid("4a4b4c4d-1515-1515-1515-151515150202"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Lay tiles with spacers.", + IsActive = true, + Name = "Set tile", + SortOrder = 2, + TemplateId = new Guid("3a3b3c3d-1515-1515-1515-151515151502") + }, + new + { + Id = new Guid("4a4b4c4d-1515-1515-1515-151515150203"), + CreatedAt = new DateTime(2026, 4, 8, 0, 0, 0, 0, DateTimeKind.Utc), + Description = "Fill joints, clean haze, and seal.", + IsActive = true, + Name = "Grout & seal", + SortOrder = 3, + TemplateId = new Guid("3a3b3c3d-1515-1515-1515-151515151502") + }); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.JobTracking", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("EmployeeId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("uniqueidentifier"); + + b.Property("Latitude") + .HasColumnType("decimal(9,6)"); + + b.Property("Longitude") + .HasColumnType("decimal(9,6)"); + + b.Property("RecordedAt") + .ValueGeneratedOnAdd() + .HasColumnType("datetime2") + .HasDefaultValueSql("GETUTCDATE()"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("EmployeeId"); + + b.HasIndex("JobId"); + + b.ToTable("JobTracking"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.JobUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("JobId") + .HasColumnType("uniqueidentifier"); + + b.Property("Message") + .HasMaxLength(4000) + .HasColumnType("nvarchar(4000)"); + + b.Property("OccurredAt") + .HasColumnType("datetimeoffset"); + + b.Property("OrganizationClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Type") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobId", "OccurredAt"); + + b.ToTable("JobUpdates", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.JobUpdateAttachment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("FileData") + .IsRequired() + .HasColumnType("varbinary(max)"); + + b.Property("FileName") + .IsRequired() + .HasMaxLength(260) + .HasColumnType("nvarchar(260)"); + + b.Property("FileSizeBytes") + .HasColumnType("bigint"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("JobUpdateId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("JobUpdateId"); + + b.ToTable("JobUpdateAttachments", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Message", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AttachmentUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("ConversationId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ExternalSenderName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ExternalSenderPhone") + .HasMaxLength(32) + .HasColumnType("nvarchar(32)"); + + b.Property("ExternalSenderType") + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("SenderId") + .HasColumnType("uniqueidentifier"); + + b.Property("SentAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ConversationId"); + + b.HasIndex("SenderId"); + + b.ToTable("Message", "messaging"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Order", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Notes") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("OrderDate") + .HasColumnType("datetime2"); + + b.Property("OrganizationClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("TotalAmount") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationClientId"); + + b.ToTable("Order", "payment"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Organization", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Address1") + .HasColumnType("nvarchar(max)"); + + b.Property("Address2") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactFirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("ContactLastName") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DefaultTaxRate") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("EmailAddress") + .HasColumnType("nvarchar(max)"); + + b.Property("EnableTax") + .HasColumnType("bit"); + + b.Property("FirstRealEstimateSentAt") + .HasColumnType("datetimeoffset"); + + b.Property("HasFreeAccount") + .HasColumnType("bit"); + + b.Property("IndustryKey") + .HasMaxLength(80) + .HasColumnType("nvarchar(80)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsSquareConnected") + .HasColumnType("bit"); + + b.Property("IsStripeConnected") + .HasColumnType("bit"); + + b.Property("OnBoardingComplete") + .HasColumnType("bit"); + + b.Property("OnboardingPresetAppliedAt") + .HasColumnType("datetimeoffset"); + + b.Property("OnboardingPresetKey") + .HasColumnType("nvarchar(max)"); + + b.Property("OnboardingTrack") + .HasColumnType("nvarchar(max)"); + + b.Property("OnboardingTrackSelectedAt") + .HasColumnType("datetimeoffset"); + + b.Property("OrgSize") + .HasColumnType("int"); + + b.Property("OrganizationName") + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationTypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("PaymentProvider") + .HasColumnType("int"); + + b.Property("PaymentSetupSkippedAt") + .HasColumnType("datetimeoffset"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("ReferralCtaShownAt") + .HasColumnType("datetimeoffset"); + + b.Property("SquareMerchantId") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("StripeConnectAccountId") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionExpiresAt") + .HasColumnType("datetime2"); + + b.Property("SubscriptionPlanName") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionStatus") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ZipCode") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationTypeId"); + + b.ToTable("Organization", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationBranding", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("BusinessName") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("FooterNote") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LogoUrl") + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PrimaryColor") + .HasColumnType("nvarchar(max)"); + + b.Property("SecondaryColor") + .HasColumnType("nvarchar(max)"); + + b.Property("Tagline") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationBranding", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationClient", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Address1") + .HasColumnType("nvarchar(max)"); + + b.Property("Address2") + .HasColumnType("nvarchar(max)"); + + b.Property("City") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("EmailAddress") + .HasColumnType("nvarchar(450)"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("SmsConsentGiven") + .HasColumnType("bit"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("ZipCode") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "EmailAddress") + .IsUnique() + .HasFilter("[EmailAddress] IS NOT NULL AND [IsActive] = 1"); + + b.ToTable("OrganizationClient", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationClientPortalSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("EmailAddress") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("nvarchar(320)"); + + b.Property("ExpiresAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("RedeemedAt") + .HasColumnType("datetimeoffset"); + + b.Property("TokenHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationClientId"); + + b.HasIndex("TokenHash") + .IsUnique(); + + b.ToTable("OrganizationClientPortalSession"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationInvoicingSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DefaultWorkflow") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0); + + b.Property("DepositPercentage") + .HasPrecision(5, 2) + .HasColumnType("decimal(5,2)"); + + b.Property("DepositRequired") + .HasColumnType("bit"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PaymentTermsDays") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationInvoicingSettings"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationOnboardingEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("EventType") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("StepName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "StepName", "EventType"); + + b.ToTable("OrganizationOnboardingEvents", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationOnboardingStep", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CompletedAt") + .HasColumnType("datetimeoffset"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsCompleted") + .HasColumnType("bit"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("StepName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "StepName") + .IsUnique(); + + b.ToTable("OrganizationOnboardingSteps", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationScheduleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AutoNotifyReschedule") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DefaultWindowMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(120); + + b.Property("EnforceTravelBuffer") + .ValueGeneratedOnAdd() + .HasColumnType("bit") + .HasDefaultValue(true); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("TravelBufferMinutes") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(20); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId") + .IsUnique(); + + b.ToTable("OrganizationScheduleSettings"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationService", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("ServiceName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("OrganizationService", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationType", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("TypeName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("OrganizationType", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationWorkflowStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(60) + .HasColumnType("nvarchar(60)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Label") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("SortOrder") + .HasColumnType("int"); + + b.Property("StatusKey") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("nvarchar(80)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Category", "StatusKey") + .IsUnique(); + + b.ToTable("OrganizationWorkflowStatus"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.PaymentHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AmountPaid") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("Currency") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerId") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("EntityId") + .HasColumnType("uniqueidentifier"); + + b.Property("EntityType") + .HasColumnType("int"); + + b.Property("EventType") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("InvoiceId") + .HasColumnType("uniqueidentifier"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("PaidAt") + .HasColumnType("datetime2"); + + b.Property("PaymentProvider") + .HasColumnType("int"); + + b.Property("RawEventJson") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Status") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("StripeInvoiceId") + .HasColumnType("nvarchar(max)"); + + b.Property("StripePaymentIntentId") + .HasColumnType("nvarchar(max)"); + + b.Property("SubscriptionId") + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("InvoiceId"); + + b.HasIndex("EntityId", "PaidAt"); + + b.ToTable("PaymentHistory"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.PriceBookCategory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("PriceBookCategories", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.PriceBookItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CategoryId") + .HasColumnType("uniqueidentifier"); + + b.Property("Cost") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Description") + .HasMaxLength(1000) + .HasColumnType("nvarchar(1000)"); + + b.Property("InventoryItemId") + .HasColumnType("uniqueidentifier"); + + b.Property("InventoryUnitsPerSale") + .ValueGeneratedOnAdd() + .HasColumnType("decimal(18,4)") + .HasDefaultValue(1.0m); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsTaxable") + .HasColumnType("bit"); + + b.Property("ItemType") + .HasColumnType("int"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PartNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("Price") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("PricePerUnit") + .HasPrecision(18, 2) + .HasColumnType("decimal(18,2)"); + + b.Property("Unit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("nvarchar(50)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("CategoryId"); + + b.HasIndex("InventoryItemId"); + + b.HasIndex("OrganizationId", "Name") + .IsUnique(); + + b.ToTable("PriceBookItems", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SecurityAlert", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Category") + .IsRequired() + .HasMaxLength(80) + .HasColumnType("nvarchar(80)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Description") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("nvarchar(1024)"); + + b.Property("DetailsJson") + .HasColumnType("nvarchar(max)"); + + b.Property("EvidenceCount") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("RuleKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("nvarchar(100)"); + + b.Property("Severity") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("nvarchar(20)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("WindowEndUtc") + .HasColumnType("datetime2"); + + b.Property("WindowStartUtc") + .HasColumnType("datetime2"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("RuleKey", "Status", "CreatedAt"); + + b.ToTable("SecurityAlert", "security"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SetupCompanionEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AnswerKey") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OccurredAt") + .HasColumnType("datetimeoffset"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("QuestionKey") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("SetupCompanionEvents"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.ShortLink", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AccessCount") + .HasColumnType("int"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("nvarchar(16)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LastAccessedAt") + .HasColumnType("datetime2"); + + b.Property("TargetUrl") + .IsRequired() + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique() + .HasFilter("[IsActive] = 1"); + + b.HasIndex("CreatedAt"); + + b.ToTable("ShortLink", "notifications"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SubscriptionRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CanceledAt") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("PaymentProfileId") + .HasColumnType("uniqueidentifier"); + + b.Property("PlanName") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("Provider") + .HasColumnType("int"); + + b.Property("ProviderPriceId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("ProviderSubscriptionId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("StartDate") + .HasColumnType("datetime2"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("nvarchar(64)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("PaymentProfileId"); + + b.ToTable("SubscriptionRecord", "payment"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SupportChatMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Content") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("FileName") + .HasMaxLength(255) + .HasColumnType("nvarchar(255)"); + + b.Property("FileSize") + .HasColumnType("bigint"); + + b.Property("FileUrl") + .HasMaxLength(2048) + .HasColumnType("nvarchar(2048)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("IsRead") + .HasColumnType("bit"); + + b.Property("SenderId") + .HasColumnType("uniqueidentifier"); + + b.Property("SenderName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("SenderRole") + .HasColumnType("int"); + + b.Property("SentAt") + .HasColumnType("datetime2"); + + b.Property("SessionId") + .HasColumnType("uniqueidentifier"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("SentAt"); + + b.HasIndex("SessionId"); + + b.ToTable("SupportChatMessage", "support"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SupportChatSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AssignedRepId") + .HasColumnType("uniqueidentifier"); + + b.Property("AssignedRepName") + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("ClosedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("CustomerEmail") + .IsRequired() + .HasMaxLength(320) + .HasColumnType("nvarchar(320)"); + + b.Property("CustomerId") + .HasColumnType("uniqueidentifier"); + + b.Property("CustomerName") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("nvarchar(200)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("EstimatedWaitSeconds") + .HasColumnType("int"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("StartedAt") + .HasColumnType("datetime2"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("AssignedRepId"); + + b.HasIndex("CustomerEmail"); + + b.HasIndex("CustomerId"); + + b.HasIndex("Status"); + + b.ToTable("SupportChatSession", "support"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SupportHubInvite", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("Code") + .IsRequired() + .HasMaxLength(12) + .HasColumnType("nvarchar(12)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("ExpiresAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("RedeemedAt") + .HasColumnType("datetimeoffset"); + + b.Property("RedeemedByUid") + .HasMaxLength(128) + .HasColumnType("nvarchar(128)"); + + b.Property("Role") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("Code") + .IsUnique(); + + b.ToTable("SupportHubInvites"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SupportHubSession", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AgentName") + .IsRequired() + .HasMaxLength(120) + .HasColumnType("nvarchar(120)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("EndedAt") + .HasColumnType("datetimeoffset"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("StartedAt") + .HasColumnType("datetimeoffset"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SupportHubSessions"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SupportHubTicket", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LastActivityAt") + .HasColumnType("datetimeoffset"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("Status") + .HasColumnType("int"); + + b.Property("Summary") + .HasMaxLength(500) + .HasColumnType("nvarchar(500)"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(160) + .HasColumnType("nvarchar(160)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("OrganizationId"); + + b.ToTable("SupportHubTickets"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SystemRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier") + .HasDefaultValueSql("NEWID()"); + + b.Property("Name") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("Roles", (string)null); + + b.HasData( + new + { + Id = new Guid("e88fbbe6-8bdf-4aca-b941-912785a94f0b"), + Name = "OrganizationAdmin" + }, + new + { + Id = new Guid("079e4277-0eb2-4222-82e4-5a751ede48f6"), + Name = "OrganizationEmployee" + }, + new + { + Id = new Guid("3da14c58-562a-437a-a2a6-47706b40eb70"), + Name = "OrganizationClient" + }, + new + { + Id = new Guid("5bc0d325-a915-4e17-8184-428ee533cf89"), + Name = "KatharixAdmin" + }, + new + { + Id = new Guid("92193eb2-dba0-433c-814e-9fca95bde016"), + Name = "KatharixEmployee" + }, + new + { + Id = new Guid("dfe36ebc-bfb5-4583-b68e-59be8ba60fa9"), + Name = "SuperAdmin" + }); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.User", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("ClientId") + .HasColumnType("uniqueidentifier"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("Email") + .HasColumnType("nvarchar(max)"); + + b.Property("FirebaseUid") + .HasColumnType("nvarchar(max)"); + + b.Property("FirstName") + .HasColumnType("nvarchar(max)"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("LastName") + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("PreferredLanguage") + .HasMaxLength(10) + .HasColumnType("nvarchar(10)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.HasIndex("ClientId"); + + b.HasIndex("OrganizationId"); + + b.ToTable("Users", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.UserRole", b => + { + b.Property("UserId") + .HasColumnType("uniqueidentifier"); + + b.Property("RoleId") + .HasColumnType("uniqueidentifier"); + + b.HasKey("UserId", "RoleId"); + + b.HasIndex("RoleId"); + + b.ToTable("UserRoles", (string)null); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Assignment", b => + { + b.HasOne("JobFlow.Domain.Models.Job", "Job") + .WithMany("Assignments") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.AssignmentAssignee", b => + { + b.HasOne("JobFlow.Domain.Models.Assignment", "Assignment") + .WithMany("AssignmentAssignees") + .HasForeignKey("AssignmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JobFlow.Domain.Models.Employee", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Assignment"); + + b.Navigation("Employee"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.AssignmentHistory", b => + { + b.HasOne("JobFlow.Domain.Models.Assignment", "Assignment") + .WithMany() + .HasForeignKey("AssignmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Assignment"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.AssignmentOrder", b => + { + b.HasOne("JobFlow.Domain.Models.Assignment", "Assignment") + .WithMany("AssignmentOrders") + .HasForeignKey("AssignmentId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JobFlow.Domain.Models.Order", "Order") + .WithMany("AssignmentOrders") + .HasForeignKey("OrderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Assignment"); + + b.Navigation("Order"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.ClientImportJob", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.ClientImportJobError", b => + { + b.HasOne("JobFlow.Domain.Models.ClientImportJob", "ClientImportJob") + .WithMany("Errors") + .HasForeignKey("ClientImportJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ClientImportJob"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.ClientImportUploadRow", b => + { + b.HasOne("JobFlow.Domain.Models.ClientImportUploadSession", "Session") + .WithMany("Rows") + .HasForeignKey("ClientImportUploadSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.ClientImportUploadSession", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Conversation", b => + { + b.HasOne("JobFlow.Domain.Models.OrganizationClient", "OrganizationClient") + .WithMany() + .HasForeignKey("OrganizationClientId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("OrganizationClient"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.ConversationParticipant", b => + { + b.HasOne("JobFlow.Domain.Models.Conversation", "Conversation") + .WithMany("Participants") + .HasForeignKey("ConversationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JobFlow.Domain.Models.User", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Conversation"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.CustomerPaymentProfile", b => + { + b.HasOne("JobFlow.Domain.Models.OrganizationClient", null) + .WithMany("PaymentProfiles") + .HasForeignKey("OrganizationClientId"); + + b.HasOne("JobFlow.Domain.Models.Organization", null) + .WithMany("PaymentProfiles") + .HasForeignKey("OrganizationId"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.DataExportJob", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Employee", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany("Employees") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JobFlow.Domain.Models.EmployeeRole", "Role") + .WithMany("Employees") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("JobFlow.Domain.Models.User", "User") + .WithMany("Employees") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Organization"); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeImportJob", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeImportJobError", b => + { + b.HasOne("JobFlow.Domain.Models.EmployeeImportJob", "EmployeeImportJob") + .WithMany("Errors") + .HasForeignKey("EmployeeImportJobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("EmployeeImportJob"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeImportUploadRow", b => + { + b.HasOne("JobFlow.Domain.Models.EmployeeImportUploadSession", "Session") + .WithMany("Rows") + .HasForeignKey("EmployeeImportUploadSessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeImportUploadSession", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeInvite", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("JobFlow.Domain.Models.EmployeeRole", "Role") + .WithMany() + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + + b.Navigation("Role"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeRole", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany("EmployeeRoles") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeRolePreset", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeRolePresetItem", b => + { + b.HasOne("JobFlow.Domain.Models.EmployeeRolePreset", "Preset") + .WithMany("Items") + .HasForeignKey("PresetId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Preset"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Estimate", b => + { + b.HasOne("JobFlow.Domain.Models.OrganizationClient", "OrganizationClient") + .WithMany() + .HasForeignKey("OrganizationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationClient"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EstimateLineItem", b => + { + b.HasOne("JobFlow.Domain.Models.Estimate", "Estimate") + .WithMany("LineItems") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JobFlow.Domain.Models.PriceBookItem", "PriceBookItem") + .WithMany() + .HasForeignKey("PriceBookItemId"); + + b.Navigation("Estimate"); + + b.Navigation("PriceBookItem"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EstimateRevisionAttachment", b => + { + b.HasOne("JobFlow.Domain.Models.EstimateRevisionRequest", "RevisionRequest") + .WithMany("Attachments") + .HasForeignKey("EstimateRevisionRequestId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RevisionRequest"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EstimateRevisionRequest", b => + { + b.HasOne("JobFlow.Domain.Models.Estimate", "Estimate") + .WithMany("RevisionRequests") + .HasForeignKey("EstimateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JobFlow.Domain.Models.OrganizationClient", "OrganizationClient") + .WithMany() + .HasForeignKey("OrganizationClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Estimate"); + + b.Navigation("OrganizationClient"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.FollowUpExecutionLog", b => + { + b.HasOne("JobFlow.Domain.Models.FollowUpRun", "Run") + .WithMany("ExecutionLogs") + .HasForeignKey("FollowUpRunId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Run"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.FollowUpRun", b => + { + b.HasOne("JobFlow.Domain.Models.FollowUpSequence", "Sequence") + .WithMany() + .HasForeignKey("FollowUpSequenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sequence"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.FollowUpStep", b => + { + b.HasOne("JobFlow.Domain.Models.FollowUpSequence", "Sequence") + .WithMany("Steps") + .HasForeignKey("FollowUpSequenceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Sequence"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Invoice", b => + { + b.HasOne("JobFlow.Domain.Models.Job", "Job") + .WithMany() + .HasForeignKey("JobId"); + + b.HasOne("JobFlow.Domain.Models.Order", "Order") + .WithMany("Invoices") + .HasForeignKey("OrderId"); + + b.HasOne("JobFlow.Domain.Models.OrganizationClient", "OrganizationClient") + .WithMany() + .HasForeignKey("OrganizationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + + b.Navigation("Order"); + + b.Navigation("OrganizationClient"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.InvoiceLineItem", b => + { + b.HasOne("JobFlow.Domain.Models.Invoice", "Invoice") + .WithMany("LineItems") + .HasForeignKey("InvoiceId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JobFlow.Domain.Models.PriceBookItem", "PriceBookItem") + .WithMany() + .HasForeignKey("PriceBookItemId"); + + b.Navigation("Invoice"); + + b.Navigation("PriceBookItem"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Job", b => + { + b.HasOne("JobFlow.Domain.Models.Estimate", "Estimate") + .WithMany() + .HasForeignKey("EstimateId"); + + b.HasOne("JobFlow.Domain.Models.OrganizationClient", "OrganizationClient") + .WithMany("Jobs") + .HasForeignKey("OrganizationClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Estimate"); + + b.Navigation("OrganizationClient"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.JobRecurrence", b => + { + b.HasOne("JobFlow.Domain.Models.Job", "Job") + .WithMany() + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.JobTemplate", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade); + + b.HasOne("JobFlow.Domain.Models.OrganizationType", "OrganizationType") + .WithMany() + .HasForeignKey("OrganizationTypeId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Organization"); + + b.Navigation("OrganizationType"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.JobTemplateItem", b => + { + b.HasOne("JobFlow.Domain.Models.JobTemplate", "Template") + .WithMany("Items") + .HasForeignKey("TemplateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Template"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.JobTracking", b => + { + b.HasOne("JobFlow.Domain.Models.User", "Employee") + .WithMany() + .HasForeignKey("EmployeeId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JobFlow.Domain.Models.Job", "Job") + .WithMany("JobTrackings") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Employee"); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.JobUpdate", b => + { + b.HasOne("JobFlow.Domain.Models.Job", "Job") + .WithMany("JobUpdates") + .HasForeignKey("JobId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Job"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.JobUpdateAttachment", b => + { + b.HasOne("JobFlow.Domain.Models.JobUpdate", "JobUpdate") + .WithMany("Attachments") + .HasForeignKey("JobUpdateId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("JobUpdate"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Message", b => + { + b.HasOne("JobFlow.Domain.Models.Conversation", "Conversation") + .WithMany("Messages") + .HasForeignKey("ConversationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JobFlow.Domain.Models.User", "Sender") + .WithMany() + .HasForeignKey("SenderId") + .OnDelete(DeleteBehavior.Restrict); + + b.Navigation("Conversation"); + + b.Navigation("Sender"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Order", b => + { + b.HasOne("JobFlow.Domain.Models.OrganizationClient", "OrganizationClient") + .WithMany() + .HasForeignKey("OrganizationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationClient"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Organization", b => + { + b.HasOne("JobFlow.Domain.Models.OrganizationType", "OrganizationType") + .WithMany() + .HasForeignKey("OrganizationTypeId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("OrganizationType"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationBranding", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationClient", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationClientPortalSession", b => + { + b.HasOne("JobFlow.Domain.Models.OrganizationClient", "OrganizationClient") + .WithMany() + .HasForeignKey("OrganizationClientId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("OrganizationClient"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationOnboardingEvent", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationOnboardingStep", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany("OnboardingSteps") + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationService", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.PaymentHistory", b => + { + b.HasOne("JobFlow.Domain.Models.Invoice", "Invoice") + .WithMany("Payments") + .HasForeignKey("InvoiceId"); + + b.Navigation("Invoice"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.PriceBookItem", b => + { + b.HasOne("JobFlow.Domain.Models.PriceBookCategory", "Category") + .WithMany("Items") + .HasForeignKey("CategoryId") + .OnDelete(DeleteBehavior.SetNull); + + b.HasOne("JobFlow.Domain.Models.InventoryItem", "InventoryItem") + .WithMany() + .HasForeignKey("InventoryItemId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Category"); + + b.Navigation("InventoryItem"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SubscriptionRecord", b => + { + b.HasOne("JobFlow.Domain.Models.CustomerPaymentProfile", "PaymentProfile") + .WithMany() + .HasForeignKey("PaymentProfileId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("PaymentProfile"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SupportChatMessage", b => + { + b.HasOne("JobFlow.Domain.Models.SupportChatSession", "Session") + .WithMany("Messages") + .HasForeignKey("SessionId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Session"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SupportChatSession", b => + { + b.HasOne("JobFlow.Domain.Models.User", "AssignedRep") + .WithMany() + .HasForeignKey("AssignedRepId") + .OnDelete(DeleteBehavior.NoAction); + + b.HasOne("JobFlow.Domain.Models.User", "Customer") + .WithMany() + .HasForeignKey("CustomerId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("AssignedRep"); + + b.Navigation("Customer"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SupportHubSession", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SupportHubTicket", b => + { + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.User", b => + { + b.HasOne("JobFlow.Domain.Models.OrganizationClient", "Client") + .WithMany() + .HasForeignKey("ClientId"); + + b.HasOne("JobFlow.Domain.Models.Organization", "Organization") + .WithMany() + .HasForeignKey("OrganizationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Client"); + + b.Navigation("Organization"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.UserRole", b => + { + b.HasOne("JobFlow.Domain.Models.SystemRole", "Role") + .WithMany("UserRoles") + .HasForeignKey("RoleId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("JobFlow.Domain.Models.User", "User") + .WithMany("UserRoles") + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Role"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Assignment", b => + { + b.Navigation("AssignmentAssignees"); + + b.Navigation("AssignmentOrders"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.ClientImportJob", b => + { + b.Navigation("Errors"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.ClientImportUploadSession", b => + { + b.Navigation("Rows"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Conversation", b => + { + b.Navigation("Messages"); + + b.Navigation("Participants"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeImportJob", b => + { + b.Navigation("Errors"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeImportUploadSession", b => + { + b.Navigation("Rows"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeRole", b => + { + b.Navigation("Employees"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EmployeeRolePreset", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Estimate", b => + { + b.Navigation("LineItems"); + + b.Navigation("RevisionRequests"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.EstimateRevisionRequest", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.FollowUpRun", b => + { + b.Navigation("ExecutionLogs"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.FollowUpSequence", b => + { + b.Navigation("Steps"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Invoice", b => + { + b.Navigation("LineItems"); + + b.Navigation("Payments"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Job", b => + { + b.Navigation("Assignments"); + + b.Navigation("JobTrackings"); + + b.Navigation("JobUpdates"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.JobTemplate", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.JobUpdate", b => + { + b.Navigation("Attachments"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Order", b => + { + b.Navigation("AssignmentOrders"); + + b.Navigation("Invoices"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Organization", b => + { + b.Navigation("EmployeeRoles"); + + b.Navigation("Employees"); + + b.Navigation("OnboardingSteps"); + + b.Navigation("PaymentProfiles"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.OrganizationClient", b => + { + b.Navigation("Jobs"); + + b.Navigation("PaymentProfiles"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.PriceBookCategory", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SupportChatSession", b => + { + b.Navigation("Messages"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.SystemRole", b => + { + b.Navigation("UserRoles"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.User", b => + { + b.Navigation("Employees"); + + b.Navigation("UserRoles"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/JobFlow.Infrastructure.Persistence/Migrations/20260425182801_Sprint4_MilestoneAndCompanion.cs b/JobFlow.Infrastructure.Persistence/Migrations/20260425182801_Sprint4_MilestoneAndCompanion.cs new file mode 100644 index 0000000..472877e --- /dev/null +++ b/JobFlow.Infrastructure.Persistence/Migrations/20260425182801_Sprint4_MilestoneAndCompanion.cs @@ -0,0 +1,64 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace JobFlow.Infrastructure.Persistence.Migrations +{ + /// + public partial class Sprint4_MilestoneAndCompanion : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "FirstRealEstimateSentAt", + table: "Organization", + type: "datetimeoffset", + nullable: true); + + migrationBuilder.AddColumn( + name: "ReferralCtaShownAt", + table: "Organization", + type: "datetimeoffset", + nullable: true); + + migrationBuilder.CreateTable( + name: "SetupCompanionEvents", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + OrganizationId = table.Column(type: "uniqueidentifier", nullable: false), + SessionId = table.Column(type: "nvarchar(max)", nullable: false), + QuestionKey = table.Column(type: "nvarchar(max)", nullable: false), + AnswerKey = table.Column(type: "nvarchar(max)", nullable: true), + OccurredAt = table.Column(type: "datetimeoffset", nullable: false), + CreatedBy = table.Column(type: "nvarchar(max)", nullable: true), + UpdatedBy = table.Column(type: "nvarchar(max)", nullable: true), + CreatedAt = table.Column(type: "datetime2", nullable: false), + UpdatedAt = table.Column(type: "datetime2", nullable: true), + IsActive = table.Column(type: "bit", nullable: false), + DeactivatedAtUtc = table.Column(type: "datetime2", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_SetupCompanionEvents", x => x.Id); + }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "SetupCompanionEvents"); + + migrationBuilder.DropColumn( + name: "FirstRealEstimateSentAt", + table: "Organization"); + + migrationBuilder.DropColumn( + name: "ReferralCtaShownAt", + table: "Organization"); + } + } +} diff --git a/JobFlow.Infrastructure.Persistence/Migrations/JobFlowDbContextModelSnapshot.cs b/JobFlow.Infrastructure.Persistence/Migrations/JobFlowDbContextModelSnapshot.cs index 2b399f3..24d1c42 100644 --- a/JobFlow.Infrastructure.Persistence/Migrations/JobFlowDbContextModelSnapshot.cs +++ b/JobFlow.Infrastructure.Persistence/Migrations/JobFlowDbContextModelSnapshot.cs @@ -4025,6 +4025,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("EnableTax") .HasColumnType("bit"); + b.Property("FirstRealEstimateSentAt") + .HasColumnType("datetimeoffset"); + b.Property("HasFreeAccount") .HasColumnType("bit"); @@ -4074,6 +4077,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("PhoneNumber") .HasColumnType("nvarchar(max)"); + b.Property("ReferralCtaShownAt") + .HasColumnType("datetimeoffset"); + b.Property("SquareMerchantId") .HasColumnType("nvarchar(max)"); @@ -4880,6 +4886,52 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("SecurityAlert", "security"); }); + modelBuilder.Entity("JobFlow.Domain.Models.SetupCompanionEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uniqueidentifier"); + + b.Property("AnswerKey") + .HasColumnType("nvarchar(max)"); + + b.Property("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("IsActive") + .HasColumnType("bit"); + + b.Property("OccurredAt") + .HasColumnType("datetimeoffset"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + b.Property("QuestionKey") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("SessionId") + .IsRequired() + .HasColumnType("nvarchar(max)"); + + b.Property("UpdatedAt") + .HasColumnType("datetime2"); + + b.Property("UpdatedBy") + .HasColumnType("nvarchar(max)"); + + b.HasKey("Id"); + + b.ToTable("SetupCompanionEvents"); + }); + modelBuilder.Entity("JobFlow.Domain.Models.ShortLink", b => { b.Property("Id") From 9a0c26d107b9bb30b2cf40f186e34beb90440259 Mon Sep 17 00:00:00 2001 From: Jerry Phillips Date: Sat, 25 Apr 2026 16:04:11 -0400 Subject: [PATCH 2/2] feat(api): [AB#104] LLM-backed setup companion ask endpoint --- .../Controllers/SetupCompanionController.cs | 22 +++++ JobFlow.API/Program.cs | 21 +++++ .../ConfigurationSettings/OpenAiSettings.cs | 7 ++ JobFlow.Business/JobFlow.Business.csproj | 1 + .../ISetupCompanionService.cs | 1 + .../Services/SetupCompanionService.cs | 86 ++++++++++++++++++- 6 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 JobFlow.Business/ConfigurationSettings/OpenAiSettings.cs diff --git a/JobFlow.API/Controllers/SetupCompanionController.cs b/JobFlow.API/Controllers/SetupCompanionController.cs index b625d87..dafab75 100644 --- a/JobFlow.API/Controllers/SetupCompanionController.cs +++ b/JobFlow.API/Controllers/SetupCompanionController.cs @@ -3,6 +3,7 @@ using JobFlow.Business.Services.ServiceInterfaces; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.RateLimiting; namespace JobFlow.API.Controllers; @@ -30,9 +31,30 @@ public async Task TrackEvent([FromBody] TrackSetupCompanionEventRequest return result.IsSuccess ? Results.Ok() : result.ToProblemDetails(); } + + [HttpPost("ask")] + [EnableRateLimiting("companion-ask")] + public async Task Ask([FromBody] AskSetupCompanionRequest request) + { + var organizationId = HttpContext.GetOrganizationId(); + var result = await _setupCompanionService.AskAsync( + organizationId, + request.SessionId, + request.Question, + request.CurrentRoute); + + return result.IsSuccess + ? Results.Ok(new { answer = result.Value }) + : result.ToProblemDetails(); + } } public sealed record TrackSetupCompanionEventRequest( string SessionId, string QuestionKey, string? AnswerKey); + +public sealed record AskSetupCompanionRequest( + string SessionId, + string Question, + string CurrentRoute); diff --git a/JobFlow.API/Program.cs b/JobFlow.API/Program.cs index c7e7330..852d0f6 100644 --- a/JobFlow.API/Program.cs +++ b/JobFlow.API/Program.cs @@ -459,6 +459,21 @@ static JsonDocument ParseFirebaseAdminSdkJson(string json) AutoReplenishment = true }); }); + + // LLM-backed setup companion — 20 requests per hour per org to control OpenAI spend + options.AddPolicy("companion-ask", context => + { + var orgId = context.User?.FindFirst("org_id")?.Value + ?? context.Connection.RemoteIpAddress?.ToString() + ?? "anonymous"; + return RateLimitPartition.GetFixedWindowLimiter($"companion:{orgId}", _ => new FixedWindowRateLimiterOptions + { + PermitLimit = 20, + Window = TimeSpan.FromHours(1), + QueueLimit = 0, + AutoReplenishment = true + }); + }); }); // ============================================================ @@ -521,6 +536,12 @@ static JsonDocument ParseFirebaseAdminSdkJson(string json) builder.Services.AddSingleton(sp => sp.GetRequiredService>().Value); builder.Services.AddSingleton(sp => sp.GetRequiredService>().Value); +builder.Services.Configure(options => +{ + options.ApiKey = builder.Configuration["OpenAI-ApiKey"] ?? ""; + options.Model = builder.Configuration["OpenAI-Model"] ?? "gpt-4o-mini"; +}); + // ============================================================ // DEPENDENCY INJECTION, MAPPINGS, AUTHORIZATION // ============================================================ diff --git a/JobFlow.Business/ConfigurationSettings/OpenAiSettings.cs b/JobFlow.Business/ConfigurationSettings/OpenAiSettings.cs new file mode 100644 index 0000000..d325c07 --- /dev/null +++ b/JobFlow.Business/ConfigurationSettings/OpenAiSettings.cs @@ -0,0 +1,7 @@ +namespace JobFlow.Business.ConfigurationSettings; + +public class OpenAiSettings +{ + public string ApiKey { get; set; } = string.Empty; + public string Model { get; set; } = "gpt-4o-mini"; +} diff --git a/JobFlow.Business/JobFlow.Business.csproj b/JobFlow.Business/JobFlow.Business.csproj index 23da398..aa81bf0 100644 --- a/JobFlow.Business/JobFlow.Business.csproj +++ b/JobFlow.Business/JobFlow.Business.csproj @@ -16,6 +16,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/JobFlow.Business/Services/ServiceInterfaces/ISetupCompanionService.cs b/JobFlow.Business/Services/ServiceInterfaces/ISetupCompanionService.cs index bb6e787..1e14088 100644 --- a/JobFlow.Business/Services/ServiceInterfaces/ISetupCompanionService.cs +++ b/JobFlow.Business/Services/ServiceInterfaces/ISetupCompanionService.cs @@ -5,4 +5,5 @@ namespace JobFlow.Business.Services.ServiceInterfaces; public interface ISetupCompanionService { Task TrackEventAsync(Guid organizationId, string sessionId, string questionKey, string? answerKey); + Task> AskAsync(Guid organizationId, string sessionId, string question, string currentRoute); } diff --git a/JobFlow.Business/Services/SetupCompanionService.cs b/JobFlow.Business/Services/SetupCompanionService.cs index 8479259..317b4ad 100644 --- a/JobFlow.Business/Services/SetupCompanionService.cs +++ b/JobFlow.Business/Services/SetupCompanionService.cs @@ -1,7 +1,10 @@ +using JobFlow.Business.ConfigurationSettings; using JobFlow.Business.DI; using JobFlow.Business.Services.ServiceInterfaces; using JobFlow.Domain; using JobFlow.Domain.Models; +using Microsoft.Extensions.Options; +using OpenAI.Chat; namespace JobFlow.Business.Services; @@ -9,10 +12,12 @@ namespace JobFlow.Business.Services; public class SetupCompanionService : ISetupCompanionService { private readonly IUnitOfWork _unitOfWork; + private readonly OpenAiSettings _openAiSettings; - public SetupCompanionService(IUnitOfWork unitOfWork) + public SetupCompanionService(IUnitOfWork unitOfWork, IOptions openAiOptions) { _unitOfWork = unitOfWork; + _openAiSettings = openAiOptions.Value; } public async Task TrackEventAsync(Guid organizationId, string sessionId, string questionKey, string? answerKey) @@ -30,4 +35,83 @@ public async Task TrackEventAsync(Guid organizationId, string sessionId, await _unitOfWork.SaveChangesAsync(); return Result.Success(); } + + public async Task> AskAsync(Guid organizationId, string sessionId, string question, string currentRoute) + { + if (string.IsNullOrWhiteSpace(_openAiSettings.ApiKey)) + return Result.Failure(Error.Failure("Companion.NotConfigured", "AI companion is not configured.")); + + var org = await _unitOfWork.RepositoryOf().GetByIdAsync(organizationId); + if (org is null) + return Result.Failure(Error.NotFound("Organization.NotFound", "Organization not found.")); + + // Track the free-text ask as an analytics event + var ev = new SetupCompanionEvent + { + OrganizationId = organizationId, + SessionId = sessionId, + QuestionKey = "free-text", + AnswerKey = null, + OccurredAt = DateTimeOffset.UtcNow + }; + await _unitOfWork.RepositoryOf().AddAsync(ev); + await _unitOfWork.SaveChangesAsync(); + + var systemPrompt = BuildSystemPrompt(org, currentRoute); + + var client = new ChatClient(_openAiSettings.Model, _openAiSettings.ApiKey); + + var messages = new List + { + new SystemChatMessage(systemPrompt), + new UserChatMessage(question) + }; + + var response = await client.CompleteChatAsync(messages, new ChatCompletionOptions + { + MaxOutputTokenCount = 400, + Temperature = 0.3f + }); + + var answer = response.Value.Content[0].Text; + return Result.Success(answer); + } + + private static string BuildSystemPrompt(Organization org, string currentRoute) + { + var onboardingStatus = org.OnBoardingComplete + ? "Onboarding complete." + : "Onboarding not yet complete."; + + var paymentStatus = org.CanAcceptPayments + ? "Payment processing is connected." + : org.PaymentSetupDeferred + ? "Payment setup is deferred — not yet connected." + : "Payment processing is not connected."; + + var industry = string.IsNullOrWhiteSpace(org.IndustryKey) ? "general" : org.IndustryKey; + var plan = string.IsNullOrWhiteSpace(org.SubscriptionPlanName) ? "Go" : org.SubscriptionPlanName; + + return $""" + You are the JobFlow Setup Companion — a friendly, concise assistant embedded inside the JobFlow app. + JobFlow is a field service management platform for small businesses (contractors, cleaning companies, landscapers, IT services, HVAC, and similar trades). + + Your sole purpose is to help users understand and set up JobFlow. You answer ONLY questions about: + - How JobFlow features work (jobs, estimates, invoices, scheduling, clients, employees, pricebook, payments, branding) + - What the user should do next in their setup + - Whether a step is required or optional for them + + If asked anything unrelated to JobFlow, politely redirect: "I can only help with JobFlow setup questions." + + Current user context: + - Industry: {industry} + - Plan: {plan} + - {onboardingStatus} + - {paymentStatus} + - Current page in the app: {currentRoute} + + Respond in plain, friendly language. Keep answers under 100 words. Use bullet points only when listing steps. + Never fabricate features that don't exist in JobFlow. + """; + } }