From b872030700b5ba0902e285f9bf5b241cf6565506 Mon Sep 17 00:00:00 2001 From: Jerry Phillips Date: Fri, 27 Mar 2026 13:26:06 -0400 Subject: [PATCH] feat(import): Data Import and Export --- .../Controllers/DataExportController.cs | 198 + .../OrganizationClientController.cs | 145 +- JobFlow.API/JobFlow.API.csproj | 1 + JobFlow.API/Models/ClientImportDtos.cs | 79 + JobFlow.API/Models/DataExportDtos.cs | 78 + JobFlow.API/Program.cs | 5 + .../Services/ClientImportCsvService.cs | 152 + JobFlow.API/Services/ClientImportProcessor.cs | 334 ++ .../ClientImportUploadSessionService.cs | 131 + .../Services/DataExportBuilderService.cs | 193 + .../Services/DataExportJobProcessor.cs | 67 + JobFlow.Domain/Models/ClientImportJob.cs | 18 + JobFlow.Domain/Models/ClientImportJobError.cs | 10 + .../Models/ClientImportUploadRow.cs | 10 + .../Models/ClientImportUploadSession.cs | 14 + JobFlow.Domain/Models/DataExportJob.cs | 18 + .../ClientImportJobConfiguration.cs | 33 + .../ClientImportJobErrorConfiguration.cs | 26 + .../ClientImportUploadRowConfiguration.cs | 25 + .../ClientImportUploadSessionConfiguration.cs | 29 + .../DataExportJobConfiguration.cs | 35 + .../JobFlowDbContext.cs | 6 +- ...735_AddClientImportJobTracking.Designer.cs | 3735 ++++++++++++++++ ...260327163735_AddClientImportJobTracking.cs | 104 + ...ImportUploadSessionPersistence.Designer.cs | 3857 ++++++++++++++++ ...AddClientImportUploadSessionPersistence.cs | 91 + ...7165440_AddAsyncDataExportJobs.Designer.cs | 3939 +++++++++++++++++ .../20260327165440_AddAsyncDataExportJobs.cs | 66 + .../JobFlowDbContextModelSnapshot.cs | 343 ++ JobFlow.Tests/JobFlow.Tests.csproj | 1 + ...onClientControllerSwaggerSignatureTests.cs | 34 + 31 files changed, 13775 insertions(+), 2 deletions(-) create mode 100644 JobFlow.API/Controllers/DataExportController.cs create mode 100644 JobFlow.API/Models/ClientImportDtos.cs create mode 100644 JobFlow.API/Models/DataExportDtos.cs create mode 100644 JobFlow.API/Services/ClientImportCsvService.cs create mode 100644 JobFlow.API/Services/ClientImportProcessor.cs create mode 100644 JobFlow.API/Services/ClientImportUploadSessionService.cs create mode 100644 JobFlow.API/Services/DataExportBuilderService.cs create mode 100644 JobFlow.API/Services/DataExportJobProcessor.cs create mode 100644 JobFlow.Domain/Models/ClientImportJob.cs create mode 100644 JobFlow.Domain/Models/ClientImportJobError.cs create mode 100644 JobFlow.Domain/Models/ClientImportUploadRow.cs create mode 100644 JobFlow.Domain/Models/ClientImportUploadSession.cs create mode 100644 JobFlow.Domain/Models/DataExportJob.cs create mode 100644 JobFlow.Infrastructure.Persistence/Configurations/ClientImportJobConfiguration.cs create mode 100644 JobFlow.Infrastructure.Persistence/Configurations/ClientImportJobErrorConfiguration.cs create mode 100644 JobFlow.Infrastructure.Persistence/Configurations/ClientImportUploadRowConfiguration.cs create mode 100644 JobFlow.Infrastructure.Persistence/Configurations/ClientImportUploadSessionConfiguration.cs create mode 100644 JobFlow.Infrastructure.Persistence/Configurations/DataExportJobConfiguration.cs create mode 100644 JobFlow.Infrastructure.Persistence/Migrations/20260327163735_AddClientImportJobTracking.Designer.cs create mode 100644 JobFlow.Infrastructure.Persistence/Migrations/20260327163735_AddClientImportJobTracking.cs create mode 100644 JobFlow.Infrastructure.Persistence/Migrations/20260327164425_AddClientImportUploadSessionPersistence.Designer.cs create mode 100644 JobFlow.Infrastructure.Persistence/Migrations/20260327164425_AddClientImportUploadSessionPersistence.cs create mode 100644 JobFlow.Infrastructure.Persistence/Migrations/20260327165440_AddAsyncDataExportJobs.Designer.cs create mode 100644 JobFlow.Infrastructure.Persistence/Migrations/20260327165440_AddAsyncDataExportJobs.cs create mode 100644 JobFlow.Tests/OrganizationClientControllerSwaggerSignatureTests.cs diff --git a/JobFlow.API/Controllers/DataExportController.cs b/JobFlow.API/Controllers/DataExportController.cs new file mode 100644 index 0000000..fda6e11 --- /dev/null +++ b/JobFlow.API/Controllers/DataExportController.cs @@ -0,0 +1,198 @@ +using Hangfire; +using JobFlow.API.Extensions; +using JobFlow.API.Models; +using JobFlow.API.Services; +using JobFlow.Business.Services.ServiceInterfaces; +using JobFlow.Domain.Models; +using JobFlow.Infrastructure.Persistence; +using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; + +namespace JobFlow.API.Controllers; + +[ApiController] +[Route("api/data-export")] +public class DataExportController : ControllerBase +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly DataExportBuilderService _builder; + private readonly IOrganizationService _organizations; + + public DataExportController( + IDbContextFactory dbContextFactory, + DataExportBuilderService builder, + IOrganizationService organizations) + { + _dbContextFactory = dbContextFactory; + _builder = builder; + _organizations = organizations; + } + + [HttpGet("json")] + public async Task ExportOrganizationDataJson(CancellationToken cancellationToken) + { + var organizationId = HttpContext.GetOrganizationId(); + var (bytes, fileName) = await _builder.BuildJsonBundleAsync(organizationId, cancellationToken); + + return Results.File(bytes, "application/json", fileName); + } + + [HttpGet("clients.csv")] + public async Task ExportClientsCsv(CancellationToken cancellationToken) + { + var organizationId = HttpContext.GetOrganizationId(); + var (bytes, fileName) = await _builder.BuildClientsCsvAsync(organizationId, cancellationToken); + + return Results.File(bytes, "text/csv", fileName); + } + + [HttpPost("jobs")] + public async Task StartDataExportJob(CancellationToken cancellationToken) + { + var organizationId = HttpContext.GetOrganizationId(); + var userId = HttpContext.GetUserId(); + + var orgResult = await _organizations.GetOrganizationDtoById(organizationId); + if (orgResult.IsFailure) + { + return Results.Problem(statusCode: 404, title: "Organization not found", detail: "Organization context is invalid."); + } + + if (!HasMinPlan(orgResult.Value.SubscriptionPlanName, "Flow")) + { + return Results.Problem( + statusCode: StatusCodes.Status403Forbidden, + title: "Subscription Required", + detail: "A Flow plan is required for async ZIP data exports."); + } + + var jobId = Guid.NewGuid(); + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var activeJob = await dbContext.Set() + .AsNoTracking() + .FirstOrDefaultAsync(x => x.OrganizationId == organizationId && (x.Status == "queued" || x.Status == "running"), cancellationToken); + + if (activeJob is not null) + { + return Results.Ok(new StartDataExportJobResponse { JobId = activeJob.Id.ToString("N") }); + } + + dbContext.Set().Add(new DataExportJob + { + Id = jobId, + OrganizationId = organizationId, + RequestedByUserId = userId, + Status = "queued", + CreatedAt = DateTime.UtcNow, + IsActive = true + }); + + await dbContext.SaveChangesAsync(cancellationToken); + + BackgroundJob.Enqueue(x => x.ProcessAsync(jobId, organizationId)); + + return Results.Ok(new StartDataExportJobResponse { JobId = jobId.ToString("N") }); + } + + [HttpGet("jobs")] + public async Task GetDataExportJobs(CancellationToken cancellationToken) + { + var organizationId = HttpContext.GetOrganizationId(); + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var jobs = await dbContext.Set() + .AsNoTracking() + .Where(x => x.OrganizationId == organizationId) + .OrderByDescending(x => x.CreatedAt) + .Take(20) + .Select(x => new DataExportJobStatusResponse + { + JobId = x.Id.ToString("N"), + Status = x.Status, + ErrorMessage = x.ErrorMessage, + FileName = x.FileName, + ContentType = x.ContentType, + StartedAtUtc = x.StartedAtUtc, + CompletedAtUtc = x.CompletedAtUtc, + ExpiresAtUtc = x.ExpiresAtUtc, + DownloadCount = x.DownloadCount + }) + .ToListAsync(cancellationToken); + + return Results.Ok(jobs); + } + + [HttpGet("jobs/{jobId}")] + public async Task GetDataExportJobStatus(string jobId, CancellationToken cancellationToken) + { + if (!Guid.TryParse(jobId, out var parsedJobId)) + return Results.BadRequest("Invalid export job id."); + + var organizationId = HttpContext.GetOrganizationId(); + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var job = await dbContext.Set() + .AsNoTracking() + .Where(x => x.Id == parsedJobId && x.OrganizationId == organizationId) + .Select(x => new DataExportJobStatusResponse + { + JobId = x.Id.ToString("N"), + Status = x.Status, + ErrorMessage = x.ErrorMessage, + FileName = x.FileName, + ContentType = x.ContentType, + StartedAtUtc = x.StartedAtUtc, + CompletedAtUtc = x.CompletedAtUtc, + ExpiresAtUtc = x.ExpiresAtUtc, + DownloadCount = x.DownloadCount + }) + .FirstOrDefaultAsync(cancellationToken); + + return job is null ? Results.NotFound() : Results.Ok(job); + } + + [HttpGet("jobs/{jobId}/download")] + public async Task DownloadDataExportJobFile(string jobId, CancellationToken cancellationToken) + { + if (!Guid.TryParse(jobId, out var parsedJobId)) + return Results.BadRequest("Invalid export job id."); + + var organizationId = HttpContext.GetOrganizationId(); + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var job = await dbContext.Set() + .FirstOrDefaultAsync(x => x.Id == parsedJobId && x.OrganizationId == organizationId, cancellationToken); + + if (job is null) + return Results.NotFound(); + + if (job.Status != "completed" || job.FileContent is null || string.IsNullOrWhiteSpace(job.FileName)) + return Results.Conflict(new { message = "Export file is not ready yet." }); + + if (job.ExpiresAtUtc.HasValue && job.ExpiresAtUtc.Value < DateTime.UtcNow) + return Results.StatusCode(StatusCodes.Status410Gone); + + job.DownloadCount += 1; + await dbContext.SaveChangesAsync(cancellationToken); + + return Results.File(job.FileContent, job.ContentType ?? "application/octet-stream", job.FileName); + } + + private static bool HasMinPlan(string? planName, string required) + { + static int Rank(string? plan) + { + var value = (plan ?? string.Empty).Trim().ToLowerInvariant(); + return value switch + { + "go" => 0, + "flow" => 1, + "max" => 2, + _ => -1 + }; + } + + return Rank(planName) >= Rank(required); + } +} diff --git a/JobFlow.API/Controllers/OrganizationClientController.cs b/JobFlow.API/Controllers/OrganizationClientController.cs index 65b1312..0093c73 100644 --- a/JobFlow.API/Controllers/OrganizationClientController.cs +++ b/JobFlow.API/Controllers/OrganizationClientController.cs @@ -1,13 +1,17 @@ using JobFlow.API.Extensions; using JobFlow.API.Mappings; +using JobFlow.API.Services; using JobFlow.Business; using JobFlow.API.Models; using JobFlow.Business.Extensions; using JobFlow.Business.Models.DTOs; using JobFlow.Business.Services.ServiceInterfaces; using JobFlow.Domain.Models; +using Hangfire; using MapsterMapper; using Microsoft.AspNetCore.Mvc; +using Microsoft.EntityFrameworkCore; +using JobFlow.Infrastructure.Persistence; namespace JobFlow.API.Controllers; @@ -18,15 +22,24 @@ public class OrganizationClientController : ControllerBase private readonly IOrganizationClientService organizationClientService; private readonly IOrganizationClientPortalService _clientPortal; private readonly IMapper _mapper; + private readonly ClientImportCsvService _csvImportService; + private readonly ClientImportUploadSessionService _uploadSessionService; + private readonly IDbContextFactory _dbContextFactory; public OrganizationClientController( IOrganizationClientService organizationClientService, IOrganizationClientPortalService clientPortal, - IMapper mapper) + IMapper mapper, + ClientImportCsvService csvImportService, + ClientImportUploadSessionService uploadSessionService, + IDbContextFactory dbContextFactory) { this.organizationClientService = organizationClientService; _clientPortal = clientPortal; _mapper = mapper; + _csvImportService = csvImportService; + _uploadSessionService = uploadSessionService; + _dbContextFactory = dbContextFactory; } [HttpGet] @@ -121,4 +134,134 @@ public async Task RestoreClient(Guid clientId) var result = await organizationClientService.RestoreClient(clientId, organizationId); return result.IsSuccess ? Results.Ok(result) : result.ToProblemDetails(); } + + [HttpPost("import/preview")] + [RequestSizeLimit(10 * 1024 * 1024)] + [Consumes("multipart/form-data")] + public async Task PreviewClientImport([FromForm] PreviewClientImportRequest request, CancellationToken cancellationToken) + { + var file = request.File; + if (file is null) + return Results.BadRequest("A CSV file is required."); + + if (!file.FileName.EndsWith(".csv", StringComparison.OrdinalIgnoreCase)) + return Results.BadRequest("Only CSV files are supported in this version."); + + try + { + var organizationId = HttpContext.GetOrganizationId(); + var parsed = await _csvImportService.ParseAsync(file, cancellationToken); + var source = string.IsNullOrWhiteSpace(request.SourceSystem) ? "csv" : request.SourceSystem.Trim(); + var uploadSessionId = await _uploadSessionService.SaveAsync(organizationId, source, parsed.Rows, cancellationToken); + + var previewRows = parsed.Rows + .Take(25) + .Select(r => r.ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase)) + .ToList(); + + var response = new ClientImportPreviewResponse + { + UploadToken = uploadSessionId.ToString("N"), + SourceSystem = source, + SourceColumns = parsed.Headers, + SuggestedMappings = parsed.SuggestedMappings, + PreviewRows = previewRows, + TotalRows = parsed.Rows.Count + }; + + return Results.Ok(response); + } + catch (Exception ex) + { + return Results.BadRequest(ex.Message); + } + } + + [HttpPost("import/start")] + public async Task StartClientImport([FromBody] StartClientImportRequest request) + { + if (request is null || string.IsNullOrWhiteSpace(request.UploadToken)) + return Results.BadRequest("Upload token is required."); + + if (!Guid.TryParse(request.UploadToken, out var uploadSessionId)) + return Results.BadRequest("Invalid upload token format."); + + var organizationId = HttpContext.GetOrganizationId(); + var uploadSession = await _uploadSessionService.GetActiveSessionAsync(uploadSessionId, organizationId, CancellationToken.None); + if (uploadSession is null) + return Results.BadRequest("Import session expired or invalid. Please upload your CSV again."); + + if (request.ColumnMappings.Count == 0) + return Results.BadRequest("At least one column mapping is required."); + + var jobId = Guid.NewGuid(); + var sourceSystem = string.IsNullOrWhiteSpace(request.SourceSystem) ? "csv" : request.SourceSystem.Trim(); + + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var importJob = new ClientImportJob + { + Id = jobId, + OrganizationId = organizationId, + SourceSystem = sourceSystem, + Status = "queued", + TotalRows = uploadSession.TotalRows, + ProcessedRows = 0, + SucceededRows = 0, + FailedRows = 0, + CreatedAt = DateTime.UtcNow, + IsActive = true + }; + + dbContext.Set().Add(importJob); + await dbContext.SaveChangesAsync(); + + BackgroundJob.Enqueue( + processor => processor.ProcessAsync(jobId, organizationId, uploadSessionId, request.ColumnMappings)); + + return Results.Ok(new StartClientImportResponse { JobId = jobId.ToString("N") }); + } + + [HttpGet("import/jobs/{jobId}")] + public async Task GetClientImportStatus(string jobId) + { + if (!Guid.TryParse(jobId, out var parsedJobId)) + return Results.BadRequest("Invalid import job id."); + + var organizationId = HttpContext.GetOrganizationId(); + + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var job = await dbContext.Set() + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == parsedJobId && x.OrganizationId == organizationId); + + if (job is null) + return Results.NotFound(); + + var errors = await dbContext.Set() + .AsNoTracking() + .Where(x => x.ClientImportJobId == parsedJobId) + .OrderBy(x => x.RowNumber) + .Take(100) + .Select(x => new ClientImportErrorItem + { + RowNumber = x.RowNumber, + Message = x.Message + }) + .ToListAsync(); + + var status = new ClientImportJobStatusResponse + { + JobId = job.Id.ToString("N"), + SourceSystem = job.SourceSystem, + Status = job.Status, + TotalRows = job.TotalRows, + ProcessedRows = job.ProcessedRows, + SucceededRows = job.SucceededRows, + FailedRows = job.FailedRows, + ErrorMessage = job.ErrorMessage, + Errors = errors + }; + + return Results.Ok(status); + } } \ No newline at end of file diff --git a/JobFlow.API/JobFlow.API.csproj b/JobFlow.API/JobFlow.API.csproj index 277f2db..0be6300 100644 --- a/JobFlow.API/JobFlow.API.csproj +++ b/JobFlow.API/JobFlow.API.csproj @@ -14,6 +14,7 @@ + diff --git a/JobFlow.API/Models/ClientImportDtos.cs b/JobFlow.API/Models/ClientImportDtos.cs new file mode 100644 index 0000000..670ce47 --- /dev/null +++ b/JobFlow.API/Models/ClientImportDtos.cs @@ -0,0 +1,79 @@ +namespace JobFlow.API.Models; + +public static class ClientImportTargetFields +{ + public const string Ignore = "Ignore"; + public const string FirstName = "FirstName"; + public const string LastName = "LastName"; + public const string FullName = "FullName"; + public const string EmailAddress = "EmailAddress"; + public const string PhoneNumber = "PhoneNumber"; + public const string Address1 = "Address1"; + public const string Address2 = "Address2"; + public const string City = "City"; + public const string State = "State"; + public const string ZipCode = "ZipCode"; + + public static readonly string[] All = + [ + Ignore, + FirstName, + LastName, + FullName, + EmailAddress, + PhoneNumber, + Address1, + Address2, + City, + State, + ZipCode + ]; +} + +public sealed class ClientImportPreviewResponse +{ + public string UploadToken { get; set; } = string.Empty; + public string SourceSystem { get; set; } = "csv"; + public IReadOnlyList SourceColumns { get; set; } = Array.Empty(); + public IReadOnlyDictionary SuggestedMappings { get; set; } = new Dictionary(); + public IReadOnlyList> PreviewRows { get; set; } = Array.Empty>(); + public IReadOnlyList SupportedTargetFields { get; set; } = ClientImportTargetFields.All; + public int TotalRows { get; set; } +} + +public sealed class PreviewClientImportRequest +{ + public IFormFile? File { get; set; } + public string? SourceSystem { get; set; } +} + +public sealed class StartClientImportRequest +{ + public string UploadToken { get; set; } = string.Empty; + public string SourceSystem { get; set; } = "csv"; + public Dictionary ColumnMappings { get; set; } = new(StringComparer.OrdinalIgnoreCase); +} + +public sealed class StartClientImportResponse +{ + public string JobId { get; set; } = string.Empty; +} + +public sealed class ClientImportErrorItem +{ + public int RowNumber { get; set; } + public string Message { get; set; } = string.Empty; +} + +public sealed class ClientImportJobStatusResponse +{ + public string JobId { get; set; } = string.Empty; + public string Status { get; set; } = "queued"; + public string SourceSystem { get; set; } = "csv"; + public int TotalRows { get; set; } + public int ProcessedRows { get; set; } + public int SucceededRows { get; set; } + public int FailedRows { get; set; } + public string? ErrorMessage { get; set; } + public IReadOnlyList Errors { get; set; } = Array.Empty(); +} diff --git a/JobFlow.API/Models/DataExportDtos.cs b/JobFlow.API/Models/DataExportDtos.cs new file mode 100644 index 0000000..c8b6099 --- /dev/null +++ b/JobFlow.API/Models/DataExportDtos.cs @@ -0,0 +1,78 @@ +namespace JobFlow.API.Models; + +public sealed class DataExportBundleDto +{ + public string ExportedAtUtc { get; set; } = string.Empty; + public Guid OrganizationId { get; set; } + public IReadOnlyList Clients { get; set; } = Array.Empty(); + public IReadOnlyList Jobs { get; set; } = Array.Empty(); + public IReadOnlyList Invoices { get; set; } = Array.Empty(); + public IReadOnlyList Employees { get; set; } = Array.Empty(); +} + +public sealed class DataExportClientDto +{ + public Guid Id { get; set; } + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? EmailAddress { get; set; } + public string? PhoneNumber { get; set; } + public string? Address1 { get; set; } + public string? Address2 { get; set; } + public string? City { get; set; } + public string? State { get; set; } + public string? ZipCode { get; set; } +} + +public sealed class DataExportJobDto +{ + public Guid Id { get; set; } + public Guid OrganizationClientId { get; set; } + public string? Title { get; set; } + public string? Comments { get; set; } + public string LifecycleStatus { get; set; } = string.Empty; + public string? InvoicingWorkflow { get; set; } +} + +public sealed class DataExportInvoiceDto +{ + public Guid Id { get; set; } + public string InvoiceNumber { get; set; } = string.Empty; + public Guid OrganizationClientId { get; set; } + public Guid? JobId { get; set; } + public DateTime InvoiceDate { get; set; } + public DateTime DueDate { get; set; } + public decimal TotalAmount { get; set; } + public decimal AmountPaid { get; set; } + public decimal BalanceDue { get; set; } + public string Status { get; set; } = string.Empty; +} + +public sealed class DataExportEmployeeDto +{ + public Guid Id { get; set; } + public string FirstName { get; set; } = string.Empty; + public string LastName { get; set; } = string.Empty; + public string? Email { get; set; } + public string? PhoneNumber { get; set; } + public string RoleName { get; set; } = string.Empty; + public bool IsActive { get; set; } +} + +public sealed class StartDataExportJobResponse +{ + public string JobId { get; set; } = string.Empty; +} + +public sealed class DataExportJobStatusResponse +{ + public string JobId { get; set; } = string.Empty; + public string Status { get; set; } = string.Empty; + public string? ErrorMessage { get; set; } + public string? FileName { get; set; } + public string? ContentType { get; set; } + public DateTime? StartedAtUtc { get; set; } + public DateTime? CompletedAtUtc { get; set; } + public DateTime? ExpiresAtUtc { get; set; } + public int DownloadCount { get; set; } +} diff --git a/JobFlow.API/Program.cs b/JobFlow.API/Program.cs index 53f1ead..57f4aaf 100644 --- a/JobFlow.API/Program.cs +++ b/JobFlow.API/Program.cs @@ -325,6 +325,11 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); builder.Services.AddJobFlowHttpClients(); builder.Services.AddAttributedServices(typeof(IJobFlowHttpClientFactory).Assembly, typeof(IUserService).Assembly); diff --git a/JobFlow.API/Services/ClientImportCsvService.cs b/JobFlow.API/Services/ClientImportCsvService.cs new file mode 100644 index 0000000..5523481 --- /dev/null +++ b/JobFlow.API/Services/ClientImportCsvService.cs @@ -0,0 +1,152 @@ +using System.Globalization; +using System.Text.RegularExpressions; +using CsvHelper; +using CsvHelper.Configuration; +using JobFlow.API.Models; + +namespace JobFlow.API.Services; + +public sealed class ClientImportCsvService +{ + private const int MaxRows = 10000; + + private static readonly Dictionary FieldHints = new(StringComparer.OrdinalIgnoreCase) + { + ["first"] = ClientImportTargetFields.FirstName, + ["firstname"] = ClientImportTargetFields.FirstName, + ["givenname"] = ClientImportTargetFields.FirstName, + ["last"] = ClientImportTargetFields.LastName, + ["lastname"] = ClientImportTargetFields.LastName, + ["surname"] = ClientImportTargetFields.LastName, + ["familyname"] = ClientImportTargetFields.LastName, + ["fullname"] = ClientImportTargetFields.FullName, + ["name"] = ClientImportTargetFields.FullName, + ["clientname"] = ClientImportTargetFields.FullName, + ["customername"] = ClientImportTargetFields.FullName, + ["email"] = ClientImportTargetFields.EmailAddress, + ["emailaddress"] = ClientImportTargetFields.EmailAddress, + ["mail"] = ClientImportTargetFields.EmailAddress, + ["phone"] = ClientImportTargetFields.PhoneNumber, + ["phonenumber"] = ClientImportTargetFields.PhoneNumber, + ["mobile"] = ClientImportTargetFields.PhoneNumber, + ["telephone"] = ClientImportTargetFields.PhoneNumber, + ["address"] = ClientImportTargetFields.Address1, + ["address1"] = ClientImportTargetFields.Address1, + ["street"] = ClientImportTargetFields.Address1, + ["address2"] = ClientImportTargetFields.Address2, + ["unit"] = ClientImportTargetFields.Address2, + ["city"] = ClientImportTargetFields.City, + ["state"] = ClientImportTargetFields.State, + ["province"] = ClientImportTargetFields.State, + ["zip"] = ClientImportTargetFields.ZipCode, + ["zipcode"] = ClientImportTargetFields.ZipCode, + ["postalcode"] = ClientImportTargetFields.ZipCode + }; + + public async Task ParseAsync(IFormFile file, CancellationToken cancellationToken) + { + if (file.Length == 0) + { + throw new InvalidOperationException("The uploaded CSV file is empty."); + } + + await using var fileStream = file.OpenReadStream(); + using var streamReader = new StreamReader(fileStream); + using var csv = new CsvReader(streamReader, new CsvConfiguration(CultureInfo.InvariantCulture) + { + HasHeaderRecord = true, + IgnoreBlankLines = true, + MissingFieldFound = null, + HeaderValidated = null, + BadDataFound = null, + TrimOptions = TrimOptions.Trim + }); + + if (!await csv.ReadAsync()) + { + throw new InvalidOperationException("Could not read the CSV header row."); + } + + csv.ReadHeader(); + + var rawHeaders = csv.HeaderRecord ?? Array.Empty(); + var headers = rawHeaders + .Where(h => !string.IsNullOrWhiteSpace(h)) + .Select(h => h.Trim()) + .Distinct(StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (headers.Count == 0) + { + throw new InvalidOperationException("No valid header columns were found in this CSV file."); + } + + var rows = new List>(capacity: Math.Min(2000, MaxRows)); + + while (await csv.ReadAsync()) + { + cancellationToken.ThrowIfCancellationRequested(); + + if (rows.Count >= MaxRows) + { + throw new InvalidOperationException($"CSV row limit exceeded. Maximum supported rows: {MaxRows}."); + } + + var row = new Dictionary(StringComparer.OrdinalIgnoreCase); + foreach (var header in headers) + { + row[header] = csv.GetField(header)?.Trim(); + } + + rows.Add(row); + } + + var suggestedMappings = BuildSuggestedMappings(headers); + + return new ParsedClientCsv(headers, rows, suggestedMappings); + } + + public static Dictionary BuildSuggestedMappings(IEnumerable headers) + { + var mappings = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var header in headers) + { + var normalized = NormalizeHeader(header); + + var mapped = FieldHints.TryGetValue(normalized, out var exact) + ? exact + : GuessFieldByContains(normalized); + + mappings[header] = mapped ?? ClientImportTargetFields.Ignore; + } + + return mappings; + } + + private static string? GuessFieldByContains(string normalized) + { + if (normalized.Contains("first") && normalized.Contains("name")) return ClientImportTargetFields.FirstName; + if (normalized.Contains("last") && normalized.Contains("name")) return ClientImportTargetFields.LastName; + if (normalized.Contains("full") && normalized.Contains("name")) return ClientImportTargetFields.FullName; + if (normalized.Contains("mail")) return ClientImportTargetFields.EmailAddress; + if (normalized.Contains("phone") || normalized.Contains("mobile") || normalized.Contains("tel")) return ClientImportTargetFields.PhoneNumber; + if (normalized.Contains("address2") || normalized.Contains("suite") || normalized.Contains("unit")) return ClientImportTargetFields.Address2; + if (normalized.Contains("address") || normalized.Contains("street")) return ClientImportTargetFields.Address1; + if (normalized.Contains("city")) return ClientImportTargetFields.City; + if (normalized.Contains("state") || normalized.Contains("province")) return ClientImportTargetFields.State; + if (normalized.Contains("zip") || normalized.Contains("postal")) return ClientImportTargetFields.ZipCode; + + return null; + } + + private static string NormalizeHeader(string header) + { + return Regex.Replace(header, "[^a-zA-Z0-9]", string.Empty).ToLowerInvariant(); + } +} + +public sealed record ParsedClientCsv( + IReadOnlyList Headers, + IReadOnlyList> Rows, + IReadOnlyDictionary SuggestedMappings); diff --git a/JobFlow.API/Services/ClientImportProcessor.cs b/JobFlow.API/Services/ClientImportProcessor.cs new file mode 100644 index 0000000..a214da7 --- /dev/null +++ b/JobFlow.API/Services/ClientImportProcessor.cs @@ -0,0 +1,334 @@ +using JobFlow.API.Models; +using JobFlow.Domain.Models; +using JobFlow.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace JobFlow.API.Services; + +public sealed class ClientImportProcessor +{ + private const int MaxPersistedErrors = 1000; + + private readonly IDbContextFactory _dbContextFactory; + private readonly ClientImportUploadSessionService _uploadSessionService; + private readonly ILogger _logger; + + public ClientImportProcessor( + IDbContextFactory dbContextFactory, + ClientImportUploadSessionService uploadSessionService, + ILogger logger) + { + _dbContextFactory = dbContextFactory; + _uploadSessionService = uploadSessionService; + _logger = logger; + } + + public async Task ProcessAsync( + Guid jobId, + Guid organizationId, + Guid uploadSessionId, + Dictionary columnMappings) + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var importJob = await dbContext.Set() + .FirstOrDefaultAsync(x => x.Id == jobId && x.OrganizationId == organizationId); + + if (importJob is null) + { + return; + } + + importJob.Status = "running"; + importJob.StartedAtUtc ??= DateTime.UtcNow; + importJob.ErrorMessage = null; + await dbContext.SaveChangesAsync(); + + try + { + var session = await _uploadSessionService.GetActiveSessionAsync(uploadSessionId, organizationId, CancellationToken.None); + if (session is null) + { + await MarkFailedAsync(dbContext, importJob, "Import session not found or expired. Please upload your CSV again."); + return; + } + + var clients = dbContext.Set(); + var errors = dbContext.Set(); + + var emailSourceColumn = columnMappings + .FirstOrDefault(x => string.Equals(x.Value, ClientImportTargetFields.EmailAddress, StringComparison.OrdinalIgnoreCase)) + .Key; + + var sessionEmails = new HashSet(StringComparer.OrdinalIgnoreCase); + if (!string.IsNullOrWhiteSpace(emailSourceColumn)) + { + foreach (var row in session.Rows) + { + if (row.Row.TryGetValue(emailSourceColumn, out var email) && !string.IsNullOrWhiteSpace(email)) + { + sessionEmails.Add(email.Trim().ToLowerInvariant()); + } + } + } + + var existingByEmail = new Dictionary(StringComparer.OrdinalIgnoreCase); + if (sessionEmails.Count > 0) + { + var existing = await clients + .Where(c => c.OrganizationId == organizationId + && c.EmailAddress != null + && sessionEmails.Contains(c.EmailAddress.ToLower())) + .ToListAsync(); + + foreach (var row in existing) + { + if (!string.IsNullOrWhiteSpace(row.EmailAddress)) + { + existingByEmail[row.EmailAddress.Trim().ToLowerInvariant()] = row; + } + } + } + + var processedRows = 0; + var succeededRows = 0; + var failedRows = 0; + var persistedErrors = 0; + + for (var index = 0; index < session.Rows.Count; index++) + { + var rowItem = session.Rows[index]; + var rowNumber = rowItem.RowNumber; + var row = rowItem.Row; + processedRows++; + + try + { + var mapped = MapRow(row, columnMappings); + + if (string.IsNullOrWhiteSpace(mapped.FirstName) + && string.IsNullOrWhiteSpace(mapped.LastName) + && string.IsNullOrWhiteSpace(mapped.EmailAddress) + && string.IsNullOrWhiteSpace(mapped.PhoneNumber)) + { + failedRows++; + if (persistedErrors < MaxPersistedErrors) + { + errors.Add(new ClientImportJobError + { + Id = Guid.NewGuid(), + ClientImportJobId = jobId, + RowNumber = rowNumber, + Message = "Row has no usable client data.", + CreatedAt = DateTime.UtcNow, + IsActive = true + }); + persistedErrors++; + } + continue; + } + + OrganizationClient entity; + if (!string.IsNullOrWhiteSpace(mapped.EmailAddress) + && existingByEmail.TryGetValue(mapped.EmailAddress.Trim().ToLowerInvariant(), out var existingClient)) + { + entity = existingClient; + } + else + { + entity = new OrganizationClient + { + Id = Guid.NewGuid(), + OrganizationId = organizationId, + CreatedAt = DateTime.UtcNow, + IsActive = true + }; + await clients.AddAsync(entity); + } + + Merge(entity, mapped); + + if (!string.IsNullOrWhiteSpace(entity.EmailAddress)) + { + existingByEmail[entity.EmailAddress.Trim().ToLowerInvariant()] = entity; + } + + succeededRows++; + } + catch (Exception ex) + { + failedRows++; + if (persistedErrors < MaxPersistedErrors) + { + errors.Add(new ClientImportJobError + { + Id = Guid.NewGuid(), + ClientImportJobId = jobId, + RowNumber = rowNumber, + Message = Truncate(ex.Message, 2000), + CreatedAt = DateTime.UtcNow, + IsActive = true + }); + persistedErrors++; + } + + _logger.LogWarning(ex, "Client import row failed. JobId={JobId}, Row={RowNumber}", jobId, rowNumber); + } + + if (processedRows % 200 == 0) + { + importJob.ProcessedRows = processedRows; + importJob.SucceededRows = succeededRows; + importJob.FailedRows = failedRows; + await dbContext.SaveChangesAsync(); + } + } + + importJob.Status = "completed"; + importJob.ProcessedRows = processedRows; + importJob.SucceededRows = succeededRows; + importJob.FailedRows = failedRows; + importJob.CompletedAtUtc = DateTime.UtcNow; + + await dbContext.SaveChangesAsync(); + await _uploadSessionService.MarkConsumedAsync(uploadSessionId, CancellationToken.None); + } + catch (Exception ex) + { + _logger.LogError(ex, "Client import job failed. JobId={JobId}", jobId); + await MarkFailedAsync(dbContext, importJob, "Import failed unexpectedly. Please retry or contact support."); + } + } + + private static async Task MarkFailedAsync(JobFlowDbContext dbContext, ClientImportJob importJob, string message) + { + importJob.Status = "failed"; + importJob.ErrorMessage = Truncate(message, 2000); + importJob.CompletedAtUtc = DateTime.UtcNow; + await dbContext.SaveChangesAsync(); + } + + private static string Truncate(string value, int maxLength) + { + if (string.IsNullOrEmpty(value) || value.Length <= maxLength) + { + return value; + } + + return value.Substring(0, maxLength); + } + + private static void Merge(OrganizationClient entity, MappedClientRow mapped) + { + entity.FirstName = Prefer(mapped.FirstName, entity.FirstName); + entity.LastName = Prefer(mapped.LastName, entity.LastName); + entity.EmailAddress = Prefer(mapped.EmailAddress, entity.EmailAddress); + entity.PhoneNumber = Prefer(mapped.PhoneNumber, entity.PhoneNumber); + entity.Address1 = Prefer(mapped.Address1, entity.Address1); + entity.Address2 = Prefer(mapped.Address2, entity.Address2); + entity.City = Prefer(mapped.City, entity.City); + entity.State = Prefer(mapped.State, entity.State); + entity.ZipCode = Prefer(mapped.ZipCode, entity.ZipCode); + entity.UpdatedAt = DateTime.UtcNow; + entity.IsActive = true; + entity.DeactivatedAtUtc = null; + } + + private static string? Prefer(string? incoming, string? current) + { + return string.IsNullOrWhiteSpace(incoming) ? current : incoming.Trim(); + } + + private static MappedClientRow MapRow( + Dictionary row, + Dictionary columnMappings) + { + var mapped = new MappedClientRow(); + + foreach (var (sourceColumn, targetField) in columnMappings) + { + if (string.IsNullOrWhiteSpace(targetField) + || string.Equals(targetField, ClientImportTargetFields.Ignore, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (!row.TryGetValue(sourceColumn, out var rawValue)) + { + continue; + } + + var value = rawValue?.Trim(); + if (string.IsNullOrWhiteSpace(value)) + { + continue; + } + + switch (targetField) + { + case ClientImportTargetFields.FirstName: + mapped.FirstName = value; + break; + case ClientImportTargetFields.LastName: + mapped.LastName = value; + break; + case ClientImportTargetFields.FullName: + ApplyFullName(mapped, value); + break; + case ClientImportTargetFields.EmailAddress: + mapped.EmailAddress = value; + break; + case ClientImportTargetFields.PhoneNumber: + mapped.PhoneNumber = value; + break; + case ClientImportTargetFields.Address1: + mapped.Address1 = value; + break; + case ClientImportTargetFields.Address2: + mapped.Address2 = value; + break; + case ClientImportTargetFields.City: + mapped.City = value; + break; + case ClientImportTargetFields.State: + mapped.State = value; + break; + case ClientImportTargetFields.ZipCode: + mapped.ZipCode = value; + break; + } + } + + return mapped; + } + + private static void ApplyFullName(MappedClientRow mapped, string fullName) + { + var parts = fullName + .Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + if (parts.Length == 0) + { + return; + } + + mapped.FirstName ??= parts[0]; + + if (parts.Length > 1) + { + mapped.LastName ??= string.Join(' ', parts.Skip(1)); + } + } + + private sealed class MappedClientRow + { + public string? FirstName { get; set; } + public string? LastName { get; set; } + public string? EmailAddress { get; set; } + public string? PhoneNumber { get; set; } + public string? Address1 { get; set; } + public string? Address2 { get; set; } + public string? City { get; set; } + public string? State { get; set; } + public string? ZipCode { get; set; } + } +} diff --git a/JobFlow.API/Services/ClientImportUploadSessionService.cs b/JobFlow.API/Services/ClientImportUploadSessionService.cs new file mode 100644 index 0000000..742893d --- /dev/null +++ b/JobFlow.API/Services/ClientImportUploadSessionService.cs @@ -0,0 +1,131 @@ +using System.Text.Json; +using JobFlow.Domain.Models; +using JobFlow.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace JobFlow.API.Services; + +public sealed class ClientImportUploadSessionService +{ + private static readonly TimeSpan SessionTtl = TimeSpan.FromMinutes(30); + private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); + + private readonly IDbContextFactory _dbContextFactory; + + public ClientImportUploadSessionService(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + public async Task SaveAsync( + Guid organizationId, + string sourceSystem, + IReadOnlyList> rows, + CancellationToken cancellationToken) + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var now = DateTime.UtcNow; + + var expired = await dbContext.Set() + .Where(x => x.ExpiresAtUtc < now) + .ToListAsync(cancellationToken); + + if (expired.Count > 0) + { + dbContext.Set().RemoveRange(expired); + } + + var sessionId = Guid.NewGuid(); + var session = new ClientImportUploadSession + { + Id = sessionId, + OrganizationId = organizationId, + SourceSystem = sourceSystem, + Status = "active", + TotalRows = rows.Count, + CreatedAt = now, + ExpiresAtUtc = now.Add(SessionTtl), + IsActive = true + }; + + dbContext.Set().Add(session); + + var rowEntities = rows.Select((row, index) => new ClientImportUploadRow + { + Id = Guid.NewGuid(), + ClientImportUploadSessionId = sessionId, + RowNumber = index + 2, + RowDataJson = JsonSerializer.Serialize(row, JsonOptions), + CreatedAt = now, + IsActive = true + }).ToList(); + + dbContext.Set().AddRange(rowEntities); + await dbContext.SaveChangesAsync(cancellationToken); + + return sessionId; + } + + public async Task GetActiveSessionAsync(Guid sessionId, Guid organizationId, CancellationToken cancellationToken) + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var now = DateTime.UtcNow; + var session = await dbContext.Set() + .AsNoTracking() + .FirstOrDefaultAsync(x => x.Id == sessionId && x.OrganizationId == organizationId, cancellationToken); + + if (session is null || session.ExpiresAtUtc < now || !string.Equals(session.Status, "active", StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + var rows = await dbContext.Set() + .AsNoTracking() + .Where(x => x.ClientImportUploadSessionId == sessionId) + .OrderBy(x => x.RowNumber) + .Select(x => new ClientImportUploadRowData( + x.RowNumber, + JsonSerializer.Deserialize>(x.RowDataJson, JsonOptions) + ?? new Dictionary(StringComparer.OrdinalIgnoreCase))) + .ToListAsync(cancellationToken); + + return new ClientImportUploadData( + session.Id, + session.OrganizationId, + session.SourceSystem, + session.TotalRows, + rows); + } + + public async Task MarkConsumedAsync(Guid sessionId, CancellationToken cancellationToken) + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var session = await dbContext.Set() + .FirstOrDefaultAsync(x => x.Id == sessionId, cancellationToken); + + if (session is null) + { + return; + } + + session.Status = "consumed"; + session.ConsumedAtUtc = DateTime.UtcNow; + session.UpdatedAt = DateTime.UtcNow; + + await dbContext.SaveChangesAsync(cancellationToken); + } +} + +public sealed record ClientImportUploadData( + Guid SessionId, + Guid OrganizationId, + string SourceSystem, + int TotalRows, + IReadOnlyList Rows); + +public sealed record ClientImportUploadRowData( + int RowNumber, + Dictionary Row); diff --git a/JobFlow.API/Services/DataExportBuilderService.cs b/JobFlow.API/Services/DataExportBuilderService.cs new file mode 100644 index 0000000..44ae748 --- /dev/null +++ b/JobFlow.API/Services/DataExportBuilderService.cs @@ -0,0 +1,193 @@ +using System.IO.Compression; +using System.Text; +using System.Text.Json; +using JobFlow.API.Models; +using JobFlow.Domain.Models; +using JobFlow.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace JobFlow.API.Services; + +public sealed class DataExportBuilderService +{ + private readonly IDbContextFactory _dbContextFactory; + + public DataExportBuilderService(IDbContextFactory dbContextFactory) + { + _dbContextFactory = dbContextFactory; + } + + public async Task<(byte[] Content, string FileName)> BuildJsonBundleAsync(Guid organizationId, CancellationToken cancellationToken) + { + var export = await BuildExportBundleAsync(organizationId, cancellationToken); + var json = JsonSerializer.Serialize(export, new JsonSerializerOptions { WriteIndented = true }); + var bytes = Encoding.UTF8.GetBytes(json); + var fileName = $"jobflow-data-export-{organizationId:N}-{DateTime.UtcNow:yyyyMMddHHmmss}.json"; + return (bytes, fileName); + } + + public async Task<(byte[] Content, string FileName)> BuildClientsCsvAsync(Guid organizationId, CancellationToken cancellationToken) + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var clients = await dbContext.Set() + .AsNoTracking() + .Where(x => x.OrganizationId == organizationId) + .OrderBy(x => x.LastName) + .ThenBy(x => x.FirstName) + .ToListAsync(cancellationToken); + + var sb = new StringBuilder(); + sb.AppendLine("Id,FirstName,LastName,EmailAddress,PhoneNumber,Address1,Address2,City,State,ZipCode"); + + foreach (var c in clients) + { + sb.AppendLine(string.Join(',', + Csv(c.Id.ToString()), + Csv(c.FirstName), + Csv(c.LastName), + Csv(c.EmailAddress), + Csv(c.PhoneNumber), + Csv(c.Address1), + Csv(c.Address2), + Csv(c.City), + Csv(c.State), + Csv(c.ZipCode))); + } + + var fileName = $"jobflow-clients-{organizationId:N}-{DateTime.UtcNow:yyyyMMddHHmmss}.csv"; + return (Encoding.UTF8.GetBytes(sb.ToString()), fileName); + } + + public async Task<(byte[] Content, string FileName)> BuildZipPackageAsync(Guid organizationId, CancellationToken cancellationToken) + { + var (jsonContent, jsonFileName) = await BuildJsonBundleAsync(organizationId, cancellationToken); + var (csvContent, csvFileName) = await BuildClientsCsvAsync(organizationId, cancellationToken); + + await using var stream = new MemoryStream(); + using (var archive = new ZipArchive(stream, ZipArchiveMode.Create, leaveOpen: true)) + { + var jsonEntry = archive.CreateEntry(jsonFileName, CompressionLevel.Fastest); + await using (var jsonEntryStream = jsonEntry.Open()) + { + await jsonEntryStream.WriteAsync(jsonContent, cancellationToken); + } + + var csvEntry = archive.CreateEntry(csvFileName, CompressionLevel.Fastest); + await using (var csvEntryStream = csvEntry.Open()) + { + await csvEntryStream.WriteAsync(csvContent, cancellationToken); + } + + var readmeEntry = archive.CreateEntry("README.txt", CompressionLevel.Fastest); + await using var readmeStream = readmeEntry.Open(); + await using var writer = new StreamWriter(readmeStream, Encoding.UTF8, leaveOpen: true); + await writer.WriteAsync( + "JobFlow Data Export\n" + + $"Generated at (UTC): {DateTime.UtcNow:O}\n" + + "Contents:\n" + + $"- {jsonFileName} (full organization data bundle)\n" + + $"- {csvFileName} (clients only)\n"); + await writer.FlushAsync(cancellationToken); + } + + var zipName = $"jobflow-export-{organizationId:N}-{DateTime.UtcNow:yyyyMMddHHmmss}.zip"; + return (stream.ToArray(), zipName); + } + + private async Task BuildExportBundleAsync(Guid organizationId, CancellationToken cancellationToken) + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); + + var clients = await dbContext.Set() + .AsNoTracking() + .Where(x => x.OrganizationId == organizationId) + .Select(x => new DataExportClientDto + { + Id = x.Id, + FirstName = x.FirstName, + LastName = x.LastName, + EmailAddress = x.EmailAddress, + PhoneNumber = x.PhoneNumber, + Address1 = x.Address1, + Address2 = x.Address2, + City = x.City, + State = x.State, + ZipCode = x.ZipCode + }) + .ToListAsync(cancellationToken); + + var jobs = await dbContext.Set() + .AsNoTracking() + .Where(x => x.OrganizationClient.OrganizationId == organizationId) + .Select(x => new DataExportJobDto + { + Id = x.Id, + OrganizationClientId = x.OrganizationClientId, + Title = x.Title, + Comments = x.Comments, + LifecycleStatus = x.LifecycleStatus.ToString(), + InvoicingWorkflow = x.InvoicingWorkflow.HasValue ? x.InvoicingWorkflow.Value.ToString() : null + }) + .ToListAsync(cancellationToken); + + var invoices = await dbContext.Set() + .AsNoTracking() + .Where(x => x.OrganizationId == organizationId) + .Select(x => new DataExportInvoiceDto + { + Id = x.Id, + InvoiceNumber = x.InvoiceNumber, + OrganizationClientId = x.OrganizationClientId, + JobId = x.JobId, + InvoiceDate = x.InvoiceDate, + DueDate = x.DueDate, + TotalAmount = x.TotalAmount, + AmountPaid = x.AmountPaid, + BalanceDue = x.TotalAmount - x.AmountPaid, + Status = x.Status.ToString() + }) + .ToListAsync(cancellationToken); + + var employees = await dbContext.Set() + .AsNoTracking() + .Include(x => x.Role) + .Where(x => x.OrganizationId == organizationId) + .Select(x => new DataExportEmployeeDto + { + Id = x.Id, + FirstName = x.FirstName, + LastName = x.LastName, + Email = x.Email, + PhoneNumber = x.PhoneNumber, + RoleName = x.Role.Name, + IsActive = x.IsActive + }) + .ToListAsync(cancellationToken); + + return new DataExportBundleDto + { + ExportedAtUtc = DateTime.UtcNow.ToString("O"), + OrganizationId = organizationId, + Clients = clients, + Jobs = jobs, + Invoices = invoices, + Employees = employees + }; + } + + private static string Csv(string? value) + { + if (value is null) + { + return string.Empty; + } + + if (!value.Contains(',') && !value.Contains('"') && !value.Contains('\n') && !value.Contains('\r')) + { + return value; + } + + return $"\"{value.Replace("\"", "\"\"")}\""; + } +} \ No newline at end of file diff --git a/JobFlow.API/Services/DataExportJobProcessor.cs b/JobFlow.API/Services/DataExportJobProcessor.cs new file mode 100644 index 0000000..385ead0 --- /dev/null +++ b/JobFlow.API/Services/DataExportJobProcessor.cs @@ -0,0 +1,67 @@ +using JobFlow.Domain.Models; +using JobFlow.Infrastructure.Persistence; +using Microsoft.EntityFrameworkCore; + +namespace JobFlow.API.Services; + +public sealed class DataExportJobProcessor +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly DataExportBuilderService _builder; + private readonly ILogger _logger; + + public DataExportJobProcessor( + IDbContextFactory dbContextFactory, + DataExportBuilderService builder, + ILogger logger) + { + _dbContextFactory = dbContextFactory; + _builder = builder; + _logger = logger; + } + + public async Task ProcessAsync(Guid jobId, Guid organizationId) + { + await using var dbContext = await _dbContextFactory.CreateDbContextAsync(); + var job = await dbContext.Set() + .FirstOrDefaultAsync(x => x.Id == jobId && x.OrganizationId == organizationId); + + if (job is null) + { + _logger.LogWarning("Data export job not found. JobId={JobId}, OrganizationId={OrganizationId}", jobId, organizationId); + return; + } + + if (job.Status is "completed" or "running") + { + return; + } + + try + { + job.Status = "running"; + job.StartedAtUtc = DateTime.UtcNow; + job.ErrorMessage = null; + await dbContext.SaveChangesAsync(); + + var (zipContent, zipName) = await _builder.BuildZipPackageAsync(organizationId, CancellationToken.None); + + job.Status = "completed"; + job.FileContent = zipContent; + job.FileName = zipName; + job.ContentType = "application/zip"; + job.CompletedAtUtc = DateTime.UtcNow; + job.ExpiresAtUtc = DateTime.UtcNow.AddDays(7); + await dbContext.SaveChangesAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to process data export job. JobId={JobId}, OrganizationId={OrganizationId}", jobId, organizationId); + + job.Status = "failed"; + job.ErrorMessage = ex.Message; + job.CompletedAtUtc = DateTime.UtcNow; + await dbContext.SaveChangesAsync(); + } + } +} \ No newline at end of file diff --git a/JobFlow.Domain/Models/ClientImportJob.cs b/JobFlow.Domain/Models/ClientImportJob.cs new file mode 100644 index 0000000..0005ffc --- /dev/null +++ b/JobFlow.Domain/Models/ClientImportJob.cs @@ -0,0 +1,18 @@ +namespace JobFlow.Domain.Models; + +public class ClientImportJob : Entity +{ + public Guid OrganizationId { get; set; } + public string SourceSystem { get; set; } = "csv"; + public string Status { get; set; } = "queued"; + public int TotalRows { get; set; } + public int ProcessedRows { get; set; } + public int SucceededRows { get; set; } + public int FailedRows { get; set; } + public string? ErrorMessage { get; set; } + public DateTime? StartedAtUtc { get; set; } + public DateTime? CompletedAtUtc { get; set; } + + public virtual Organization Organization { get; set; } + public virtual ICollection Errors { get; set; } = new List(); +} diff --git a/JobFlow.Domain/Models/ClientImportJobError.cs b/JobFlow.Domain/Models/ClientImportJobError.cs new file mode 100644 index 0000000..c20aeee --- /dev/null +++ b/JobFlow.Domain/Models/ClientImportJobError.cs @@ -0,0 +1,10 @@ +namespace JobFlow.Domain.Models; + +public class ClientImportJobError : Entity +{ + public Guid ClientImportJobId { get; set; } + public int RowNumber { get; set; } + public string Message { get; set; } = string.Empty; + + public virtual ClientImportJob ClientImportJob { get; set; } +} diff --git a/JobFlow.Domain/Models/ClientImportUploadRow.cs b/JobFlow.Domain/Models/ClientImportUploadRow.cs new file mode 100644 index 0000000..1f3482d --- /dev/null +++ b/JobFlow.Domain/Models/ClientImportUploadRow.cs @@ -0,0 +1,10 @@ +namespace JobFlow.Domain.Models; + +public class ClientImportUploadRow : Entity +{ + public Guid ClientImportUploadSessionId { get; set; } + public int RowNumber { get; set; } + public string RowDataJson { get; set; } = string.Empty; + + public virtual ClientImportUploadSession Session { get; set; } +} diff --git a/JobFlow.Domain/Models/ClientImportUploadSession.cs b/JobFlow.Domain/Models/ClientImportUploadSession.cs new file mode 100644 index 0000000..4615965 --- /dev/null +++ b/JobFlow.Domain/Models/ClientImportUploadSession.cs @@ -0,0 +1,14 @@ +namespace JobFlow.Domain.Models; + +public class ClientImportUploadSession : Entity +{ + public Guid OrganizationId { get; set; } + public string SourceSystem { get; set; } = "csv"; + public string Status { get; set; } = "active"; + public int TotalRows { get; set; } + public DateTime ExpiresAtUtc { get; set; } + public DateTime? ConsumedAtUtc { get; set; } + + public virtual Organization Organization { get; set; } + public virtual ICollection Rows { get; set; } = new List(); +} diff --git a/JobFlow.Domain/Models/DataExportJob.cs b/JobFlow.Domain/Models/DataExportJob.cs new file mode 100644 index 0000000..3b0cbb2 --- /dev/null +++ b/JobFlow.Domain/Models/DataExportJob.cs @@ -0,0 +1,18 @@ +namespace JobFlow.Domain.Models; + +public class DataExportJob : Entity +{ + public Guid OrganizationId { get; set; } + public Guid RequestedByUserId { get; set; } + public string Status { get; set; } = "queued"; + public string? ErrorMessage { get; set; } + public string? FileName { get; set; } + public string? ContentType { get; set; } + public byte[]? FileContent { get; set; } + public int DownloadCount { get; set; } + public DateTime? StartedAtUtc { get; set; } + public DateTime? CompletedAtUtc { get; set; } + public DateTime? ExpiresAtUtc { get; set; } + + public virtual Organization Organization { get; set; } +} \ No newline at end of file diff --git a/JobFlow.Infrastructure.Persistence/Configurations/ClientImportJobConfiguration.cs b/JobFlow.Infrastructure.Persistence/Configurations/ClientImportJobConfiguration.cs new file mode 100644 index 0000000..21c0f71 --- /dev/null +++ b/JobFlow.Infrastructure.Persistence/Configurations/ClientImportJobConfiguration.cs @@ -0,0 +1,33 @@ +using JobFlow.Domain.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace JobFlow.Infrastructure.Persistence.Configurations; + +internal sealed class ClientImportJobConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ClientImportJob"); + builder.HasKey(x => x.Id); + + builder.Property(x => x.SourceSystem) + .HasMaxLength(64) + .IsRequired(); + + builder.Property(x => x.Status) + .HasMaxLength(32) + .IsRequired(); + + builder.Property(x => x.ErrorMessage) + .HasMaxLength(2000); + + builder.HasIndex(x => new { x.OrganizationId, x.CreatedAt }); + builder.HasIndex(x => x.Status); + + builder.HasOne(x => x.Organization) + .WithMany() + .HasForeignKey(x => x.OrganizationId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/JobFlow.Infrastructure.Persistence/Configurations/ClientImportJobErrorConfiguration.cs b/JobFlow.Infrastructure.Persistence/Configurations/ClientImportJobErrorConfiguration.cs new file mode 100644 index 0000000..61b2b37 --- /dev/null +++ b/JobFlow.Infrastructure.Persistence/Configurations/ClientImportJobErrorConfiguration.cs @@ -0,0 +1,26 @@ +using JobFlow.Domain.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace JobFlow.Infrastructure.Persistence.Configurations; + +internal sealed class ClientImportJobErrorConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ClientImportJobError"); + builder.HasKey(x => x.Id); + + builder.Property(x => x.Message) + .HasMaxLength(2000) + .IsRequired(); + + builder.HasIndex(x => x.ClientImportJobId); + builder.HasIndex(x => x.RowNumber); + + builder.HasOne(x => x.ClientImportJob) + .WithMany(x => x.Errors) + .HasForeignKey(x => x.ClientImportJobId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/JobFlow.Infrastructure.Persistence/Configurations/ClientImportUploadRowConfiguration.cs b/JobFlow.Infrastructure.Persistence/Configurations/ClientImportUploadRowConfiguration.cs new file mode 100644 index 0000000..bc37331 --- /dev/null +++ b/JobFlow.Infrastructure.Persistence/Configurations/ClientImportUploadRowConfiguration.cs @@ -0,0 +1,25 @@ +using JobFlow.Domain.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace JobFlow.Infrastructure.Persistence.Configurations; + +internal sealed class ClientImportUploadRowConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ClientImportUploadRow"); + builder.HasKey(x => x.Id); + + builder.Property(x => x.RowDataJson) + .IsRequired(); + + builder.HasIndex(x => new { x.ClientImportUploadSessionId, x.RowNumber }) + .IsUnique(); + + builder.HasOne(x => x.Session) + .WithMany(x => x.Rows) + .HasForeignKey(x => x.ClientImportUploadSessionId) + .OnDelete(DeleteBehavior.Cascade); + } +} diff --git a/JobFlow.Infrastructure.Persistence/Configurations/ClientImportUploadSessionConfiguration.cs b/JobFlow.Infrastructure.Persistence/Configurations/ClientImportUploadSessionConfiguration.cs new file mode 100644 index 0000000..64513be --- /dev/null +++ b/JobFlow.Infrastructure.Persistence/Configurations/ClientImportUploadSessionConfiguration.cs @@ -0,0 +1,29 @@ +using JobFlow.Domain.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace JobFlow.Infrastructure.Persistence.Configurations; + +internal sealed class ClientImportUploadSessionConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("ClientImportUploadSession"); + builder.HasKey(x => x.Id); + + builder.Property(x => x.SourceSystem) + .HasMaxLength(64) + .IsRequired(); + + builder.Property(x => x.Status) + .HasMaxLength(32) + .IsRequired(); + + builder.HasIndex(x => new { x.OrganizationId, x.Status, x.ExpiresAtUtc }); + + builder.HasOne(x => x.Organization) + .WithMany() + .HasForeignKey(x => x.OrganizationId) + .OnDelete(DeleteBehavior.Restrict); + } +} diff --git a/JobFlow.Infrastructure.Persistence/Configurations/DataExportJobConfiguration.cs b/JobFlow.Infrastructure.Persistence/Configurations/DataExportJobConfiguration.cs new file mode 100644 index 0000000..bb8b220 --- /dev/null +++ b/JobFlow.Infrastructure.Persistence/Configurations/DataExportJobConfiguration.cs @@ -0,0 +1,35 @@ +using JobFlow.Domain.Models; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +namespace JobFlow.Infrastructure.Persistence.Configurations; + +internal sealed class DataExportJobConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("DataExportJob"); + builder.HasKey(x => x.Id); + + builder.Property(x => x.Status) + .HasMaxLength(32) + .IsRequired(); + + builder.Property(x => x.ErrorMessage) + .HasMaxLength(2000); + + builder.Property(x => x.FileName) + .HasMaxLength(255); + + builder.Property(x => x.ContentType) + .HasMaxLength(128); + + builder.HasIndex(x => new { x.OrganizationId, x.CreatedAt }); + builder.HasIndex(x => new { x.OrganizationId, x.Status }); + + builder.HasOne(x => x.Organization) + .WithMany() + .HasForeignKey(x => x.OrganizationId) + .OnDelete(DeleteBehavior.Restrict); + } +} \ No newline at end of file diff --git a/JobFlow.Infrastructure.Persistence/JobFlowDbContext.cs b/JobFlow.Infrastructure.Persistence/JobFlowDbContext.cs index 29dc8fa..76fd7ad 100644 --- a/JobFlow.Infrastructure.Persistence/JobFlowDbContext.cs +++ b/JobFlow.Infrastructure.Persistence/JobFlowDbContext.cs @@ -1,6 +1,5 @@ using JobFlow.Domain.Models; using System.Linq.Expressions; -using JobFlow.Domain.Models; using Microsoft.EntityFrameworkCore; namespace JobFlow.Infrastructure.Persistence; @@ -30,6 +29,11 @@ public JobFlowDbContext(DbContextOptions options) : base(options) public DbSet FollowUpSteps { get; set; } public DbSet FollowUpRuns { get; set; } public DbSet FollowUpExecutionLogs { get; set; } + public DbSet ClientImportJobs { get; set; } + public DbSet ClientImportJobErrors { get; set; } + public DbSet ClientImportUploadSessions { get; set; } + public DbSet ClientImportUploadRows { get; set; } + public DbSet DataExportJobs { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { diff --git a/JobFlow.Infrastructure.Persistence/Migrations/20260327163735_AddClientImportJobTracking.Designer.cs b/JobFlow.Infrastructure.Persistence/Migrations/20260327163735_AddClientImportJobTracking.Designer.cs new file mode 100644 index 0000000..d24a171 --- /dev/null +++ b/JobFlow.Infrastructure.Persistence/Migrations/20260327163735_AddClientImportJobTracking.Designer.cs @@ -0,0 +1,3735 @@ +// +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("20260327163735_AddClientImportJobTracking")] + partial class AddClientImportJobTracking + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.3") + .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.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.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.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.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("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.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.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.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("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + 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.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("Quantity") + .HasColumnType("int"); + + 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.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("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("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.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("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("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("OrganizationName") + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationTypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("PaymentProvider") + .HasColumnType("int"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("SquareMerchantId") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("StripeConnectAccountId") + .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(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("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"); + + 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("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + 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.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.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.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.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); + }); + + 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("IsActive") + .HasColumnType("bit"); + + 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.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.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.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.Navigation("Estimate"); + }); + + 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.Navigation("Invoice"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Job", b => + { + b.HasOne("JobFlow.Domain.Models.OrganizationClient", "OrganizationClient") + .WithMany("Jobs") + .HasForeignKey("OrganizationClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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.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.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.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.Conversation", b => + { + b.Navigation("Messages"); + + b.Navigation("Participants"); + }); + + 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.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.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/20260327163735_AddClientImportJobTracking.cs b/JobFlow.Infrastructure.Persistence/Migrations/20260327163735_AddClientImportJobTracking.cs new file mode 100644 index 0000000..7aba85e --- /dev/null +++ b/JobFlow.Infrastructure.Persistence/Migrations/20260327163735_AddClientImportJobTracking.cs @@ -0,0 +1,104 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace JobFlow.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddClientImportJobTracking : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ClientImportJob", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + OrganizationId = table.Column(type: "uniqueidentifier", nullable: false), + SourceSystem = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + Status = table.Column(type: "nvarchar(32)", maxLength: 32, nullable: false), + TotalRows = table.Column(type: "int", nullable: false), + ProcessedRows = table.Column(type: "int", nullable: false), + SucceededRows = table.Column(type: "int", nullable: false), + FailedRows = table.Column(type: "int", nullable: false), + ErrorMessage = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: true), + StartedAtUtc = table.Column(type: "datetime2", nullable: true), + CompletedAtUtc = table.Column(type: "datetime2", nullable: true), + 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_ClientImportJob", x => x.Id); + table.ForeignKey( + name: "FK_ClientImportJob_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ClientImportJobError", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ClientImportJobId = table.Column(type: "uniqueidentifier", nullable: false), + RowNumber = table.Column(type: "int", nullable: false), + Message = table.Column(type: "nvarchar(2000)", maxLength: 2000, 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_ClientImportJobError", x => x.Id); + table.ForeignKey( + name: "FK_ClientImportJobError_ClientImportJob_ClientImportJobId", + column: x => x.ClientImportJobId, + principalTable: "ClientImportJob", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ClientImportJob_OrganizationId_CreatedAt", + table: "ClientImportJob", + columns: new[] { "OrganizationId", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_ClientImportJob_Status", + table: "ClientImportJob", + column: "Status"); + + migrationBuilder.CreateIndex( + name: "IX_ClientImportJobError_ClientImportJobId", + table: "ClientImportJobError", + column: "ClientImportJobId"); + + migrationBuilder.CreateIndex( + name: "IX_ClientImportJobError_RowNumber", + table: "ClientImportJobError", + column: "RowNumber"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ClientImportJobError"); + + migrationBuilder.DropTable( + name: "ClientImportJob"); + } + } +} diff --git a/JobFlow.Infrastructure.Persistence/Migrations/20260327164425_AddClientImportUploadSessionPersistence.Designer.cs b/JobFlow.Infrastructure.Persistence/Migrations/20260327164425_AddClientImportUploadSessionPersistence.Designer.cs new file mode 100644 index 0000000..032d64c --- /dev/null +++ b/JobFlow.Infrastructure.Persistence/Migrations/20260327164425_AddClientImportUploadSessionPersistence.Designer.cs @@ -0,0 +1,3857 @@ +// +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("20260327164425_AddClientImportUploadSessionPersistence")] + partial class AddClientImportUploadSessionPersistence + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.3") + .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.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.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.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("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.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.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.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("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + 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.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("Quantity") + .HasColumnType("int"); + + 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.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("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("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.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("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("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("OrganizationName") + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationTypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("PaymentProvider") + .HasColumnType("int"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("SquareMerchantId") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("StripeConnectAccountId") + .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(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("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"); + + 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("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + 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.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.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.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.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); + }); + + 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("IsActive") + .HasColumnType("bit"); + + 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.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.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.Navigation("Estimate"); + }); + + 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.Navigation("Invoice"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Job", b => + { + b.HasOne("JobFlow.Domain.Models.OrganizationClient", "OrganizationClient") + .WithMany("Jobs") + .HasForeignKey("OrganizationClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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.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.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.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.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.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.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/20260327164425_AddClientImportUploadSessionPersistence.cs b/JobFlow.Infrastructure.Persistence/Migrations/20260327164425_AddClientImportUploadSessionPersistence.cs new file mode 100644 index 0000000..e5bceac --- /dev/null +++ b/JobFlow.Infrastructure.Persistence/Migrations/20260327164425_AddClientImportUploadSessionPersistence.cs @@ -0,0 +1,91 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace JobFlow.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddClientImportUploadSessionPersistence : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "ClientImportUploadSession", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + OrganizationId = table.Column(type: "uniqueidentifier", nullable: false), + SourceSystem = table.Column(type: "nvarchar(64)", maxLength: 64, nullable: false), + Status = table.Column(type: "nvarchar(32)", maxLength: 32, nullable: false), + TotalRows = table.Column(type: "int", nullable: false), + ExpiresAtUtc = table.Column(type: "datetime2", nullable: false), + ConsumedAtUtc = table.Column(type: "datetime2", nullable: true), + 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_ClientImportUploadSession", x => x.Id); + table.ForeignKey( + name: "FK_ClientImportUploadSession_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateTable( + name: "ClientImportUploadRow", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + ClientImportUploadSessionId = table.Column(type: "uniqueidentifier", nullable: false), + RowNumber = table.Column(type: "int", nullable: false), + RowDataJson = table.Column(type: "nvarchar(max)", 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_ClientImportUploadRow", x => x.Id); + table.ForeignKey( + name: "FK_ClientImportUploadRow_ClientImportUploadSession_ClientImportUploadSessionId", + column: x => x.ClientImportUploadSessionId, + principalTable: "ClientImportUploadSession", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + migrationBuilder.CreateIndex( + name: "IX_ClientImportUploadRow_ClientImportUploadSessionId_RowNumber", + table: "ClientImportUploadRow", + columns: new[] { "ClientImportUploadSessionId", "RowNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_ClientImportUploadSession_OrganizationId_Status_ExpiresAtUtc", + table: "ClientImportUploadSession", + columns: new[] { "OrganizationId", "Status", "ExpiresAtUtc" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "ClientImportUploadRow"); + + migrationBuilder.DropTable( + name: "ClientImportUploadSession"); + } + } +} diff --git a/JobFlow.Infrastructure.Persistence/Migrations/20260327165440_AddAsyncDataExportJobs.Designer.cs b/JobFlow.Infrastructure.Persistence/Migrations/20260327165440_AddAsyncDataExportJobs.Designer.cs new file mode 100644 index 0000000..b552810 --- /dev/null +++ b/JobFlow.Infrastructure.Persistence/Migrations/20260327165440_AddAsyncDataExportJobs.Designer.cs @@ -0,0 +1,3939 @@ +// +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("20260327165440_AddAsyncDataExportJobs")] + partial class AddAsyncDataExportJobs + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.3") + .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.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.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("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.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.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.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("CreatedAt") + .HasColumnType("datetime2"); + + b.Property("CreatedBy") + .HasColumnType("nvarchar(max)"); + + b.Property("DeactivatedAtUtc") + .HasColumnType("datetime2"); + + b.Property("DueDate") + .HasColumnType("datetime2"); + + 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.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("Quantity") + .HasColumnType("int"); + + 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.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("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("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.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("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("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("OrganizationName") + .HasColumnType("nvarchar(max)"); + + b.Property("OrganizationTypeId") + .HasColumnType("uniqueidentifier"); + + b.Property("PaymentProvider") + .HasColumnType("int"); + + b.Property("PhoneNumber") + .HasColumnType("nvarchar(max)"); + + b.Property("SquareMerchantId") + .HasColumnType("nvarchar(max)"); + + b.Property("State") + .HasColumnType("nvarchar(max)"); + + b.Property("StripeConnectAccountId") + .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(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("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"); + + 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("IsActive") + .HasColumnType("bit"); + + b.Property("OrganizationId") + .HasColumnType("uniqueidentifier"); + + 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.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.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.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.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); + }); + + 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("IsActive") + .HasColumnType("bit"); + + 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.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.Navigation("Estimate"); + }); + + 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.Navigation("Invoice"); + }); + + modelBuilder.Entity("JobFlow.Domain.Models.Job", b => + { + b.HasOne("JobFlow.Domain.Models.OrganizationClient", "OrganizationClient") + .WithMany("Jobs") + .HasForeignKey("OrganizationClientId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + 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.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.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.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.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.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.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/20260327165440_AddAsyncDataExportJobs.cs b/JobFlow.Infrastructure.Persistence/Migrations/20260327165440_AddAsyncDataExportJobs.cs new file mode 100644 index 0000000..cb90d4c --- /dev/null +++ b/JobFlow.Infrastructure.Persistence/Migrations/20260327165440_AddAsyncDataExportJobs.cs @@ -0,0 +1,66 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace JobFlow.Infrastructure.Persistence.Migrations +{ + /// + public partial class AddAsyncDataExportJobs : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "DataExportJob", + columns: table => new + { + Id = table.Column(type: "uniqueidentifier", nullable: false), + OrganizationId = table.Column(type: "uniqueidentifier", nullable: false), + RequestedByUserId = table.Column(type: "uniqueidentifier", nullable: false), + Status = table.Column(type: "nvarchar(32)", maxLength: 32, nullable: false), + ErrorMessage = table.Column(type: "nvarchar(2000)", maxLength: 2000, nullable: true), + FileName = table.Column(type: "nvarchar(255)", maxLength: 255, nullable: true), + ContentType = table.Column(type: "nvarchar(128)", maxLength: 128, nullable: true), + FileContent = table.Column(type: "varbinary(max)", nullable: true), + DownloadCount = table.Column(type: "int", nullable: false), + StartedAtUtc = table.Column(type: "datetime2", nullable: true), + CompletedAtUtc = table.Column(type: "datetime2", nullable: true), + ExpiresAtUtc = table.Column(type: "datetime2", nullable: true), + 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_DataExportJob", x => x.Id); + table.ForeignKey( + name: "FK_DataExportJob_Organization_OrganizationId", + column: x => x.OrganizationId, + principalTable: "Organization", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + }); + + migrationBuilder.CreateIndex( + name: "IX_DataExportJob_OrganizationId_CreatedAt", + table: "DataExportJob", + columns: new[] { "OrganizationId", "CreatedAt" }); + + migrationBuilder.CreateIndex( + name: "IX_DataExportJob_OrganizationId_Status", + table: "DataExportJob", + columns: new[] { "OrganizationId", "Status" }); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "DataExportJob"); + } + } +} diff --git a/JobFlow.Infrastructure.Persistence/Migrations/JobFlowDbContextModelSnapshot.cs b/JobFlow.Infrastructure.Persistence/Migrations/JobFlowDbContextModelSnapshot.cs index 92e92c0..3d36dea 100644 --- a/JobFlow.Infrastructure.Persistence/Migrations/JobFlowDbContextModelSnapshot.cs +++ b/JobFlow.Infrastructure.Persistence/Migrations/JobFlowDbContextModelSnapshot.cs @@ -195,6 +195,213 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AssignmentOrder", (string)null); }); + 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") @@ -324,6 +531,77 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") @@ -2990,6 +3268,50 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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") @@ -3030,6 +3352,17 @@ protected override void BuildModel(ModelBuilder modelBuilder) .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") @@ -3486,6 +3819,16 @@ protected override void BuildModel(ModelBuilder modelBuilder) 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"); diff --git a/JobFlow.Tests/JobFlow.Tests.csproj b/JobFlow.Tests/JobFlow.Tests.csproj index e7d0bf2..30419fe 100644 --- a/JobFlow.Tests/JobFlow.Tests.csproj +++ b/JobFlow.Tests/JobFlow.Tests.csproj @@ -20,6 +20,7 @@ + diff --git a/JobFlow.Tests/OrganizationClientControllerSwaggerSignatureTests.cs b/JobFlow.Tests/OrganizationClientControllerSwaggerSignatureTests.cs new file mode 100644 index 0000000..1086890 --- /dev/null +++ b/JobFlow.Tests/OrganizationClientControllerSwaggerSignatureTests.cs @@ -0,0 +1,34 @@ +using JobFlow.API.Controllers; +using JobFlow.API.Models; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace JobFlow.Tests; + +public class OrganizationClientControllerSwaggerSignatureTests +{ + [Fact] + public void PreviewClientImport_UsesFromFormRequestDto_AndConsumesMultipartFormData() + { + var method = typeof(OrganizationClientController) + .GetMethod(nameof(OrganizationClientController.PreviewClientImport)); + + Assert.NotNull(method); + + var parameters = method!.GetParameters(); + Assert.NotEmpty(parameters); + + var requestParam = parameters[0]; + Assert.Equal(typeof(PreviewClientImportRequest), requestParam.ParameterType); + Assert.NotNull(requestParam.GetCustomAttributes(typeof(FromFormAttribute), inherit: true).SingleOrDefault()); + + Assert.DoesNotContain(parameters, p => p.ParameterType == typeof(IFormFile)); + + var consumes = method.GetCustomAttributes(typeof(ConsumesAttribute), inherit: true) + .Cast() + .SingleOrDefault(); + + Assert.NotNull(consumes); + Assert.Contains("multipart/form-data", consumes!.ContentTypes); + } +}